前端工程化的痛点
每个前端项目都会遇到这些问题:
- 依赖版本冲突(同事A的node_modules能跑,B的跑不起来)
- 构建速度慢(每次npm run build要等3分钟)
- 代码风格不统一(Prettier和ESLint打起来了)
- 部署到测试环境需要手动scp
这篇文章用MonkeyCode生成完整的Vue3项目工程化方案,包括配置、脚本和CI/CD。
给MonkeyCode的统一Prompt
用Vite + Vue3 + TypeScript搭建前端项目,要求:
1. ESLint + Prettier + stylelint配置
2. Pinia状态管理
3. Vue Router路由
4. Axios封装(拦截器、错误处理)
5. 环境变量配置(dev/staging/prod)
6. 组件自动导入(unplugin-vue-components)
7. Git Hooks(commit前检查、commit信息规范)
8. Docker构建配置
9. GitHub Actions CI/CD流水线
项目是一个后台管理系统,包含用户管理、订单管理模块
MonkeyCode生成的完整项目结构
my-vue3-admin/
├── .github/
│ └── workflows/
│ └── ci-cd.yml
├── .husky/
│ ├── commit-msg
│ └── pre-commit
├── docker/
│ ├── Dockerfile
│ └── nginx.conf
├── src/
│ ├── api/
│ │ ├── index.ts # Axios封装
│ │ ├── user.ts # 用户API
│ │ └── order.ts # 订单API
│ ├── assets/
│ │ └── styles/
│ │ ├── variables.scss
│ │ └── common.scss
│ ├── components/
│ │ ├── common/ # 通用组件
│ │ └── business/ # 业务组件
│ ├── composables/ # 组合式函数
│ ├── layouts/
│ │ ├── DefaultLayout.vue
│ │ └── AuthLayout.vue
│ ├── router/
│ │ └── index.ts
│ ├── stores/ # Pinia stores
│ │ ├── user.ts
│ │ └── order.ts
│ ├── views/
│ │ ├── user/
│ │ │ ├── List.vue
│ │ │ └── Detail.vue
│ │ └── order/
│ │ ├── List.vue
│ │ └── Detail.vue
│ ├── App.vue
│ └── main.ts
├── env.d.ts
├── index.html
├── package.json
├── tsconfig.json
├── vite.config.ts
├── .eslintrc.cjs
├── .prettierrc
├── .stylelintrc.cjs
└── docker-compose.yml
MonkeyCode生成的配置文件
// vite.config.ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import { resolve } from 'path'
import AutoImport from 'unplugin-auto-import/vite'
import Components from 'unplugin-vue-components/vite'
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
import viteCompression from 'vite-plugin-compression'
export default defineConfig({
plugins: [
vue(),
// 自动导入(API + 组件)
AutoImport({
imports: [
'vue',
'vue-router',
'pinia',
{
'axios': ['default']
}
],
resolvers: [ElementPlusResolver()],
dts: 'src/auto-imports.d.ts',
}),
Components({
resolvers: [ElementPlusResolver()],
dts: 'src/auto-components.d.ts',
}),
// Gzip压缩
viteCompression({
algorithm: 'gzip',
ext: '.gz',
}),
],
resolve: {
alias: {
'@': resolve(__dirname, 'src'),
},
},
// 构建优化
build: {
target: 'es2015',
minify: 'terser',
terserOptions: {
compress: {
drop_console: true,
drop_debugger: true,
},
},
rollupOptions: {
output: {
// 拆分vendor
manualChunks: {
'vue-vendor': ['vue', 'vue-router', 'pinia'],
'element-plus': ['element-plus'],
'echarts': ['echarts'],
},
},
},
},
// 开发服务器配置
server: {
port: 3000,
proxy: {
'/api': {
target: 'http://localhost:8080',
changeOrigin: true,
},
},
},
})
// .eslintrc.cjs
module.exports = {
root: true,
env: {
browser: true,
es2021: true,
node: true,
},
extends: [
'eslint:recommended',
'plugin:vue/vue3-recommended',
'plugin:@typescript-eslint/recommended',
'plugin:prettier/recommended', // 最后加载,覆盖ESLint规则
],
parser: 'vue-eslint-parser',
parserOptions: {
ecmaVersion: 'latest',
parser: '@typescript-eslint/parser',
sourceType: 'module',
},
plugins: ['vue', '@typescript-eslint'],
rules: {
'vue/multi-word-component-names': 'off',
'@typescript-eslint/no-explicit-any': 'warn',
'@typescript-eslint/no-unused-vars': [
'warn',
{ argsIgnorePattern: '^_' },
],
'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
},
}
// .prettierrc
{
"semi": false,
"singleQuote": true,
"printWidth": 100,
"tabWidth": 2,
"trailingComma": "es5",
"bracketSpacing": true,
"arrowParens": "avoid",
"endOfLine": "lf"
}
Axios封装(企业级)
// src/api/index.ts
import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios'
import { ElMessage } from 'element-plus'
import router from '@/router'
// 环境配置
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL
// 创建axios实例
const service: AxiosInstance = axios.create({
baseURL: API_BASE_URL,
timeout: 30000,
headers: {
'Content-Type': 'application/json',
},
})
// 请求拦截器
service.interceptors.request.use(
(config) => {
// 添加Token
const token = localStorage.getItem('token')
if (token) {
config.headers.Authorization = `Bearer ${token}`
}
// 添加租户ID
const tenantId = localStorage.getItem('tenantId')
if (tenantId) {
config.headers['X-Tenant-ID'] = tenantId
}
// 请求防重复(POST/PUT/DELETE)
const method = config.method?.toUpperCase()
if (['POST', 'PUT', 'DELETE'].includes(method || '')) {
const requestKey = `${config.url}-${JSON.stringify(config.data)}`
if (window.__requestCache?.has(requestKey)) {
return Promise.reject(new Error('重复请求'))
}
window.__requestCache?.add(requestKey)
setTimeout(() => window.__requestCache?.delete(requestKey), 5000)
}
return config
},
(error) => {
return Promise.reject(error)
}
)
// 响应拦截器
service.interceptors.response.use(
(response: AxiosResponse) => {
const res = response.data
// 业务状态码处理
if (response.config.responseType === 'blob') {
// 文件下载直接返回
return response
}
if (res.code !== 200 && res.code !== 0) {
// 业务错误
ElMessage.error(res.message || '请求失败')
// Token过期
if (res.code === 401) {
localStorage.removeItem('token')
router.push('/login')
}
return Promise.reject(new Error(res.message || '请求失败'))
}
return res
},
(error) => {
// 网络错误处理
let message = '网络错误,请稍后重试'
if (error.code === 'ECONNABORTED') {
message = '请求超时,请重试'
} else if (error.response) {
switch (error.response.status) {
case 400:
message = '请求参数错误'
break
case 401:
message = '登录已过期,请重新登录'
localStorage.removeItem('token')
router.push('/login')
break
case 403:
message = '没有权限'
break
case 404:
message = '请求资源不存在'
break
case 500:
message = '服务器错误'
break
}
}
ElMessage.error(message)
return Promise.reject(error)
}
)
// 扩展Request方法
service.form = <T = any>(url: string, data?: object, config?: AxiosRequestConfig) => {
return service.post<T>(url, data, {
...config,
headers: { 'Content-Type': 'multipart/form-data' },
})
}
service.download = <T = any>(url: string, data?: object, config?: AxiosRequestConfig) => {
return service.post<T>(url, data, {
...config,
responseType: 'blob',
})
}
export default service
// src/auto-imports.d.ts - 自动导入的类型声明
declare global {
const service: typeof import('./api/index').default
const useUserStore: typeof import('@/stores/user').useUserStore
const useOrderStore: typeof import('@/stores/order').useOrderStore
}
Pinia Stores
// src/stores/user.ts
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
import { login, logout, getUserInfo } from '@/api/user'
interface UserInfo {
id: number
username: string
nickname: string
avatar: string
roles: string[]
permissions: string[]
}
export const useUserStore = defineStore('user', () => {
// State
const token = ref(localStorage.getItem('token') || '')
const userInfo = ref<UserInfo | null>(null)
const permissions = ref<string[]>([])
// Getters
const isLoggedIn = computed(() => !!token.value)
const hasPermission = computed(() => (perm: string) => permissions.value.includes(perm))
// Actions
async function loginAction(username: string, password: string) {
const res = await login({ username, password })
token.value = res.data.token
localStorage.setItem('token', res.data.token)
return res.data
}
async function getUserInfoAction() {
const res = await getUserInfo()
userInfo.value = res.data.userInfo
permissions.value = res.data.permissions
return res.data
}
async function logoutAction() {
await logout()
token.value = ''
userInfo.value = null
permissions.value = []
localStorage.removeItem('token')
}
return {
token,
userInfo,
permissions,
isLoggedIn,
hasPermission,
loginAction,
getUserInfoAction,
logoutAction,
}
})
Git Hooks配置
# .husky/commit-msg
#!/bin/sh
npx --no -- commitlint --edit "$1"
# .husky/pre-commit
#!/bin/sh
npx lint-staged
// package.json 添加
{
"lint-staged": {
"*.{vue,ts,tsx,js,jsx}": [
"eslint --fix",
"prettier --write"
],
"*.{css,scss,vue}": [
"stylelint --fix",
"prettier --write"
]
},
"commitlint": {
"extends": ["@commitlint/config-conventional"],
"rules": {
"type-enum": [
2,
"always",
[
"feat", // 新功能
"fix", // Bug修复
"docs", // 文档更新
"style", // 代码格式
"refactor", // 重构
"perf", // 性能优化
"test", // 测试
"chore", // 构建/工具
"revert" // 回滚
]
]
}
}
}
Docker + Nginx配置
# docker/Dockerfile
FROM node:18-alpine as builder
WORKDIR /app
COPY package.json ./
RUN npm install --registry=https://registry.npmmirror.com
COPY . .
RUN npm run build
# 生产镜像
FROM nginx:alpine
# 复制构建产物
COPY --from=builder /app/dist /usr/share/nginx/html
# 复制nginx配置
COPY docker/nginx.conf /etc/nginx/nginx.conf
# 复制入口脚本
COPY docker/entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh
EXPOSE 80
ENTRYPOINT ["/entrypoint.sh"]
# docker/nginx.conf
worker_processes auto;
error_log /var/log/nginx/error.log warn;
pid /var/run/nginx.pid;
events {
worker_connections 1024;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
access_log /var/log/nginx/access.log main;
sendfile on;
tcp_nopush on;
keepalive_timeout 65;
gzip on;
gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;
gzip_min_length 1000;
server {
listen 80;
server_name localhost;
root /usr/share/nginx/html;
index index.html;
# SPA history模式支持
location / {
try_files $uri $uri/ /index.html;
}
# 静态资源缓存
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
expires 30d;
add_header Cache-Control "public, immutable";
}
# API代理
location /api/ {
proxy_pass http://backend:8080/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
# 健康检查
location /health {
access_log off;
return 200 "OK";
}
}
}
GitHub Actions CI/CD
# .github/workflows/ci-cd.yml
name: CI/CD
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
env:
NODE_VERSION: '18'
jobs:
lint:
name: Lint & Type Check
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Run ESLint
run: npm run lint
- name: Run Type Check
run: npm run type-check
- name: Upload coverage
uses: codecov/codecov-action@v3
with:
files: ./coverage/lcov.info
build:
name: Build
runs-on: ubuntu-latest
needs: lint
strategy:
matrix:
environment: [staging, production]
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Build
run: npm run build:${{ matrix.environment }}
env:
VITE_API_BASE_URL: ${{ vars.API_BASE_URL }}
- name: Upload build artifacts
uses: actions/upload-artifact@v4
with:
name: dist-${{ matrix.environment }}
path: dist/
deploy:
name: Deploy to ${{ matrix.environment }}
runs-on: ubuntu-latest
needs: build
if: github.ref == 'refs/heads/main'
environment:
name: ${{ matrix.environment }}
url: ${{ matrix.environment == 'production' && vars.PRODUCTION_URL || vars.STAGING_URL }}
strategy:
matrix:
include:
- environment: staging
server: ${{ secrets.STAGING_SERVER }}
- environment: production
server: ${{ secrets.PRODUCTION_SERVER }}
steps:
- name: Download artifacts
uses: actions/download-artifact@v4
with:
name: dist-${{ matrix.environment }}
path: dist/
- name: Deploy via SSH
uses: appleboy/ssh-action@v1
with:
host: ${{ matrix.server.host }}
username: ${{ matrix.server.user }}
key: ${{ matrix.server.key }}
script: |
cd /var/www/my-vue3-admin
docker-compose pull
docker-compose up -d
docker system prune -f
一键启动脚本
#!/bin/bash
# scripts/init.sh
# 安装依赖
echo " Installing dependencies..."
npm install
# 启动开发服务器
echo " Starting dev server..."
npm run dev
# 或者构建生产版本
# npm run build:staging
# npm run build:production
用MonkeyCode生成前端工程化配置的优势:一次性拿到完整的配置模板,不需要东拼西凑。但环境变量和部署脚本需要根据实际情况修改。
文章摘自:https://www.cnblogs.com/jaryn/p/20218949
