前端工程化

📦 一、模块化

1. CommonJS、ES Module、AMD、UMD 的区别?

CommonJS (Node.js)

复制代码
// 导出
module.exports = { name: 'Alice' };
// 或
exports.name = 'Alice';

// 导入
const user = require('./user');

// 特点:
// 1. 同步加载,适合服务端
// 2. 运行时加载,加载的是值的拷贝
// 3. 可以动态引入:require(condition ? './a' : './b')

ES Module (浏览器标准)

复制代码
// 导出
export const name = 'Alice';
export default function() {}

// 导入
import user from './user.js';
import { name } from './user.js';

// 特点:
// 1. 异步加载,适合浏览器
// 2. 编译时加载(静态分析),加载的是值的引用
// 3. 支持 Tree Shaking
// 4. 导入是只读的,不能修改

AMD (RequireJS)

复制代码
// 定义模块
define(['jquery'], function($) {
  return { name: 'Alice' };
});

// 使用
require(['./user'], function(user) {
  console.log(user.name);
});

// 特点:异步加载,依赖前置,主要用于浏览器

UMD (通用模块)

复制代码
(function (root, factory) {
  if (typeof define === 'function' && define.amd) {
    // AMD
    define(['jquery'], factory);
  } else if (typeof module === 'object' && module.exports) {
    // CommonJS
    module.exports = factory(require('jquery'));
  } else {
    // 浏览器全局变量
    root.MyModule = factory(root.jQuery);
  }
}(this, function ($) {
  return { name: 'Alice' };
}));

// 特点:兼容 AMD、CommonJS、全局变量

核心区别对比表:

特性 CommonJS ES Module
加载方式 运行时同步 编译时异步
输出 值的拷贝 值的引用
this 指向当前模块 undefined
循环依赖 加载部分已执行代码 报错或undefined
Tree Shaking 不支持 支持

🔧 二、构建工具

2. Webpack 核心概念和工作原理?

核心概念:

复制代码
// webpack.config.js
module.exports = {
  // 1. Entry:入口
  entry: './src/index.js',
  
  // 2. Output:输出
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: '[name].[contenthash].js'
  },
  
  // 3. Loader:转换器
  module: {
    rules: [
      {
        test: /\.css$/,
        use: ['style-loader', 'css-loader'] // 从右往左执行
      },
      {
        test: /\.jsx?$/,
        use: 'babel-loader',
        exclude: /node_modules/
      }
    ]
  },
  
  // 4. Plugin:插件
  plugins: [
    new HtmlWebpackPlugin({
      template: './public/index.html'
    }),
    new MiniCssExtractPlugin()
  ],
  
  // 5. Mode:模式
  mode: 'production' // development | production
};

工作原理(简化版):

复制代码
// 1. 初始化参数
const options = require('./webpack.config.js');

// 2. 创建 Compiler 对象
const compiler = new Webpack(options);

// 3. 从 entry 开始递归分析依赖
compiler.run((err, stats) => {
  // 4. 调用 loader 转换模块
  // 5. 解析依赖关系,构建依赖图
  // 6. 根据依赖图生成 chunk
  // 7. 输出到文件系统
});

打包流程详解:

复制代码
1. 初始化阶段
   ├─ 读取配置文件
   ├─ 合并 shell 参数
   └─ 初始化 compiler

2. 编译阶段
   ├─ 从 entry 开始
   ├─ 调用 loader 处理模块
   ├─ 使用 acorn 解析 AST
   ├─ 找出依赖模块(import/require)
   └─ 递归处理所有依赖

3. 输出阶段
   ├─ 根据依赖关系组装 chunk
   ├─ 把 chunk 转换成文件
   └─ 输出到 output 目录

3. Loader 和 Plugin 的区别?如何自定义?

区别:

复制代码
Loader:
- 文件转换器,处理单个文件
- 在 module.rules 中配置
- 本质是一个函数,接收源文件,返回转换后的结果

Plugin:
- 功能扩展器,可以访问整个编译生命周期
- 在 plugins 数组中配置
- 本质是一个类,有 apply 方法

自定义 Loader:

复制代码
// my-loader.js
module.exports = function(source) {
  // source 是文件内容
  // this 是 loader 上下文
  
  // 同步 loader
  return source.replace(/console\.log/g, '');
  
  // 异步 loader
  const callback = this.async();
  setTimeout(() => {
    callback(null, source.replace(/console\.log/g, ''));
  }, 1000);
};

// 使用
module.exports = {
  module: {
    rules: [
      {
        test: /\.js$/,
        use: path.resolve(__dirname, 'my-loader.js')
      }
    ]
  }
};

自定义 Plugin:

复制代码
// my-plugin.js
class MyPlugin {
  apply(compiler) {
    // compiler 是 webpack 实例
    
    // 在编译开始时执行
    compiler.hooks.compile.tap('MyPlugin', (params) => {
      console.log('开始编译...');
    });
    
    // 在生成资源到 output 目录之前
    compiler.hooks.emit.tapAsync('MyPlugin', (compilation, callback) => {
      // compilation 包含所有模块信息
      
      // 遍历所有即将输出的文件
      for (let filename in compilation.assets) {
        let content = compilation.assets[filename].source();
        
        // 修改文件内容
        compilation.assets[filename] = {
          source: () => content + '\n// Added by MyPlugin',
          size: () => content.length
        };
      }
      
      callback();
    });
  }
}

module.exports = MyPlugin;

// 使用
plugins: [new MyPlugin()]

4. Webpack 性能优化手段?

构建速度优化:

复制代码
module.exports = {
  // 1. 缩小搜索范围
  resolve: {
    modules: [path.resolve(__dirname, 'node_modules')], // 指定查找目录
    extensions: ['.js', '.json'], // 减少后缀尝试
    alias: {
      '@': path.resolve(__dirname, 'src')
    }
  },
  
  module: {
    rules: [
      {
        test: /\.js$/,
        use: 'babel-loader',
        include: path.resolve(__dirname, 'src'), // 只处理 src
        exclude: /node_modules/ // 排除 node_modules
      }
    ]
  },
  
  // 2. 多线程打包
  module: {
    rules: [
      {
        test: /\.js$/,
        use: [
          {
            loader: 'thread-loader',
            options: { workers: 3 }
          },
          'babel-loader'
        ]
      }
    ]
  },
  
  // 3. 缓存
  cache: {
    type: 'filesystem', // 使用文件系统缓存
    cacheDirectory: path.resolve(__dirname, '.webpack_cache')
  },
  
  // 4. DLL 预编译(不常变的库)
  // 已被 cache 替代,但了解思想
};

打包体积优化:

复制代码
module.exports = {
  optimization: {
    // 1. 代码分割
    splitChunks: {
      chunks: 'all',
      cacheGroups: {
        vendor: {
          test: /[\\/]node_modules[\\/]/,
          name: 'vendors',
          priority: 10
        },
        common: {
          minChunks: 2,
          priority: 5,
          reuseExistingChunk: true
        }
      }
    },
    
    // 2. Tree Shaking(production 默认开启)
    usedExports: true,
    
    // 3. 压缩
    minimize: true,
    minimizer: [
      new TerserPlugin({
        terserOptions: {
          compress: {
            drop_console: true // 删除 console
          }
        }
      }),
      new CssMinimizerPlugin()
    ]
  },
  
  // 4. 动态导入(懒加载)
  // 代码中使用 import() 语法
  
  // 5. 外部扩展(CDN)
  externals: {
    react: 'React',
    'react-dom': 'ReactDOM'
  }
};

// 组件懒加载示例
const Home = lazy(() => import('./pages/Home'));

运行时优化:

复制代码
module.exports = {
  output: {
    // 1. 文件名哈希(利用缓存)
    filename: '[name].[contenthash:8].js',
    chunkFilename: '[name].[contenthash:8].chunk.js'
  },
  
  // 2. Source Map 优化
  devtool: 'source-map', // production
  // devtool: 'eval-cheap-module-source-map', // development
  
  optimization: {
    // 3. 模块 ID 稳定
    moduleIds: 'deterministic',
    
    // 4. Runtime chunk 分离
    runtimeChunk: 'single'
  }
};

5. Vite 为什么比 Webpack 快?原理是什么?

核心区别:

复制代码
Webpack(打包器):
├─ 开发环境:先打包所有模块 → 启动服务器
├─ 修改文件:重新打包相关模块 → HMR
└─ 冷启动慢(大项目 10s+)

Vite(无打包开发服务器):
├─ 开发环境:直接启动服务器 → 按需编译
├─ 修改文件:只重新编译单个模块 → HMR
└─ 冷启动快(1-2s)

Vite 原理:

复制代码
// 1. 开发环境利用浏览器原生 ES Module
// index.html
<script type="module" src="/src/main.js"></script>

// 2. 浏览器请求 main.js
// GET /src/main.js

// 3. Vite 拦截请求,实时编译
import { createApp } from 'vue'; // 浏览器不认识裸模块
// ↓ Vite 转换
import { createApp } from '/@modules/vue.js'; // 指向 node_modules

// 4. 预构建依赖(首次)
// Vite 用 esbuild 预构建 node_modules
// vue、react 等打包成单文件,避免请求瀑布

// 5. HMR 原理
// WebSocket 通信,模块热替换
if (import.meta.hot) {
  import.meta.hot.accept((newModule) => {
    // 只更新当前模块
  });
}

对比总结:

复制代码
// Webpack 开发流程
entry → 递归解析 → loader 转换 → 打包成 bundle → 启动服务 (慢)

// Vite 开发流程
启动服务 (快) → 浏览器请求 → 实时编译单个文件 → 返回

// 生产环境
Vite 用 Rollup 打包(和 Webpack 类似)

🎯 三、代码质量

6. ESLint、Prettier、Husky、lint-staged 的作用和配置?

完整配置流程:

复制代码
// package.json
{
  "scripts": {
    "lint": "eslint src --ext .js,.jsx,.ts,.tsx",
    "lint:fix": "eslint src --ext .js,.jsx,.ts,.tsx --fix",
    "format": "prettier --write \"src/**/*.{js,jsx,ts,tsx,json,css,md}\"",
    "prepare": "husky install"
  },
  "devDependencies": {
    "eslint": "^8.0.0",
    "prettier": "^2.8.0",
    "husky": "^8.0.0",
    "lint-staged": "^13.0.0"
  },
  "lint-staged": {
    "*.{js,jsx,ts,tsx}": [
      "eslint --fix",
      "prettier --write"
    ],
    "*.{json,css,md}": [
      "prettier --write"
    ]
  }
}

ESLint 配置:

复制代码
// .eslintrc.js
module.exports = {
  env: {
    browser: true,
    es2021: true,
    node: true
  },
  extends: [
    'eslint:recommended',
    'plugin:react/recommended',
    'plugin:@typescript-eslint/recommended',
    'prettier' // 必须放最后,关闭 ESLint 中与 Prettier 冲突的规则
  ],
  parser: '@typescript-eslint/parser',
  parserOptions: {
    ecmaVersion: 'latest',
    sourceType: 'module',
    ecmaFeatures: {
      jsx: true
    }
  },
  plugins: ['react', '@typescript-eslint'],
  rules: {
    'no-console': 'warn',
    'no-unused-vars': 'error',
    'react/prop-types': 'off'
  }
};

Prettier 配置:

复制代码
// .prettierrc.js
module.exports = {
  semi: true,              // 分号
  singleQuote: true,       // 单引号
  tabWidth: 2,             // 缩进
  trailingComma: 'es5',    // 尾逗号
  printWidth: 80,          // 每行宽度
  arrowParens: 'avoid'     // 箭头函数参数括号
};

Husky + lint-staged:

复制代码
# 1. 安装
npm install husky lint-staged -D

# 2. 初始化 husky
npx husky install

# 3. 添加 pre-commit hook
npx husky add .husky/pre-commit "npx lint-staged"

# 4. 添加 commit-msg hook (可选,配合 commitlint)
npx husky add .husky/commit-msg 'npx --no -- commitlint --edit $1'

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

npx lint-staged

Commitlint 配置(可选):

复制代码
// commitlint.config.js
module.exports = {
  extends: ['@commitlint/config-conventional'],
  rules: {
    'type-enum': [
      2,
      'always',
      ['feat', 'fix', 'docs', 'style', 'refactor', 'test', 'chore']
    ],
    'subject-case': [0]
  }
};

// 提交格式:
// feat: 新功能
// fix: 修复 bug
// docs: 文档修改
// style: 代码格式(不影响功能)
// refactor: 重构
// test: 测试
// chore: 构建/工具

工作流程:

复制代码
1. 开发者修改代码
2. git add .
3. git commit -m "feat: 添加登录功能"
   ↓
4. Husky 触发 pre-commit hook
   ↓
5. lint-staged 只检查暂存区文件
   ├─ 运行 eslint --fix
   ├─ 运行 prettier --write
   └─ 自动修复并重新 add
   ↓
6. Husky 触发 commit-msg hook
   └─ commitlint 检查提交信息格式
   ↓
7. 通过则提交成功,否则中断

🔄 四、CI/CD

7. 前端 CI/CD 流程和工具?

完整流程:

复制代码
代码提交 → 自动化测试 → 构建打包 → 部署上线 → 监控回滚

GitHub Actions 示例:

复制代码
# .github/workflows/deploy.yml
name: Deploy to Production

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

jobs:
  # 1. 测试和构建
  build:
    runs-on: ubuntu-latest
    
    steps:
      - name: Checkout 代码
        uses: actions/checkout@v3
      
      - name: 设置 Node.js
        uses: actions/setup-node@v3
        with:
          node-version: '18'
          cache: 'npm'
      
      - name: 安装依赖
        run: npm ci
      
      - name: 代码检查
        run: npm run lint
      
      - name: 单元测试
        run: npm run test
      
      - name: 构建
        run: npm run build
      
      - name: 上传构建产物
        uses: actions/upload-artifact@v3
        with:
          name: dist
          path: dist/
  
  # 2. 部署到服务器
  deploy:
    needs: build
    runs-on: ubuntu-latest
    if: github.ref == 'refs/heads/main'
    
    steps:
      - name: 下载构建产物
        uses: actions/download-artifact@v3
        with:
          name: dist
      
      - name: 部署到阿里云 OSS
        uses: fangbinwei/aliyun-oss-website-action@v1
        with:
          accessKeyId: ${{ secrets.ACCESS_KEY_ID }}
          accessKeySecret: ${{ secrets.ACCESS_KEY_SECRET }}
          bucket: my-bucket
          endpoint: oss-cn-hangzhou.aliyuncs.com
          folder: dist/
      
      - name: 清除 CDN 缓存
        run: |
          curl -X POST https://api.cdn.com/purge \
            -H "Authorization: Bearer ${{ secrets.CDN_TOKEN }}"

GitLab CI 示例:

复制代码
# .gitlab-ci.yml
stages:
  - test
  - build
  - deploy

variables:
  NODE_VERSION: "18"

# 缓存依赖
cache:
  paths:
    - node_modules/

# 测试阶段
test:
  stage: test
  image: node:$NODE_VERSION
  script:
    - npm ci
    - npm run lint
    - npm run test:unit
    - npm run test:e2e
  coverage: '/Statements\s+:\s+(\d+\.\d+)%/'
  artifacts:
    reports:
      coverage_report:
        coverage_format: cobertura
        path: coverage/cobertura-coverage.xml

# 构建阶段
build:
  stage: build
  image: node:$NODE_VERSION
  script:
    - npm ci
    - npm run build
  artifacts:
    paths:
      - dist/
    expire_in: 1 week
  only:
    - main
    - develop

# 部署到测试环境
deploy:test:
  stage: deploy
  script:
    - scp -r dist/* user@test-server:/var/www/html/
  environment:
    name: test
    url: https://test.example.com
  only:
    - develop

# 部署到生产环境
deploy:prod:
  stage: deploy
  script:
    - scp -r dist/* user@prod-server:/var/www/html/
    - ssh user@prod-server "pm2 reload app"
  environment:
    name: production
    url: https://www.example.com
  when: manual  # 手动触发
  only:
    - main

Docker 部署:

复制代码
# Dockerfile
# 多阶段构建
FROM node:18-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

# 生产镜像
FROM nginx:alpine
COPY --from=builder /app/dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/nginx.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

# docker-compose.yml
version: '3.8'
services:
  web:
    build: .
    ports:
      - "80:80"
    environment:
      - NODE_ENV=production
    restart: always

部署策略:

复制代码
// 1. 蓝绿部署
// 维护两套环境,快速切换

// 2. 金丝雀发布(灰度发布)
// Nginx 配置示例
upstream backend {
  server v1.example.com weight=9;  # 90% 流量到旧版本
  server v2.example.com weight=1;  # 10% 流量到新版本
}

// 3. 滚动更新
// Kubernetes 示例
spec:
  replicas: 3
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxUnavailable: 1  # 最多 1 个不可用
      maxSurge: 1        # 最多超出 1 个

📊 五、性能监控

8. 前端监控体系如何搭建?

监控分类:

复制代码
// 1. 性能监控
const performanceData = {
  // FP: First Paint
  FP: performance.getEntriesByType('paint')[0].startTime,
  
  // FCP: First Contentful Paint
  FCP: performance.getEntriesByType('paint')[1].startTime,
  
  // LCP: Largest Contentful Paint
  LCP: new PerformanceObserver((list) => {
    const entries = list.getEntries();
    const lastEntry = entries[entries.length - 1];
    console.log('LCP:', lastEntry.renderTime || lastEntry.loadTime);
  }).observe({ entryTypes: ['largest-contentful-paint'] }),
  
  // FID: First Input Delay
  FID: new PerformanceObserver((list) => {
    list.getEntries().forEach(entry => {
      console.log('FID:', entry.processingStart - entry.startTime);
    });
  }).observe({ entryTypes: ['first-input'] }),
  
  // CLS: Cumulative Layout Shift
  CLS: new PerformanceObserver((list) => {
    let cls = 0;
    list.getEntries().forEach(entry => {
      if (!entry.hadRecentInput) {
        cls += entry.value;
      }
    });
    console.log('CLS:', cls);
  }).observe({ entryTypes: ['layout-shift'] }),
  
  // TTFB: Time to First Byte
  TTFB: performance.timing.responseStart - performance.timing.requestStart,
  
  // 页面加载总时间
  loadTime: performance.timing.loadEventEnd - performance.timing.navigationStart
};

// 上报数据
function reportPerformance(data) {
  navigator.sendBeacon('/api/monitor/performance', JSON.stringify(data));
}

错误监控:

复制代码
// 2. JS 错误监控
window.addEventListener('error', (event) => {
  const errorInfo = {
    type: 'jsError',
    message: event.message,
    filename: event.filename,
    lineno: event.lineno,
    colno: event.colno,
    stack: event.error?.stack,
    userAgent: navigator.userAgent,
    url: window.location.href,
    timestamp: Date.now()
  };
  
  reportError(errorInfo);
}, true);

// Promise 错误
window.addEventListener('unhandledrejection', (event) => {
  const errorInfo = {
    type: 'promiseError',
    message: event.reason?.message || event.reason,
    stack: event.reason?.stack,
    timestamp: Date.now()
  };
  
  reportError(errorInfo);
});

// 资源加载错误
window.addEventListener('error', (event) => {
  if (event.target !== window) {
    const errorInfo = {
      type: 'resourceError',
      tagName: event.target.tagName,
      src: event.target.src || event.target.href,
      timestamp: Date.now()
    };
    
    reportError(errorInfo);
  }
}, true);

// React 错误边界
class ErrorBoundary extends React.Component {
  componentDidCatch(error, errorInfo) {
    reportError({
      type: 'reactError',
      message: error.message,
      stack: error.stack,
      componentStack: errorInfo.componentStack
    });
  }
  
  render() {
    return this.props.children;
  }
}

用户行为监控:

复制代码
// 3. PV/UV 统计
function trackPageView() {
  const data = {
    type: 'pv',
    url: window.location.href,
    referrer: document.referrer,
    userId: getUserId(),
    timestamp: Date.now()
  };
  
  reportBehavior(data);
}

// 页面停留时间
let startTime = Date.now();
window.addEventListener('beforeunload', () => {
  const duration = Date.now() - startTime;
  navigator.sendBeacon('/api/monitor/duration', JSON.stringify({
    url: window.location.href,
    duration
  }));
});

// 点击事件埋点
document.addEventListener('click', (event) => {
  const target = event.target;
  const dataTrack = target.getAttribute('data-track');
  
  if (dataTrack) {
    reportBehavior({
      type: 'click',
      element: dataTrack,
      timestamp: Date.now()
    });
  }
});

// 自定义埋点
function trackEvent(eventName, params) {
  reportBehavior({
    type: 'customEvent',
    eventName,
    params,
    timestamp: Date.now()
  });
}

// 使用
trackEvent('购买商品', { productId: '123', price: 99 });

接口监控:

复制代码
// 4. 封装 fetch/axios
const originalFetch = window.fetch;
window.fetch = function(...args) {
  const startTime = Date.now();
  const url = args[0];
  
  return originalFetch.apply(this, args)
    .then(response => {
      const duration = Date.now() - startTime;
      
      reportAPI({
        url,
        method: args[1]?.method || 'GET',
        status: response.status,
        duration,
        success: response.ok
      });
      
      return response;
    })
    .catch(error => {
      reportAPI({
        url,
        method: args[1]?.method || 'GET',
        error: error.message,
        success: false
      });
      
      throw error;
    });
};

上报策略:

复制代码
// 批量上报(节流)
class Monitor {
  constructor() {
    this.queue = [];
    this.timer = null;
  }
  
  // 添加到队列
  addLog(data) {
    this.queue.push(data);
    
    // 超过 10 条立即上报
    if (this.queue.length >= 10) {
      this.flush();
      return;
    }
    
    // 否则 3 秒后上报
    if (!this.timer) {
      this.timer = setTimeout(() => {
        this.flush();
      }, 3000);
    }
  }
  
  // 上报
  flush() {
    if (this.queue.length === 0) return;
    
    // 使用 sendBeacon 保证数据发送(页面卸载时也能发)
    navigator.sendBeacon(
      '/api/monitor/log',
      JSON.stringify(this.queue)
    );
    
    this.queue = [];
    this.timer = null;
  }
}

const monitor = new Monitor();

// 页面卸载时上报剩余数据
window.addEventListener('beforeunload', () => {
  monitor.flush();
});

完整监控 SDK:

复制代码
class MonitorSDK {
  constructor(options) {
    this.appId = options.appId;
    this.apiUrl = options.apiUrl;
    this.userId = this.getUserId();
    
    this.initPerformance();
    this.initError();
    this.initBehavior();
  }
  
  // 生成用户 ID
  getUserId() {
    let userId = localStorage.getItem('monitor_user_id');
    if (!userId) {
      userId = `user_${Date.now()}_${Math.random()}`;
      localStorage.setItem('monitor_user_id', userId);
    }
    return userId;
  }
  
  // 初始化性能监控
  initPerformance() {
    if (window.PerformanceObserver) {
      // 监控 LCP
      new PerformanceObserver((list) => {
        const entries = list.getEntries();
        const lastEntry = entries[entries.length - 1];
        this.report({
          type: 'performance',
          metric: 'LCP',
          value: lastEntry.renderTime || lastEntry.loadTime
        });
      }).observe({ entryTypes: ['largest-contentful-paint'] });
    }
  }
  
  // 初始化错误监控
  initError() {
    window.addEventListener('error', (event) => {
      this.report({
        type: 'error',
        subType: 'jsError',
        message: event.message,
        stack: event.error?.stack
      });
    }, true);
  }
  
  // 初始化行为监控
  initBehavior() {
    // PV
    this.report({
      type: 'behavior',
      subType: 'pv',
      url: window.location.href
    });
  }
  
  // 上报
  report(data) {
    const logData = {
      ...data,
      appId: this.appId,
      userId: this.userId,
      timestamp: Date.now(),
      userAgent: navigator.userAgent,
      url: window.location.href
    };
    
    navigator.sendBeacon(this.apiUrl, JSON.stringify(logData));
  }
}

// 使用
const monitor = new MonitorSDK({
  appId: 'my-app',
  apiUrl: 'https://api.example.com/monitor'
});

🧪 六、测试

9. 前端测试的分类和工具?

测试金字塔:

复制代码
        /\
       /E2E\        端到端测试 (少量)
      /------\
     /集成测试 \     组件集成 (适量)
    /----------\
   /  单元测试   \   函数/组件 (大量)
  /--------------\

单元测试(Jest + Testing Library):

复制代码
// utils.js
export function add(a, b) {
  return a + b;
}

export function formatPrice(price) {
  return `¥${price.toFixed(2)}`;
}

// utils.test.js
import { add, formatPrice } from './utils';

describe('工具函数测试', () => {
  test('add 函数', () => {
    expect(add(1, 2)).toBe(3);
    expect(add(-1, 1)).toBe(0);
  });
  
  test('formatPrice 函数', () => {
    expect(formatPrice(100)).toBe('¥100.00');
    expect(formatPrice(99.9)).toBe('¥99.90');
  });
});

// Button.jsx
function Button({ onClick, children }) {
  return <button onClick={onClick}>{children}</button>;
}

// Button.test.jsx
import { render, screen, fireEvent } from '@testing-library/react';
import Button from './Button';

describe('Button 组件', () => {
  test('渲染正确的文本', () => {
    render(<Button>点击我</Button>);
    expect(screen.getByText('点击我')).toBeInTheDocument();
  });
  
  test('点击事件触发', () => {
    const handleClick = jest.fn();
    render(<Button onClick={handleClick}>点击</Button>);
    
    fireEvent.click(screen.getByText('点击'));
    expect(handleClick).toHaveBeenCalledTimes(1);
  });
});

集成测试:

复制代码
// TodoList.test.jsx
import { render, screen, fireEvent } from '@testing-library/react';
import TodoList from './TodoList';

test('添加和删除待办事项', () => {
  render(<TodoList />);
  
  // 添加待办
  const input = screen.getByPlaceholderText('输入待办事项');
  const addButton = screen.getByText('添加');
  
  fireEvent.change(input, { target: { value: '买菜' } });
  fireEvent.click(addButton);
  
  expect(screen.getByText('买菜')).toBeInTheDocument();
  
  // 删除待办
  const deleteButton = screen.getByText('删除');
  fireEvent.click(deleteButton);
  
  expect(screen.queryByText('买菜')).not.toBeInTheDocument();
});

E2E 测试(Cypress):

复制代码
// cypress/e2e/login.cy.js
describe('登录流程', () => {
  beforeEach(() => {
    cy.visit('http://localhost:3000/login');
  });
  
  it('成功登录', () => {
    // 输入用户名和密码
    cy.get('input[name="username"]').type('admin');
    cy.get('input[name="password"]').type('123456');
    
    // 点击登录按钮
    cy.get('button[type="submit"]').click();
    
    // 验证跳转到首页
    cy.url().should('include', '/home');
    cy.contains('欢迎回来').should('be.visible');
  });
  
  it('登录失败提示', () => {
    cy.get('input[name="username"]').type('wrong');
    cy.get('input[name="password"]').type('wrong');
    cy.get('button[type="submit"]').click();
    
    cy.contains('用户名或密码错误').should('be.visible');
  });
});

覆盖率配置:

复制代码
// jest.config.js
module.exports = {
  collectCoverage: true,
  collectCoverageFrom: [
    'src/**/*.{js,jsx,ts,tsx}',
    '!src/**/*.test.{js,jsx}',
    '!src/index.js'
  ],
  coverageThreshold: {
    global: {
      statements: 80,
      branches: 80,
      functions: 80,
      lines: 80
    }
  }
};

🎁 七、综合实战题

10. 从0到1搭建一个完整的前端工程?

完整清单:

复制代码
# 1. 初始化项目
npm create vite@latest my-project -- --template react-ts
cd my-project

# 2. 安装依赖
npm install
npm install -D eslint prettier husky lint-staged @commitlint/cli @commitlint/config-conventional

# 3. 配置代码规范
npm init @eslint/config
echo "module.exports = { semi: true, singleQuote: true };" > .prettierrc.js

# 4. 配置 Git Hooks
npx husky install
npx husky add .husky/pre-commit "npx lint-staged"
npx husky add .husky/commit-msg 'npx --no -- commitlint --edit $1'

# 5. 配置环境变量
echo "VITE_API_URL=http://localhost:3000" > .env.development
echo "VITE_API_URL=https://api.example.com" > .env.production

# 6. 目录结构
mkdir -p src/{components,pages,utils,hooks,store,styles,types,api}

# 7. 配置路径别名
# vite.config.ts 中添加 resolve.alias

# 8. 配置 CI/CD
mkdir .github/workflows
# 创建 deploy.yml

# 9. 配置监控
# 引入 Sentry 或自建监控

# 10. 文档
echo "# Project Documentation" > README.md

目录结构:

复制代码
my-project/
├── .github/
│   └── workflows/
│       └── deploy.yml
├── .husky/
│   ├── pre-commit
│   └── commit-msg
├── public/
├── src/
│   ├── api/              # API 请求
│   ├── components/       # 通用组件
│   ├── hooks/            # 自定义 Hooks
│   ├── pages/            # 页面组件
│   ├── store/            # 状态管理
│   ├── styles/           # 全局样式
│   ├── types/            # TS 类型定义
│   ├── utils/            # 工具函数
│   ├── App.tsx
│   └── main.tsx
├── .env.development
├── .env.production
├── .eslintrc.js
├── .prettierrc.js
├── commitlint.config.js
├── tsconfig.json
├── vite.config.ts
└── package.json
相关推荐
zach28 分钟前
前端落地实战:Next.js 项目 Nginx + PM2 生产部署全流程(附踩坑总结)
前端·pm2·next.js
overmind37 分钟前
oeasy教h5前端001 修改的乐趣_直接修改在线网页
前端
只睡四小时42 分钟前
JS 手写 D-pad 空间导航:电视端焦点引擎实战
android·开发语言·前端·javascript·ecmascript·hls·空间导航
恋猫de小郭1 小时前
Android 原生的 Compose A2UI 也来了,你还抱着 XML 养老吗?
android·前端·flutter
庄园特聘拆椅狂魔1 小时前
从连连看到粒子消散——前端小游戏与视觉特效的技术选型
前端
JudithHuang1 小时前
React 常用 Hooks
前端·react.js·前端框架
sycmancia2 小时前
Qml——Window元素使用
前端
每天都好困2 小时前
03 AR 投影与误差修正:把地图上的点贴到视频画面上
前端
每天都好困2 小时前
04 地图图层封装:像管理 Photoshop 图层一样管理地图元素
前端
去伪存真3 小时前
构建你的第一个 DevOps AI Agent:自动化捕获、分析并修复 CI 故障
前端·agent