Vue 项目工程化配置
大白话解释: 工程化就是"把项目搭建得规范、易维护"。就像盖房子需要设计图纸、施工规范一样,写代码也需要统一的目录结构、代码规范、构建配置。
为什么要工程化?
- 团队协作:大家用一样的目录结构和规范,看代码更方便
- 代码质量:ESLint、Prettier 自动检查,减少低级错误
- 开发效率:Vite 快速启动,热更新即时生效
- 易于维护:清晰的目录结构,新成员快速上手
工程化包含什么?
- 项目创建:用 Vite 脚手架快速创建
- 目录结构:按功能分类(api、components、pages、stores 等)
- 代码规范:ESLint + Prettier + EditorConfig
- Git 规范:husky + lint-staged + commitlint
- 构建配置:Vite 配置、环境变量、路径别名
Vue 3 + Vite 项目的工程化最佳实践,涵盖项目创建、配置、规范、部署全流程。
项目创建
bash
# 创建 Vue 3 + TypeScript 项目
yarn create vite my-vue-app --template vue-ts
# 进入项目安装依赖
cd my-vue-app
yarn install目录结构
src/
├── api/ # 接口请求
│ ├── modules/ # 按业务拆分
│ │ ├── user.ts
│ │ └── order.ts
│ └── request.ts # axios 封装
├── assets/ # 静态资源(会被构建处理)
│ ├── images/
│ └── fonts/
├── components/ # 公共组件
│ ├── common/ # 通用基础组件
│ └── business/ # 业务组件
├── composables/ # 组合式函数
│ ├── useFetch.ts
│ └── useAuth.ts
├── constants/ # 常量
│ └── index.ts
├── directives/ # 自定义指令
│ └── permission.ts
├── layouts/ # 布局组件
│ ├── DefaultLayout.vue
│ └── AdminLayout.vue
├── pages/ # 页面组件(按路由拆分)
│ ├── home/
│ │ └── index.vue
│ ├── user/
│ │ ├── list.vue
│ │ └── detail.vue
│ └── login.vue
├── router/ # 路由配置
│ ├── index.ts
│ └── guards.ts
├── stores/ # Pinia 状态
│ ├── user.ts
│ └── app.ts
├── styles/ # 全局样式
│ ├── variables.scss
│ ├── mixins.scss
│ └── global.scss
├── types/ # 类型定义
│ ├── api.d.ts
│ └── model.d.ts
├── utils/ # 工具函数
│ ├── format.ts
│ └── storage.ts
├── App.vue
├── main.ts
└── env.d.tsVite 配置
Vite 完整配置详见 Vue 3 + Vite 开发指南。
ts
// vite.config.ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import path from 'path'
import { fileURLToPath } from 'url'
const __dirname = path.dirname(fileURLToPath(import.meta.url))
export default defineConfig({
plugins: [vue()],
resolve: {
alias: {
'@': path.resolve(__dirname, 'src'),
},
},
server: {
port: 3000,
open: true,
proxy: {
'/api': {
target: 'http://localhost:8080',
changeOrigin: true,
rewrite: (reqPath) => reqPath.replace(/^\/api/, ''),
},
},
},
build: {
outDir: 'dist',
rollupOptions: {
output: {
manualChunks: {
'vue-vendor': ['vue', 'vue-router', 'pinia'],
},
},
},
},
})环境变量
Vite 项目使用 .env 文件管理环境变量,详见 环境变量管理。
bash
# .env.development —— 开发环境
VITE_API_BASE_URL=http://localhost:8080
# .env.production —— 生产环境
VITE_API_BASE_URL=https://api.example.comts
// 使用
const apiUrl = import.meta.env.VITE_API_BASE_URL
const isDev = import.meta.env.DEV💡 只有
VITE_前缀的变量才会暴露给客户端。
TypeScript 配置
Vue 3 + Vite 项目的 TypeScript 配置,详见 TypeScript 常用命令。
json
// tsconfig.json
{
"compilerOptions": {
"target": "ESNext",
"module": "ESNext",
"moduleResolution": "bundler",
"strict": true,
"jsx": "preserve",
"resolveJsonModule": true,
"isolatedModules": true,
"esModuleInterop": true,
"lib": ["ESNext", "DOM", "DOM.Iterable"],
"skipLibCheck": true,
"noEmit": true,
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]
},
"types": ["vite/client", "node"]
},
"include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.vue"],
"exclude": ["node_modules", "dist"]
}ESLint + Prettier
详见 ESLint 常用命令 和 Prettier 常用命令。
bash
yarn add -D eslint @vue/eslint-config-typescript @vue/eslint-config-prettier eslint-plugin-vue💡 以下为 ESLint 8 的配置格式(
.eslintrc.cjs)。ESLint 9+ 已改为 flat config(eslint.config.js),API 不同。新项目建议参考 ESLint 官方迁移指南。
js
// .eslintrc.cjs(ESLint 8 格式)
module.exports = {
root: true,
env: {
browser: true,
es2021: true,
node: true,
},
extends: [
'plugin:vue/vue3-recommended',
'@vue/eslint-config-typescript',
'@vue/eslint-config-prettier',
],
parserOptions: {
ecmaVersion: 'latest',
},
rules: {
'vue/multi-word-component-names': 'off',
'@typescript-eslint/no-unused-vars': 'warn',
'@typescript-eslint/no-explicit-any': 'warn',
'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off',
},
}ESLint 9+ Flat Config 示例
js
// eslint.config.js(ESLint 9+ 格式)
import js from '@eslint/js'
import tseslint from 'typescript-eslint'
import pluginVue from 'eslint-plugin-vue'
import prettier from 'eslint-config-prettier'
export default [
// 基础规则
js.configs.recommended,
// TypeScript 规则
...tseslint.configs.recommended,
// Vue 规则
...pluginVue.configs['flat/recommended'],
// 关闭与 Prettier 冲突的规则
prettier,
{
rules: {
'vue/multi-word-component-names': 'off',
'@typescript-eslint/no-unused-vars': 'warn',
'@typescript-eslint/no-explicit-any': 'warn',
'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off',
},
},
]💡 Flat Config 的核心变化:不再使用
extends/plugins字符串,改为直接导入配置对象并放入数组;不再需要.eslintrc.*文件,配置集中在eslint.config.js一个文件中。
json
// .prettierrc
{
"semi": false,
"singleQuote": true,
"trailingComma": "all",
"printWidth": 100,
"tabWidth": 2,
"arrowParens": "always",
"endOfLine": "auto"
}json
// .prettierignore
dist
node_modules
*.mdGit Hooks
使用 husky + lint-staged 在提交时自动检查代码,详见 husky + lint-staged 常用命令。
bash
yarn add -D husky lint-staged
npx husky initjson
// package.json
{
"lint-staged": {
"*.{vue,ts,tsx}": "eslint --fix",
"*.{css,scss,json,md}": "prettier --write"
}
}测试配置(Vitest)
Vue 3 + Vite 项目推荐使用 Vitest 作为测试框架。
bash
# happy-dom 更轻量快速,推荐首选;jsdom 兼容性更全面但较慢,按需二选一
yarn add -D vitest @vue/test-utils happy-domts
// vitest.config.ts
import { defineConfig } from 'vitest/config'
import vue from '@vitejs/plugin-vue'
export default defineConfig({
plugins: [vue()],
test: {
environment: 'happy-dom', // 或 'jsdom'
globals: true, // 全局 API(describe/it/expect)
include: ['src/**/*.{test,spec}.{ts,tsx}'],
coverage: {
provider: 'v8',
reporter: ['text', 'json', 'html'],
},
},
})ts
// src/stores/__tests__/counter.test.ts
import { describe, it, expect, beforeEach } from 'vitest'
import { setActivePinia, createPinia } from 'pinia'
import { useCounterStore } from '../counter'
describe('Counter Store', () => {
beforeEach(() => {
setActivePinia(createPinia())
})
it('初始值为 0', () => {
const store = useCounterStore()
expect(store.count).toBe(0)
})
it('increment 正常工作', () => {
const store = useCounterStore()
store.increment()
expect(store.count).toBe(1)
})
})ts
// src/components/__tests__/Button.test.ts
import { describe, it, expect } from 'vitest'
import { mount } from '@vue/test-utils'
import MyButton from '../MyButton.vue'
describe('MyButton', () => {
it('渲染插槽内容', () => {
const wrapper = mount(MyButton, {
slots: { default: '点击我' },
})
expect(wrapper.text()).toBe('点击我')
})
it('点击触发事件', async () => {
const wrapper = mount(MyButton)
await wrapper.trigger('click')
expect(wrapper.emitted('click')).toHaveLength(1)
})
})json
// package.json
{
"scripts": {
"test": "vitest",
"test:run": "vitest run",
"test:coverage": "vitest run --coverage"
}
}Axios 封装
ts
// src/api/request.ts
import axios, { type AxiosInstance, type AxiosRequestConfig, type AxiosResponse } from 'axios'
import { useUserStore } from '@/stores/user'
import router from '@/router'
// 响应数据格式
interface ApiResponse<T = any> {
code: number
data: T
message: string
}
const service: AxiosInstance = axios.create({
baseURL: import.meta.env.VITE_API_BASE_URL,
timeout: 15000,
headers: {
'Content-Type': 'application/json',
},
})
// 请求拦截器
service.interceptors.request.use(
(config) => {
const userStore = useUserStore()
if (userStore.token) {
config.headers.Authorization = `Bearer ${userStore.token}`
}
return config
},
(error) => Promise.reject(error)
)
// 响应拦截器
service.interceptors.response.use(
(response: AxiosResponse<ApiResponse>) => {
const { code, data, message } = response.data
if (code === 200) {
return data as any
}
// token 失效
if (code === 401) {
const userStore = useUserStore()
userStore.logout()
router.push('/login')
return Promise.reject(new Error('登录已过期'))
}
// 其他错误
return Promise.reject(new Error(message || '请求失败'))
},
(error) => {
if (error.response?.status === 401) {
const userStore = useUserStore()
userStore.logout()
router.push('/login')
}
return Promise.reject(error)
}
)
// 封装请求方法
export function get<T>(url: string, params?: any, config?: AxiosRequestConfig): Promise<T> {
return service.get(url, { params, ...config })
}
export function post<T>(url: string, data?: any, config?: AxiosRequestConfig): Promise<T> {
return service.post(url, data, config)
}
export function put<T>(url: string, data?: any, config?: AxiosRequestConfig): Promise<T> {
return service.put(url, data, config)
}
export function del<T>(url: string, config?: AxiosRequestConfig): Promise<T> {
return service.delete(url, config)
}
export default servicets
// src/api/modules/user.ts
import { get, post } from '../request'
interface UserInfo {
id: number
name: string
roles: string[]
}
export function getUserInfo() {
return get<UserInfo>('/user/info')
}
export function login(data: { username: string; password: string }) {
return post<{ token: string; user: UserInfo }>('/auth/login', data)
}CSS 方案
SCSS 变量
scss
// src/styles/variables.scss
$primary-color: #1890ff;
$font-size-base: 14px;
$border-radius: 4px;
$spacing-sm: 8px;
$spacing-md: 16px;
$spacing-lg: 24px;ts
// vite.config.ts
export default {
css: {
preprocessorOptions: {
scss: {
// Sass 新版推荐 @use 替代 @import:@use 不会产生全局命名空间污染,
// 且支持 as * 将所有成员平铺引入,避免重复引用导致的样式冲突。
additionalData: `@use "@/styles/variables" as *;`,
},
},
},
}Scoped 样式
vue
<style scoped>
/* 组件级样式隔离 */
.card { padding: 16px; }
/* 深度选择器 —— 修改子组件样式 */
:deep(.child-class) { color: red; }
/* 全局样式(慎用) */
:global(.ant-table) { margin: 0; }
</style>路径别名
ts
// vite.config.ts
import path from 'path'
import { fileURLToPath } from 'url'
const __dirname = path.dirname(fileURLToPath(import.meta.url))
export default {
resolve: {
alias: {
'@': path.resolve(__dirname, 'src'),
},
},
}json
// tsconfig.json
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]
}
}
}部署
部署方式(Docker、GitHub Pages、Vercel 等)详见 Vue 项目部署指南。
bash
# 构建生产版本
yarn build
# 预览构建产物
yarn preview