Skip to content

前端工程化实践

前端工程化是现代 Web 开发的重要组成部分,本教程将带您了解前端工程化的核心概念和最佳实践。

什么是前端工程化?

前端工程化是指将软件工程的方法和实践应用到前端开发中,通过工具、流程和规范来提高开发效率、代码质量和项目可维护性。

核心目标

  • 🚀 提高开发效率 - 自动化重复性工作
  • 🔧 保证代码质量 - 统一代码规范和最佳实践
  • 📦 优化构建产物 - 减少包体积,提升性能
  • 🔄 简化部署流程 - 自动化部署和发布

项目初始化

使用脚手架工具

bash
# Vue 项目
npm create vue@latest my-project
cd my-project
npm install

# React 项目
npx create-react-app my-app --template typescript
cd my-app

# Vite 通用项目
npm create vite@latest my-project
cd my-project
npm install

项目结构规划

my-project/
├── public/                 # 静态资源
├── src/
│   ├── assets/            # 项目资源文件
│   ├── components/        # 公共组件
│   ├── views/             # 页面组件
│   ├── router/            # 路由配置
│   ├── store/             # 状态管理
│   ├── utils/             # 工具函数
│   ├── api/               # API 接口
│   ├── types/             # TypeScript 类型定义
│   └── styles/            # 样式文件
├── tests/                 # 测试文件
├── docs/                  # 项目文档
├── .github/               # GitHub 配置
└── config/                # 配置文件

代码规范

ESLint 配置

安装 ESLint:

bash
npm install -D eslint @typescript-eslint/parser @typescript-eslint/eslint-plugin

配置 .eslintrc.js

javascript
module.exports = {
  root: true,
  env: {
    node: true,
    browser: true,
    es2022: true
  },
  extends: [
    'eslint:recommended',
    '@typescript-eslint/recommended',
    '@vue/typescript/recommended'
  ],
  parser: '@typescript-eslint/parser',
  parserOptions: {
    ecmaVersion: 2022,
    sourceType: 'module'
  },
  rules: {
    // 自定义规则
    'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
    'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
    '@typescript-eslint/no-unused-vars': 'error',
    'prefer-const': 'error',
    'no-var': 'error'
  }
}

Prettier 配置

安装 Prettier:

bash
npm install -D prettier eslint-config-prettier eslint-plugin-prettier

配置 .prettierrc

json
{
  "semi": false,
  "singleQuote": true,
  "tabWidth": 2,
  "trailingComma": "es5",
  "printWidth": 80,
  "bracketSpacing": true,
  "arrowParens": "avoid"
}

Husky + lint-staged

安装 Husky:

bash
npm install -D husky lint-staged
npx husky install

配置 package.json

json
{
  "scripts": {
    "prepare": "husky install"
  },
  "lint-staged": {
    "*.{js,ts,vue}": [
      "eslint --fix",
      "prettier --write"
    ],
    "*.{css,scss,less}": [
      "prettier --write"
    ]
  }
}

添加 pre-commit 钩子:

bash
npx husky add .husky/pre-commit "npx lint-staged"

构建优化

Vite 配置优化

typescript
// 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'),
      '@components': resolve(__dirname, 'src/components'),
      '@utils': resolve(__dirname, 'src/utils')
    }
  },
  
  // 构建优化
  build: {
    // 分包策略
    rollupOptions: {
      output: {
        manualChunks: {
          'vue-vendor': ['vue', 'vue-router', 'pinia'],
          'ui-vendor': ['element-plus'],
          'utils-vendor': ['lodash-es', 'dayjs']
        }
      }
    },
    
    // 压缩配置
    minify: 'terser',
    terserOptions: {
      compress: {
        drop_console: true,
        drop_debugger: true
      }
    }
  },
  
  // 开发服务器
  server: {
    port: 3000,
    open: true,
    proxy: {
      '/api': {
        target: 'http://localhost:8080',
        changeOrigin: true,
        rewrite: path => path.replace(/^\/api/, '')
      }
    }
  }
})

Webpack 优化(如果使用)

javascript
// webpack.config.js
const path = require('path')
const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer')

module.exports = {
  // 入口优化
  entry: {
    app: './src/main.js',
    vendor: ['vue', 'vue-router', 'vuex']
  },
  
  // 输出优化
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: '[name].[contenthash].js',
    chunkFilename: '[name].[contenthash].js'
  },
  
  // 优化配置
  optimization: {
    splitChunks: {
      chunks: 'all',
      cacheGroups: {
        vendor: {
          test: /[\\/]node_modules[\\/]/,
          name: 'vendors',
          chunks: 'all'
        }
      }
    }
  },
  
  // 插件
  plugins: [
    // 分析包大小
    process.env.ANALYZE && new BundleAnalyzerPlugin()
  ].filter(Boolean)
}

样式工程化

CSS 预处理器

安装 Sass:

bash
npm install -D sass

样式架构:

scss
// styles/index.scss
@import './variables';
@import './mixins';
@import './base';
@import './components';
@import './utilities';

// styles/_variables.scss
:root {
  // 颜色系统
  --color-primary: #409eff;
  --color-success: #67c23a;
  --color-warning: #e6a23c;
  --color-danger: #f56c6c;
  
  // 间距系统
  --spacing-xs: 4px;
  --spacing-sm: 8px;
  --spacing-md: 16px;
  --spacing-lg: 24px;
  --spacing-xl: 32px;
  
  // 字体系统
  --font-size-xs: 12px;
  --font-size-sm: 14px;
  --font-size-md: 16px;
  --font-size-lg: 18px;
  --font-size-xl: 20px;
}

// styles/_mixins.scss
@mixin flex-center {
  display: flex;
  align-items: center;
  justify-content: center;
}

@mixin text-ellipsis {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

@mixin responsive($breakpoint) {
  @if $breakpoint == mobile {
    @media (max-width: 767px) { @content; }
  }
  @if $breakpoint == tablet {
    @media (min-width: 768px) and (max-width: 1023px) { @content; }
  }
  @if $breakpoint == desktop {
    @media (min-width: 1024px) { @content; }
  }
}

PostCSS 配置

javascript
// postcss.config.js
module.exports = {
  plugins: {
    'postcss-import': {},
    'tailwindcss/nesting': {},
    tailwindcss: {},
    autoprefixer: {},
    ...(process.env.NODE_ENV === 'production' ? { cssnano: {} } : {})
  }
}

状态管理

Pinia 最佳实践

typescript
// stores/user.ts
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
import type { User, LoginCredentials } from '@/types/user'
import { userApi } from '@/api/user'

export const useUserStore = defineStore('user', () => {
  // State
  const currentUser = ref<User | null>(null)
  const loading = ref(false)
  const error = ref<string | null>(null)

  // Getters
  const isLoggedIn = computed(() => !!currentUser.value)
  const userRole = computed(() => currentUser.value?.role || 'guest')

  // Actions
  const login = async (credentials: LoginCredentials) => {
    loading.value = true
    error.value = null
    
    try {
      const response = await userApi.login(credentials)
      currentUser.value = response.data
      
      // 存储到 localStorage
      localStorage.setItem('user', JSON.stringify(response.data))
      localStorage.setItem('token', response.token)
      
      return response
    } catch (err) {
      error.value = err instanceof Error ? err.message : 'Login failed'
      throw err
    } finally {
      loading.value = false
    }
  }

  const logout = () => {
    currentUser.value = null
    localStorage.removeItem('user')
    localStorage.removeItem('token')
  }

  const initializeAuth = () => {
    const storedUser = localStorage.getItem('user')
    const storedToken = localStorage.getItem('token')
    
    if (storedUser && storedToken) {
      currentUser.value = JSON.parse(storedUser)
    }
  }

  return {
    // State
    currentUser,
    loading,
    error,
    
    // Getters
    isLoggedIn,
    userRole,
    
    // Actions
    login,
    logout,
    initializeAuth
  }
})

API 管理

统一的 API 客户端

typescript
// api/client.ts
import axios, { type AxiosInstance, type AxiosRequestConfig } from 'axios'

class ApiClient {
  private instance: AxiosInstance

  constructor(baseURL: string) {
    this.instance = axios.create({
      baseURL,
      timeout: 10000,
      headers: {
        'Content-Type': 'application/json'
      }
    })

    this.setupInterceptors()
  }

  private setupInterceptors() {
    // 请求拦截器
    this.instance.interceptors.request.use(
      (config) => {
        const token = localStorage.getItem('token')
        if (token) {
          config.headers.Authorization = `Bearer ${token}`
        }
        return config
      },
      (error) => Promise.reject(error)
    )

    // 响应拦截器
    this.instance.interceptors.response.use(
      (response) => response.data,
      (error) => {
        if (error.response?.status === 401) {
          // 处理未授权
          localStorage.removeItem('token')
          window.location.href = '/login'
        }
        return Promise.reject(error)
      }
    )
  }

  async get<T>(url: string, config?: AxiosRequestConfig): Promise<T> {
    return this.instance.get(url, config)
  }

  async post<T>(url: string, data?: any, config?: AxiosRequestConfig): Promise<T> {
    return this.instance.post(url, data, config)
  }

  async put<T>(url: string, data?: any, config?: AxiosRequestConfig): Promise<T> {
    return this.instance.put(url, data, config)
  }

  async delete<T>(url: string, config?: AxiosRequestConfig): Promise<T> {
    return this.instance.delete(url, config)
  }
}

export const apiClient = new ApiClient(import.meta.env.VITE_API_BASE_URL)

测试策略

单元测试

安装测试工具:

bash
npm install -D vitest @vue/test-utils jsdom

配置 vitest.config.ts

typescript
import { defineConfig } from 'vitest/config'
import vue from '@vitejs/plugin-vue'

export default defineConfig({
  plugins: [vue()],
  test: {
    environment: 'jsdom',
    globals: true,
    setupFiles: ['./tests/setup.ts']
  }
})

测试示例:

typescript
// tests/components/Button.test.ts
import { describe, it, expect } from 'vitest'
import { mount } from '@vue/test-utils'
import Button from '@/components/Button.vue'

describe('Button', () => {
  it('renders properly', () => {
    const wrapper = mount(Button, {
      props: { type: 'primary' },
      slots: { default: 'Click me' }
    })
    
    expect(wrapper.text()).toContain('Click me')
    expect(wrapper.classes()).toContain('btn-primary')
  })

  it('emits click event', async () => {
    const wrapper = mount(Button)
    
    await wrapper.trigger('click')
    
    expect(wrapper.emitted()).toHaveProperty('click')
  })
})

E2E 测试

安装 Playwright:

bash
npm install -D @playwright/test

E2E 测试示例:

typescript
// tests/e2e/login.spec.ts
import { test, expect } from '@playwright/test'

test('user can login', async ({ page }) => {
  await page.goto('/login')
  
  await page.fill('[data-testid="email"]', 'user@example.com')
  await page.fill('[data-testid="password"]', 'password123')
  await page.click('[data-testid="login-button"]')
  
  await expect(page).toHaveURL('/dashboard')
  await expect(page.locator('[data-testid="user-name"]')).toBeVisible()
})

CI/CD 流程

GitHub Actions 配置

yaml
# File: .github/workflows/ci.yml
name: CI/CD Pipeline

on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main]

jobs:
  test:
    runs-on: ubuntu-latest
    
    steps:
      - uses: actions/checkout@v3
      
      - name: Setup Node.js
        uses: actions/setup-node@v3
        with:
          node-version: '18'
          cache: 'npm'
      
      - name: Install dependencies
        run: npm ci
      
      - name: Run linting
        run: npm run lint
      
      - name: Run tests
        run: npm run test:unit
      
      - name: Run E2E tests
        run: npm run test:e2e
      
      - name: Build project
        run: npm run build
      
      - name: Upload coverage
        uses: codecov/codecov-action@v3

  deploy:
    needs: test
    runs-on: ubuntu-latest
    if: github.ref == 'refs/heads/main'
    
    steps:
      - uses: actions/checkout@v3
      
      - name: Setup Node.js
        uses: actions/setup-node@v3
        with:
          node-version: '18'
          cache: 'npm'
      
      - name: Install and build
        run: |
          npm ci
          npm run build
      
      - name: Deploy to production
        run: npm run deploy
        env:
          DEPLOY_TOKEN: ${{ secrets.DEPLOY_TOKEN }}

性能监控

性能指标收集

typescript
// utils/performance.ts
export class PerformanceMonitor {
  static measurePageLoad() {
    window.addEventListener('load', () => {
      const navigation = performance.getEntriesByType('navigation')[0] as PerformanceNavigationTiming
      
      const metrics = {
        dns: navigation.domainLookupEnd - navigation.domainLookupStart,
        tcp: navigation.connectEnd - navigation.connectStart,
        request: navigation.responseStart - navigation.requestStart,
        response: navigation.responseEnd - navigation.responseStart,
        dom: navigation.domContentLoadedEventEnd - navigation.responseEnd,
        load: navigation.loadEventEnd - navigation.loadEventStart,
        total: navigation.loadEventEnd - navigation.navigationStart
      }
      
      console.log('Performance Metrics:', metrics)
      
      // 发送到监控服务
      this.sendMetrics(metrics)
    })
  }

  static measureLCP() {
    new PerformanceObserver((list) => {
      const entries = list.getEntries()
      const lastEntry = entries[entries.length - 1]
      
      console.log('LCP:', lastEntry.startTime)
      this.sendMetric('lcp', lastEntry.startTime)
    }).observe({ entryTypes: ['largest-contentful-paint'] })
  }

  static measureFID() {
    new PerformanceObserver((list) => {
      const entries = list.getEntries()
      entries.forEach((entry) => {
        console.log('FID:', entry.processingStart - entry.startTime)
        this.sendMetric('fid', entry.processingStart - entry.startTime)
      })
    }).observe({ entryTypes: ['first-input'] })
  }

  private static sendMetrics(metrics: Record<string, number>) {
    // 发送到监控服务
    fetch('/api/metrics', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(metrics)
    })
  }

  private static sendMetric(name: string, value: number) {
    fetch('/api/metrics', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ [name]: value })
    })
  }
}

// 在应用启动时初始化
PerformanceMonitor.measurePageLoad()
PerformanceMonitor.measureLCP()
PerformanceMonitor.measureFID()

最佳实践总结

项目结构

  • 清晰的目录结构
  • 合理的文件命名
  • 模块化的代码组织

代码质量

  • 统一的代码规范
  • 完善的类型定义
  • 充分的测试覆盖

构建优化

  • 合理的分包策略
  • 资源压缩和优化
  • 缓存策略配置

开发体验

  • 热更新和快速构建
  • 完善的错误提示
  • 便捷的调试工具

部署流程

  • 自动化的 CI/CD
  • 环境配置管理
  • 监控和日志收集

工具推荐

开发工具

  • VS Code - 编辑器
  • Vue DevTools - Vue 调试工具
  • Chrome DevTools - 浏览器调试

构建工具

  • Vite - 现代构建工具
  • Webpack - 传统构建工具
  • Rollup - 库打包工具

质量保证

  • ESLint - 代码检查
  • Prettier - 代码格式化
  • Husky - Git 钩子

测试工具

  • Vitest - 单元测试
  • Playwright - E2E 测试
  • Storybook - 组件测试

前端工程化是一个持续演进的过程,需要根据项目需求和团队情况不断优化和调整。掌握这些核心概念和工具,将大大提升您的开发效率和项目质量!🚀

vitepress开发指南