Skip to content

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.ts

Vite 配置

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.com
ts
// 使用
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
*.md

Git Hooks

使用 husky + lint-staged 在提交时自动检查代码,详见 husky + lint-staged 常用命令

bash
yarn add -D husky lint-staged
npx husky init
json
// 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-dom
ts
// 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 service
ts
// 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>

路径别名

详见 Vue 3 + Vite 开发指南

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

参考

个人学习笔记,部分内容借助 AI 辅助整理,仅供查阅参考,请以官方文档为准