Skip to content

构建工具与工程化

代码质量工具

1. ESLint 的核心原理是什么?

工作流程

typescript
// 1. 解析代码为 AST
const code = 'const a = 1'
const ast = parse(code)

// 2. 遍历 AST 节点
traverse(ast, {
  VariableDeclaration(node) {
    // 检查规则
    if (node.kind === 'const' && !isUpperCase(node.id.name)) {
      report('常量应该大写')
    }
  }
})

// 3. 报告问题
// 4. 自动修复(如果可能)

配置文件

javascript
// .eslintrc.js
module.exports = {
  // 环境
  env: {
    browser: true,
    es2021: true,
    node: true,
  },
  
  // 继承配置
  extends: [
    'eslint:recommended',
    'plugin:@typescript-eslint/recommended',
    'plugin:react/recommended',
    'plugin:react-hooks/recommended',
    'prettier', // 必须放在最后
  ],
  
  // 解析器
  parser: '@typescript-eslint/parser',
  parserOptions: {
    ecmaVersion: 'latest',
    sourceType: 'module',
    ecmaFeatures: {
      jsx: true,
    },
    project: './tsconfig.json',
  },
  
  // 插件
  plugins: ['@typescript-eslint', 'react', 'react-hooks'],
  
  // 规则
  rules: {
    'no-console': 'warn',
    '@typescript-eslint/no-unused-vars': 'error',
    'react/react-in-jsx-scope': 'off',
    'react-hooks/rules-of-hooks': 'error',
    'react-hooks/exhaustive-deps': 'warn',
  },
  
  // 覆盖特定文件
  overrides: [
    {
      files: ['*.test.ts', '*.test.tsx'],
      rules: {
        'no-console': 'off',
      },
    },
  ],
}

2. 如何编写自定义 ESLint 规则?

javascript
// eslint-plugin-custom/rules/no-console-log.js
module.exports = {
  meta: {
    type: 'suggestion',
    docs: {
      description: '禁止使用 console.log',
      category: 'Best Practices',
    },
    fixable: 'code',
    schema: [],
  },
  
  create(context) {
    return {
      // 访问 CallExpression 节点
      CallExpression(node) {
        // 检查是否是 console.log
        if (
          node.callee.type === 'MemberExpression' &&
          node.callee.object.name === 'console' &&
          node.callee.property.name === 'log'
        ) {
          context.report({
            node,
            message: '不要使用 console.log,请使用 logger',
            fix(fixer) {
              // 自动修复
              return fixer.replaceText(
                node.callee,
                'logger.info'
              )
            },
          })
        }
      },
    }
  },
}

// 使用
// .eslintrc.js
module.exports = {
  plugins: ['custom'],
  rules: {
    'custom/no-console-log': 'error',
  },
}

3. Prettier 与 ESLint 如何配合使用?

javascript
// .prettierrc.js
module.exports = {
  // 基础配置
  semi: false,
  singleQuote: true,
  trailingComma: 'es5',
  printWidth: 100,
  tabWidth: 2,
  useTabs: false,
  
  // JSX
  jsxSingleQuote: false,
  jsxBracketSameLine: false,
  
  // 其他
  arrowParens: 'always',
  endOfLine: 'lf',
}

// .prettierignore
dist
node_modules
*.min.js

与 ESLint 集成

bash
# 安装
pnpm add -D eslint-config-prettier eslint-plugin-prettier

# .eslintrc.js
module.exports = {
  extends: [
    'eslint:recommended',
    'plugin:@typescript-eslint/recommended',
    'prettier', // 关闭 ESLint 中与 Prettier 冲突的规则
  ],
  plugins: ['prettier'],
  rules: {
    'prettier/prettier': 'error', // Prettier 规则作为 ESLint 规则
  },
}

VS Code 配置

json
// .vscode/settings.json
{
  "editor.formatOnSave": true,
  "editor.defaultFormatter": "esbenp.prettier-vscode",
  "editor.codeActionsOnSave": {
    "source.fixAll.eslint": true
  },
  "[typescript]": {
    "editor.defaultFormatter": "esbenp.prettier-vscode"
  }
}

4. Husky 和 lint-staged 如何配置?

bash
# 安装
pnpm add -D husky lint-staged

# 初始化 husky
pnpm exec husky init
json
// package.json
{
  "scripts": {
    "prepare": "husky install"
  },
  "lint-staged": {
    "*.{ts,tsx,js,jsx}": [
      "eslint --fix",
      "prettier --write"
    ],
    "*.{json,md,yml,yaml}": [
      "prettier --write"
    ]
  }
}
bash
# .husky/pre-commit
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"

pnpm lint-staged

# .husky/commit-msg
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"

pnpm commitlint --edit $1

Commitlint 配置

javascript
// commitlint.config.js
module.exports = {
  extends: ['@commitlint/config-conventional'],
  rules: {
    'type-enum': [
      2,
      'always',
      [
        'feat',     // 新功能
        'fix',      // 修复
        'docs',     // 文档
        'style',    // 格式
        'refactor', // 重构
        'perf',     // 性能
        'test',     // 测试
        'chore',    // 构建/工具
        'revert',   // 回退
      ],
    ],
    'subject-case': [0],
  },
}

CI/CD

5. GitHub Actions 的基础配置?

yaml
# .github/workflows/ci.yml
name: CI

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

jobs:
  lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      
      - uses: pnpm/action-setup@v2
        with:
          version: 8
      
      - uses: actions/setup-node@v3
        with:
          node-version: 18
          cache: 'pnpm'
      
      - name: Install dependencies
        run: pnpm install --frozen-lockfile
      
      - name: Lint
        run: pnpm lint
      
      - name: Type check
        run: pnpm typecheck

  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      
      - uses: pnpm/action-setup@v2
        with:
          version: 8
      
      - uses: actions/setup-node@v3
        with:
          node-version: 18
          cache: 'pnpm'
      
      - name: Install dependencies
        run: pnpm install --frozen-lockfile
      
      - name: Test
        run: pnpm test:coverage
      
      - name: Upload coverage
        uses: codecov/codecov-action@v3
        with:
          files: ./coverage/coverage-final.json

  build:
    runs-on: ubuntu-latest
    needs: [lint, test]
    steps:
      - uses: actions/checkout@v3
      
      - uses: pnpm/action-setup@v2
        with:
          version: 8
      
      - uses: actions/setup-node@v3
        with:
          node-version: 18
          cache: 'pnpm'
      
      - name: Install dependencies
        run: pnpm install --frozen-lockfile
      
      - name: Build
        run: pnpm build
      
      - name: Upload artifacts
        uses: actions/upload-artifact@v3
        with:
          name: dist
          path: dist/

6. 如何配置自动部署?

yaml
# .github/workflows/deploy.yml
name: Deploy

on:
  push:
    branches: [main]

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      
      - uses: pnpm/action-setup@v2
        with:
          version: 8
      
      - uses: actions/setup-node@v3
        with:
          node-version: 18
          cache: 'pnpm'
      
      - name: Install dependencies
        run: pnpm install --frozen-lockfile
      
      - name: Build
        run: pnpm build
        env:
          VITE_API_URL: ${{ secrets.API_URL }}
      
      # 部署到 Vercel
      - 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'
      
      # 或部署到 Netlify
      - name: Deploy to Netlify
        uses: nwtgck/actions-netlify@v2
        with:
          publish-dir: './dist'
          production-branch: main
          github-token: ${{ secrets.GITHUB_TOKEN }}
          deploy-message: 'Deploy from GitHub Actions'
        env:
          NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
          NETLIFY_SITE_ID: ${{ secrets.NETLIFY_SITE_ID }}
      
      # 或部署到 GitHub Pages
      - name: Deploy to GitHub Pages
        uses: peaceiris/actions-gh-pages@v3
        with:
          github_token: ${{ secrets.GITHUB_TOKEN }}
          publish_dir: ./dist

7. 如何优化 CI/CD 性能?

1. 缓存依赖

yaml
- uses: actions/setup-node@v3
  with:
    node-version: 18
    cache: 'pnpm'

# 或手动缓存
- uses: actions/cache@v3
  with:
    path: |
      ~/.pnpm-store
      node_modules
    key: ${{ runner.os }}-pnpm-${{ hashFiles('**/pnpm-lock.yaml') }}
    restore-keys: |
      ${{ runner.os }}-pnpm-

2. 并行执行

yaml
jobs:
  lint:
    runs-on: ubuntu-latest
    # 并行运行
  
  test:
    runs-on: ubuntu-latest
    # 并行运行
  
  build:
    runs-on: ubuntu-latest
    needs: [lint, test]  # 等待前面的任务完成

3. 矩阵构建

yaml
jobs:
  test:
    runs-on: ${{ matrix.os }}
    strategy:
      matrix:
        os: [ubuntu-latest, windows-latest, macos-latest]
        node: [16, 18, 20]
    steps:
      - uses: actions/setup-node@v3
        with:
          node-version: ${{ matrix.node }}

4. 条件执行

yaml
jobs:
  deploy:
    if: github.ref == 'refs/heads/main'
    steps:
      - name: Deploy
        run: pnpm deploy

  test:
    if: github.event_name == 'pull_request'
    steps:
      - name: Test
        run: pnpm test

包管理器

8. npm、yarn、pnpm 的区别?

特性npmyarnpnpm
安装速度最快
磁盘空间小(硬链接)
lock 文件package-lock.jsonyarn.lockpnpm-lock.yaml
workspace支持支持支持
严格模式

pnpm 的优势

bash
# 1. 节省磁盘空间
# npm/yarn: 每个项目都有完整的 node_modules
project-a/node_modules/lodash  (1MB)
project-b/node_modules/lodash  (1MB)
# 总共 2MB

# pnpm: 使用硬链接
~/.pnpm-store/lodash           (1MB)
project-a/node_modules/lodash 硬链接
project-b/node_modules/lodash 硬链接
# 总共 1MB

# 2. 严格的依赖管理
# 只能访问 package.json 中声明的依赖
# 防止幽灵依赖

# 3. 更快的安装速度
# 并行安装 + 硬链接

9. 如何处理依赖版本锁定?

json
// package.json
{
  "dependencies": {
    // 精确版本
    "react": "18.2.0",
    
    // 兼容版本(推荐)
    "vue": "^3.3.0",  // 3.3.0 <= version < 4.0.0
    
    // 次要版本
    "lodash": "~4.17.0",  // 4.17.0 <= version < 4.18.0
    
    // 最新版本(不推荐)
    "axios": "*",
  },
  
  // 覆盖依赖版本
  "overrides": {
    "lodash": "4.17.21"
  },
  
  // pnpm 特有
  "pnpm": {
    "overrides": {
      "react": "^18.2.0"
    }
  }
}

锁文件的作用

bash
# package-lock.json / pnpm-lock.yaml
# 1. 锁定确切版本
# 2. 确保团队使用相同版本
# 3. 加速安装

# 安装时使用锁文件
pnpm install --frozen-lockfile  # CI 环境推荐
pnpm install  # 更新锁文件

环境变量管理

10. 如何管理不同环境的配置?

bash
# .env
VITE_APP_TITLE=My App
VITE_API_URL=http://localhost:3000

# .env.development
VITE_API_URL=http://localhost:8080
VITE_DEBUG=true

# .env.production
VITE_API_URL=https://api.production.com
VITE_DEBUG=false

# .env.local (不提交到 git)
VITE_SECRET_KEY=xxx
typescript
// 使用环境变量
console.log(import.meta.env.VITE_APP_TITLE)
console.log(import.meta.env.VITE_API_URL)

// TypeScript 类型定义
// env.d.ts
interface ImportMetaEnv {
  readonly VITE_APP_TITLE: string
  readonly VITE_API_URL: string
  readonly VITE_DEBUG: string
}

interface ImportMeta {
  readonly env: ImportMetaEnv
}

dotenv-cli 使用

bash
# 安装
pnpm add -D dotenv-cli

# package.json
{
  "scripts": {
    "dev": "dotenv -e .env.development vite",
    "build:staging": "dotenv -e .env.staging vite build",
    "build:prod": "dotenv -e .env.production vite build"
  }
}

性能监控

11. 如何监控构建性能?

javascript
// vite.config.ts
import { defineConfig } from 'vite'
import { visualizer } from 'rollup-plugin-visualizer'

export default defineConfig({
  plugins: [
    visualizer({
      open: true,
      gzipSize: true,
      brotliSize: true,
      filename: 'dist/stats.html',
    }),
  ],
  
  build: {
    // 输出分析
    reportCompressedSize: true,
    
    // chunk 大小警告
    chunkSizeWarningLimit: 1000,
    
    rollupOptions: {
      output: {
        manualChunks: {
          vendor: ['react', 'react-dom'],
        },
      },
    },
  },
})

webpack-bundle-analyzer

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

module.exports = {
  plugins: [
    new BundleAnalyzerPlugin({
      analyzerMode: 'static',
      openAnalyzer: false,
      reportFilename: 'bundle-report.html',
    }),
  ],
}

12. 如何进行代码质量检查?

bash
# 安装工具
pnpm add -D @typescript-eslint/parser @typescript-eslint/eslint-plugin
pnpm add -D eslint-plugin-sonarjs
pnpm add -D eslint-plugin-security
javascript
// .eslintrc.js
module.exports = {
  extends: [
    'plugin:sonarjs/recommended',  // 代码质量
    'plugin:security/recommended', // 安全检查
  ],
  plugins: ['sonarjs', 'security'],
  rules: {
    'sonarjs/cognitive-complexity': ['error', 15],
    'sonarjs/no-duplicate-string': 'error',
    'security/detect-object-injection': 'warn',
  },
}

SonarQube 集成

yaml
# .github/workflows/sonarqube.yml
name: SonarQube

on:
  push:
    branches: [main]

jobs:
  sonarqube:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
        with:
          fetch-depth: 0
      
      - name: SonarQube Scan
        uses: sonarsource/sonarqube-scan-action@master
        env:
          SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
          SONAR_HOST_URL: ${{ secrets.SONAR_HOST_URL }}

测试工具

13. Vitest 的配置和使用?

typescript
// vitest.config.ts
import { defineConfig } from 'vitest/config'
import react from '@vitejs/plugin-react'

export default defineConfig({
  plugins: [react()],
  test: {
    globals: true,
    environment: 'jsdom',
    setupFiles: './src/test/setup.ts',
    coverage: {
      provider: 'v8',
      reporter: ['text', 'json', 'html'],
      exclude: [
        'node_modules/',
        'src/test/',
      ],
    },
  },
})

// src/test/setup.ts
import { expect, afterEach } from 'vitest'
import { cleanup } from '@testing-library/react'
import matchers from '@testing-library/jest-dom/matchers'

expect.extend(matchers)

afterEach(() => {
  cleanup()
})

测试示例

typescript
// Button.test.tsx
import { render, screen, fireEvent } from '@testing-library/react'
import { describe, it, expect, vi } from 'vitest'
import { Button } from './Button'

describe('Button', () => {
  it('renders correctly', () => {
    render(<Button>Click me</Button>)
    expect(screen.getByText('Click me')).toBeInTheDocument()
  })
  
  it('calls onClick when clicked', () => {
    const onClick = vi.fn()
    render(<Button onClick={onClick}>Click me</Button>)
    
    fireEvent.click(screen.getByText('Click me'))
    expect(onClick).toHaveBeenCalledTimes(1)
  })
})

14. E2E 测试如何配置?

Playwright 配置

typescript
// playwright.config.ts
import { defineConfig, devices } from '@playwright/test'

export default defineConfig({
  testDir: './e2e',
  fullyParallel: true,
  forbidOnly: !!process.env.CI,
  retries: process.env.CI ? 2 : 0,
  workers: process.env.CI ? 1 : undefined,
  reporter: 'html',
  
  use: {
    baseURL: 'http://localhost:3000',
    trace: 'on-first-retry',
    screenshot: 'only-on-failure',
  },
  
  projects: [
    {
      name: 'chromium',
      use: { ...devices['Desktop Chrome'] },
    },
    {
      name: 'firefox',
      use: { ...devices['Desktop Firefox'] },
    },
    {
      name: 'webkit',
      use: { ...devices['Desktop Safari'] },
    },
  ],
  
  webServer: {
    command: 'pnpm dev',
    url: 'http://localhost:3000',
    reuseExistingServer: !process.env.CI,
  },
})

测试示例

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

test('homepage has title', async ({ page }) => {
  await page.goto('/')
  
  await expect(page).toHaveTitle(/My App/)
})

test('login flow', async ({ page }) => {
  await page.goto('/login')
  
  await page.fill('input[name="email"]', 'user@example.com')
  await page.fill('input[name="password"]', 'password')
  await page.click('button[type="submit"]')
  
  await expect(page).toHaveURL('/dashboard')
})

文档工具

15. 如何搭建项目文档?

VitePress 配置

typescript
// .vitepress/config.ts
import { defineConfig } from 'vitepress'

export default defineConfig({
  title: 'My Project',
  description: 'Project documentation',
  
  themeConfig: {
    nav: [
      { text: 'Guide', link: '/guide/' },
      { text: 'API', link: '/api/' },
    ],
    
    sidebar: {
      '/guide/': [
        {
          text: 'Introduction',
          items: [
            { text: 'Getting Started', link: '/guide/getting-started' },
            { text: 'Installation', link: '/guide/installation' },
          ],
        },
      ],
    },
    
    socialLinks: [
      { icon: 'github', link: 'https://github.com/user/repo' },
    ],
  },
})

Storybook 配置

typescript
// .storybook/main.ts
import type { StorybookConfig } from '@storybook/react-vite'

const config: StorybookConfig = {
  stories: ['../src/**/*.stories.@(js|jsx|ts|tsx)'],
  addons: [
    '@storybook/addon-links',
    '@storybook/addon-essentials',
    '@storybook/addon-interactions',
  ],
  framework: {
    name: '@storybook/react-vite',
    options: {},
  },
}

export default config

// Button.stories.tsx
import type { Meta, StoryObj } from '@storybook/react'
import { Button } from './Button'

const meta: Meta<typeof Button> = {
  title: 'Components/Button',
  component: Button,
  tags: ['autodocs'],
}

export default meta
type Story = StoryObj<typeof Button>

export const Primary: Story = {
  args: {
    children: 'Button',
    variant: 'primary',
  },
}

最佳实践

16. 前端工程化的完整配置清单?

bash
# 项目初始化
pnpm init

# 包管理器
pnpm add -D pnpm

# 构建工具
pnpm add -D vite @vitejs/plugin-react

# TypeScript
pnpm add -D typescript @types/node @types/react @types/react-dom

# 代码质量
pnpm add -D eslint prettier
pnpm add -D @typescript-eslint/parser @typescript-eslint/eslint-plugin
pnpm add -D eslint-config-prettier eslint-plugin-prettier

# Git Hooks
pnpm add -D husky lint-staged @commitlint/cli @commitlint/config-conventional

# 测试
pnpm add -D vitest @testing-library/react @testing-library/jest-dom
pnpm add -D @playwright/test

# 文档
pnpm add -D vitepress

# 工具
pnpm add -D cross-env dotenv-cli

配置文件

json
// package.json
{
  "scripts": {
    "dev": "vite",
    "build": "tsc && vite build",
    "preview": "vite preview",
    "test": "vitest",
    "test:e2e": "playwright test",
    "lint": "eslint . --ext .ts,.tsx",
    "format": "prettier --write \"src/**/*.{ts,tsx}\"",
    "typecheck": "tsc --noEmit",
    "prepare": "husky install"
  }
}