现代前端工程化体系搭建:从零构建企业级前端项目架构
引言 前端工程化是提升团队开发效率和代码质量的关键。一个完善的工程化体系包括项目架构、开发规范、构建工具、测试策略、CI/CD流程等多个方面。本文将从零开始,搭建一套完整的企业级前端工程化体系。 一、项目架构设计 1.1 单体仓库(Monorepo) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 # 项目结构 my-project/ ├── packages/ │ ├── shared/ # 共享代码 │ │ ├── src/ │ │ ├── package.json │ │ └── tsconfig.json │ ├── ui/ # UI组件库 │ │ ├── src/ │ │ ├── package.json │ │ └── tsconfig.json │ ├── web/ # Web应用 │ │ ├── src/ │ │ ├── package.json │ │ └── vite.config.ts │ └── admin/ # 管理后台 │ ├── src/ │ ├── package.json │ └── vite.config.ts ├── apps/ │ └── mobile/ # 移动应用 ├── .gitignore ├── pnpm-workspace.yaml # pnpm workspace配置 ├── package.json ├── nx.json # Nx配置 └── tsconfig.base.json 1 2 3 4 # pnpm-workspace.yaml packages: - 'packages/*' - 'apps/*' 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 // package.json { "name": "my-monorepo", "private": true, "scripts": { "dev": "turbo run dev", "build": "turbo run build", "test": "turbo run test", "lint": "turbo run lint", "format": "prettier --write \"**/*.{ts,tsx,json,md}\"", "changeset": "changeset", "version": "changeset version", "release": "turbo run build && changeset publish" }, "devDependencies": { "@changesets/cli": "^2.27.0", "prettier": "^3.1.0", "turbo": "^1.11.0", "typescript": "^5.3.0" } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 // turbo.json { "$schema": "https://turbo.build/schema.json", "globalDependencies": ["**/.env.*local"], "pipeline": { "build": { "dependsOn": ["^build"], "outputs": ["dist/**", ".next/**", "!.next/cache/**"] }, "dev": { "cache": false, "persistent": true }, "lint": { "outputs": [] }, "test": { "dependsOn": ["build"], "outputs": [], "inputs": ["src/**/*.tsx", "src/**/*.ts", "test/**/*.ts", "test/**/*.tsx"] } } } 1.2 分层架构 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 // 分层架构设计 src/ ├── main/ # 主入口层 │ ├── index.ts │ └── App.vue ├── presentation/ # 展示层(页面、组件) │ ├── pages/ │ │ ├── home/ │ │ ├── about/ │ │ └── dashboard/ │ └── components/ │ ├── common/ │ └── features/ ├── application/ # 应用层(业务逻辑) │ ├── useCases/ │ │ ├── auth/ │ │ ├── user/ │ │ └── product/ │ └── services/ │ └── api/ ├── domain/ # 领域层(核心业务) │ ├── entities/ │ ├── valueObjects/ │ └── repositories/ ├── infrastructure/ # 基础设施层 │ ├── api/ │ ├── storage/ │ └── config/ └── shared/ # 共享代码 ├── utils/ ├── constants/ ├── types/ └── validators/ 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 // 领域实体示例 // domain/entities/User.ts export class User { constructor( private readonly _id: string, private _name: string, private _email: string, private _role: UserRole ) {} get id(): string { return this._id } get name(): string { return this._name } get email(): string { return this._email } get role(): UserRole { return this._role } changeName(name: string): void { if (name.length < 2) { throw new Error('Name too short') } this._name = name } hasRole(role: UserRole): boolean { return this._role === role } } enum UserRole { ADMIN = 'ADMIN', USER = 'USER', GUEST = 'GUEST' } 二、代码规范 2.1 ESLint配置 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 // .eslintrc.cjs module.exports = { root: true, env: { browser: true, es2021: true, node: true }, extends: [ 'eslint:recommended', 'plugin:@typescript-eslint/recommended', 'plugin:vue/vue3-recommended', 'prettier' ], parser: 'vue-eslint-parser', parserOptions: { ecmaVersion: 'latest', parser: '@typescript-eslint/parser', sourceType: 'module' }, plugins: ['@typescript-eslint', 'vue', 'import'], rules: { // TypeScript规则 '@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }], '@typescript-eslint/no-explicit-any': 'warn', '@typescript-eslint/explicit-function-return-type': 'off', '@typescript-eslint/explicit-module-boundary-types': 'off', '@typescript-eslint/no-non-null-assertion': 'warn', // Vue规则 'vue/multi-word-component-names': 'off', 'vue/no-v-html': 'warn', 'vue/require-default-prop': 'error', 'vue/require-prop-types': 'error', 'vue/component-name-in-template-casing': ['error', 'PascalCase'], // Import规则 'import/order': [ 'error', { groups: [ 'builtin', 'external', 'internal', 'parent', 'sibling', 'index' ], 'newlines-between': 'always', alphabetize: { order: 'asc', caseInsensitive: true } } ], 'import/no-unresolved': 'error', 'import/no-cycle': 'warn', // 通用规则 'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off', 'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off' }, globals: { defineProps: 'readonly', defineEmits: 'readonly', defineExpose: 'readonly', withDefaults: 'readonly' } } 2.2 Prettier配置 1 2 3 4 5 6 7 8 9 10 11 // .prettierrc { "semi": false, "singleQuote": true, "printWidth": 100, "tabWidth": 2, "trailingComma": "es5", "arrowParens": "avoid", "endOfLine": "lf", "vueIndentScriptAndStyle": false } 1 2 3 4 5 6 7 8 9 // .prettierignore node_modules dist build coverage *.min.js *.min.css package-lock.json pnpm-lock.yaml 2.3 Git提交规范 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 // .commitlintrc.cjs module.exports = { extends: ['@commitlint/config-conventional'], rules: { 'type-enum': [ 2, 'always', [ 'feat', // 新功能 'fix', // 修复bug 'docs', // 文档更新 'style', // 代码格式调整 'refactor', // 重构 'perf', // 性能优化 'test', // 测试相关 'build', // 构建系统 'ci', // CI配置 'chore', // 其他杂项 'revert' // 回退提交 ] ], 'scope-case': [2, 'always', 'kebab-case'], 'subject-case': [0], 'subject-empty': [2, 'never'], 'subject-max-length': [2, 'always', 100], 'type-empty': [2, 'never'], 'scope-empty': [1, 'never'] } } 1 2 3 4 5 # .husky/pre-commit #!/usr/bin/env sh . "$(dirname -- "$0")/_/husky.sh" pnpm lint-staged 1 2 3 4 5 # .husky/commit-msg #!/usr/bin/env sh . "$(dirname -- "$0")/_/husky.sh" pnpm commitlint --edit $1 1 2 3 4 5 6 7 8 9 10 11 12 // package.json { "lint-staged": { "*.{ts,tsx,vue}": [ "eslint --fix", "prettier --write" ], "*.{json,md,yml,yaml}": [ "prettier --write" ] } } 三、构建优化 3.1 Vite配置 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 // vite.config.ts import { defineConfig } from 'vite' import vue from '@vitejs/plugin-vue' import { resolve } from 'path' export default defineConfig({ plugins: [vue()], resolve: { alias: { '@': resolve(__dirname, 'src'), '@shared': resolve(__dirname, '../shared/src') } }, build: { target: 'es2015', outDir: 'dist', assetsDir: 'assets', sourcemap: false, minify: 'terser', rollupOptions: { output: { // 手动分包 manualChunks: { 'vue-vendor': ['vue', 'vue-router', 'pinia'], 'ui-library': ['element-plus'], 'utils': ['lodash-es', 'axios', 'dayjs'] }, // 文件命名 chunkFileNames: 'js/[name]-[hash].js', entryFileNames: 'js/[name]-[hash].js', assetFileNames: '[ext]/[name]-[hash].[ext]' } }, // Terser配置 terserOptions: { compress: { drop_console: true, drop_debugger: true, pure_funcs: ['console.log'] } }, // chunk大小警告阈值 chunkSizeWarningLimit: 1000 }, server: { port: 3000, host: true, open: true, proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true, rewrite: (path) => path.replace(/^\/api/, '') } } }, // CSS配置 css: { preprocessorOptions: { scss: { additionalData: `@use "@/styles/variables" as *;` } }, modules: { localsConvention: 'camelCase' } }, // 依赖优化 optimizeDeps: { include: ['vue', 'vue-router', 'pinia', 'axios'], exclude: [] } }) 3.2 环境变量管理 1 2 3 4 5 # .env.development VITE_APP_TITLE=MyApp (Development) VITE_API_BASE_URL=http://localhost:8080/api VITE_ENABLE_MOCK=true VITE_APP_MOCK_PORT=3001 1 2 3 4 # .env.production VITE_APP_TITLE=MyApp VITE_API_BASE_URL=https://api.example.com VITE_ENABLE_MOCK=false 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 // src/config/env.ts interface EnvConfig { appTitle: string apiBaseUrl: string enableMock: boolean mockPort?: number } export const env: EnvConfig = { appTitle: import.meta.env.VITE_APP_TITLE || 'MyApp', apiBaseUrl: import.meta.env.VITE_API_BASE_URL || '/api', enableMock: import.meta.env.VITE_ENABLE_MOCK === 'true', mockPort: import.meta.env.VITE_APP_MOCK_PORT ? parseInt(import.meta.env.VITE_APP_MOCK_PORT) : undefined } 四、测试策略 4.1 单元测试 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 // vitest.config.ts import { defineConfig } from 'vitest/config' import vue from '@vitejs/plugin-vue' export default defineConfig({ plugins: [vue()], test: { globals: true, environment: 'jsdom', setupFiles: ['./test/setup.ts'], coverage: { provider: 'v8', reporter: ['text', 'json', 'html'], exclude: [ 'node_modules/', 'test/', '**/*.d.ts', '**/*.config.*', '**/mockData', 'src/main.ts' ] } } }) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 // test/utils.test.ts import { describe, it, expect } from 'vitest' import { formatDate, calculateAge } from '@/utils/date' describe('Date Utils', () => { describe('formatDate', () => { it('should format date correctly', () => { const date = new Date('2025-01-01') expect(formatDate(date, 'YYYY-MM-DD')).toBe('2025-01-01') }) it('should handle invalid date', () => { expect(formatDate(null, 'YYYY-MM-DD')).toBe('') }) }) describe('calculateAge', () => { it('should calculate age correctly', () => { const birthDate = new Date('1990-01-01') const currentDate = new Date('2025-01-01') expect(calculateAge(birthDate, currentDate)).toBe(35) }) }) }) 4.2 组件测试 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 // test/components/Button.test.ts import { describe, it, expect } from 'vitest' import { mount } from '@vue/test-utils' import Button from '@/components/common/Button.vue' describe('Button Component', () => { it('renders properly', () => { const wrapper = mount(Button, { props: { text: 'Click me', type: 'primary' } }) expect(wrapper.text()).toContain('Click me') expect(wrapper.classes()).toContain('btn-primary') }) it('emits click event', async () => { const wrapper = mount(Button, { props: { text: 'Click me' } }) await wrapper.trigger('click') expect(wrapper.emitted('click')).toBeTruthy() }) it('disables button when loading', () => { const wrapper = mount(Button, { props: { text: 'Submit', loading: true } }) const button = wrapper.find('button') expect(button.attributes('disabled')).toBeDefined() }) }) 4.3 E2E测试 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 // e2e/auth.spec.ts import { test, expect } from '@playwright/test' test.describe('Authentication', () => { test.beforeEach(async ({ page }) => { await page.goto('http://localhost:3000') }) test('should login with valid credentials', async ({ page }) => { await page.click('text=Login') await page.fill('[name="email"]', 'user@example.com') await page.fill('[name="password"]', 'password123') await page.click('button[type="submit"]') await expect(page).toHaveURL(/.*dashboard/) await expect(page.locator('text=Welcome')).toBeVisible() }) test('should show error with invalid credentials', async ({ page }) => { await page.click('text=Login') await page.fill('[name="email"]', 'invalid@example.com') await page.fill('[name="password"]', 'wrongpassword') await page.click('button[type="submit"]') await expect(page.locator('text=Invalid credentials')).toBeVisible() }) }) 五、CI/CD流程 5.1 GitHub Actions配置 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 # .github/workflows/ci.yml name: CI on: push: branches: [main, develop] pull_request: branches: [main, develop] jobs: lint: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Setup pnpm uses: pnpm/action-setup@v2 with: version: 8 - name: Setup Node.js uses: actions/setup-node@v4 with: node-version: 20 cache: 'pnpm' - name: Install dependencies run: pnpm install --frozen-lockfile - name: Run ESLint run: pnpm lint - name: Run TypeScript check run: pnpm typecheck test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Setup pnpm uses: pnpm/action-setup@v2 with: version: 8 - name: Setup Node.js uses: actions/setup-node@v4 with: node-version: 20 cache: 'pnpm' - name: Install dependencies run: pnpm install --frozen-lockfile - name: Run unit tests run: pnpm test:unit - name: Run component tests run: pnpm test:component - name: Upload coverage uses: codecov/codecov-action@v3 with: files: ./coverage/lcov.info build: needs: [lint, test] runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Setup pnpm uses: pnpm/action-setup@v2 with: version: 8 - name: Setup Node.js uses: actions/setup-node@v4 with: node-version: 20 cache: 'pnpm' - name: Install dependencies run: pnpm install --frozen-lockfile - name: Build run: pnpm build - name: Upload build artifacts uses: actions/upload-artifact@v3 with: name: dist path: dist 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 # .github/workflows/deploy.yml name: Deploy on: push: branches: [main] jobs: deploy: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Setup pnpm uses: pnpm/action-setup@v2 with: version: 8 - name: Setup Node.js uses: actions/setup-node@v4 with: node-version: 20 cache: 'pnpm' - name: Install dependencies run: pnpm install --frozen-lockfile - name: Build run: pnpm build env: NODE_ENV: production - name: Deploy to Vercel uses: amondnet/vercel-action@v25 with: vercel-token: ${{ secrets.VERCEL_TOKEN }} vercel-org-id: ${{ secrets.VERCEL_ORG_ID }} vercel-project-id: ${{ secrets.VERCEL_PROJECT_ID }} vercel-args: '--prod' 六、监控与日志 6.1 错误监控 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 // src/monitoring/sentry.ts import * as Sentry from '@sentry/vue' export function setupSentry(app: any) { if (import.meta.env.PROD) { Sentry.init({ app, dsn: import.meta.env.VITE_SENTRY_DSN, environment: import.meta.env.MODE, release: import.meta.env.VITE_APP_VERSION, // 性能监控 integrations: [ new Sentry.BrowserTracing({ tracePropagationTargets: ['localhost', 'example.com'] }), new Sentry.Replay() ], // 采样率 tracesSampleRate: 0.1, replaysSessionSampleRate: 0.1, replaysOnErrorSampleRate: 1.0, // 过滤敏感信息 beforeSend(event) { // 移除敏感数据 if (event.request?.headers) { delete event.request.headers['authorization'] } return event } }) } } 6.2 性能监控 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 // src/monitoring/analytics.ts export class Analytics { private queue: any[] = [] private apiKey: string constructor(apiKey: string) { this.apiKey = apiKey this.init() } private init() { // 页面加载性能 window.addEventListener('load', () => { setTimeout(() => { this.trackPageLoad() }, 0) }) // 定期发送队列数据 setInterval(() => { this.flush() }, 5000) } private trackPageLoad() { const perfData = performance.getEntriesByType('navigation')[0] as PerformanceNavigationTiming this.track('page_load', { // DNS查询时间 dns: perfData.domainLookupEnd - perfData.domainLookupStart, // TCP连接时间 tcp: perfData.connectEnd - perfData.connectStart, // 请求响应时间 request: perfData.responseEnd - perfData.requestStart, // DOM解析时间 domParse: perfData.domComplete - perfData.domInteractive, // 首次绘制 firstPaint: performance.getEntriesByName('first-paint')[0]?.startTime, // 首次内容绘制 firstContentfulPaint: performance.getEntriesByName('first-contentful-paint')[0]?.startTime }) } track(eventName: string, data?: any) { this.queue.push({ event: eventName, data, timestamp: Date.now(), url: location.href, userAgent: navigator.userAgent }) } private flush() { if (this.queue.length === 0) return const data = [...this.queue] this.queue = [] fetch('/api/analytics', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-API-Key': this.apiKey }, body: JSON.stringify({ events: data }), keepalive: true }).catch(err => { // 失败时重新加入队列 this.queue.unshift(...data) }) } } export const analytics = new Analytics(import.meta.env.VITE_ANALYTICS_API_KEY) 总结 前端工程化是一个系统性的工作: ...