JavaScript 模块化与工程化实践

27 阅读9分钟

欢迎使用我的小程序👇👇👇👇👇

扫码_搜索联合传播样式-标准色版2.png

引言

在前几篇文章中,我们学习了JavaScript的基础知识、ES6特性、数组对象操作、异步编程以及错误处理。本文将继续深入,探讨现代JavaScript开发中至关重要的模块化与工程化实践,帮助你构建可维护、可扩展的大型应用。

一、JavaScript模块化演进史

1.1 模块化前夜:全局变量污染

// 原始方式:全局命名空间污染
var userData = {
  name: 'John',
  age: 25
};

function getUserName() {
  return userData.name;
}

function updateUser(updates) {
  for (var key in updates) {
    userData[key] = updates[key];
  }
}

// 问题:所有变量和函数都在全局作用域
// 容易造成命名冲突、难以维护

1.2 立即执行函数表达式(IIFE)

// IIFE模式:创建私有作用域
var UserModule = (function() {
  // 私有变量
  var userData = {
    name: 'John',
    age: 25
  };

  // 私有方法
  function validateUser(data) {
    return data.name && data.age > 0;
  }

  // 公共API
  return {
    getUserName: function() {
      return userData.name;
    },
    updateUser: function(updates) {
      if (validateUser(updates)) {
        for (var key in updates) {
          userData[key] = updates[key];
        }
        return true;
      }
      return false;
    },
    getUserInfo: function() {
      return { ...userData };
    }
  };
})();

// 使用
console.log(UserModule.getUserName()); // John
UserModule.updateUser({ age: 26 });

1.3 CommonJS(Node.js)

// math.js
function add(a, b) {
  return a + b;
}

function subtract(a, b) {
  return a - b;
}

// 导出
module.exports = {
  add,
  subtract
};

// 或者
exports.add = add;
exports.subtract = subtract;

// app.js
// 导入
const math = require('./math.js');
console.log(math.add(5, 3)); // 8

// 或者解构导入
const { add, subtract } = require('./math.js');

1.4 AMD(异步模块定义)

// 使用RequireJS
// math.js
define(['dependency'], function(dependency) {
  // 模块代码
  function add(a, b) {
    return a + b;
  }
  
  return {
    add: add
  };
});

// app.js
require(['math'], function(math) {
  console.log(math.add(2, 3)); // 5
});

二、ES6模块系统

2.1 基本语法

// math.js - 命名导出
export const PI = 3.14159;

export function add(a, b) {
  return a + b;
}

export function multiply(a, b) {
  return a * b;
}

// 或者统一导出
const PI = 3.14159;
const add = (a, b) => a + b;
const multiply = (a, b) => a * b;

export { PI, add, multiply };

// user.js - 默认导出
class User {
  constructor(name, email) {
    this.name = name;
    this.email = email;
  }
  
  getInfo() {
    return `${this.name} <${this.email}>`;
  }
}

export default User;

// 或者默认导出函数
export default function createUser(name, email) {
  return {
    name,
    email,
    createdAt: new Date()
  };
}

2.2 导入方式

// app.js - 各种导入方式
// 1. 命名导入
import { add, multiply, PI } from './math.js';
console.log(add(2, 3)); // 5
console.log(PI); // 3.14159

// 2. 重命名导入
import { add as addNumbers, multiply as times } from './math.js';

// 3. 导入全部
import * as MathUtils from './math.js';
console.log(MathUtils.add(1, 2));

// 4. 默认导入
import User from './user.js';
const user = new User('John', 'john@example.com');

// 或者重命名默认导入
import { default as UserClass } from './user.js';

// 5. 混合导入
import User, { PI } from './user.js'; // 如果user.js有默认导出和命名导出

// 6. 动态导入(按需加载)
async function loadModule() {
  const module = await import('./math.js');
  console.log(module.add(5, 3));
}

button.addEventListener('click', () => {
  loadModule();
});

// 7. 内联导入(Web Worker、Service Worker等)
if (typeof window !== 'undefined') {
  import('./browser-module.js').then(module => {
    // 浏览器特有代码
  });
} else {
  import('./node-module.js').then(module => {
    // Node.js特有代码
  });
}

2.3 模块特性详解

// 1. 静态分析特性
// ES6模块在编译时确定依赖关系,支持静态优化

// 2. 严格模式
// 模块默认在严格模式下运行,无需'use strict'

// 3. 顶级作用域
// 每个模块有自己的顶级作用域,变量不会污染全局

// 4. 值引用 vs 值拷贝
// counter.js
export let counter = 0;
export function increment() {
  counter++;
}

// app.js
import { counter, increment } from './counter.js';

console.log(counter); // 0
increment();
console.log(counter); // 1 - 直接引用,值已更新

// 5. 循环依赖处理
// a.js
import { b } from './b.js';
export const a = 'a';
console.log('模块a中b的值:', b);

// b.js
import { a } from './a.js';
export const b = 'b';
console.log('模块b中a的值:', a); // 注意:此时a可能是undefined

三、现代打包工具

3.1 Webpack基础配置

// webpack.config.js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');

module.exports = {
  // 入口文件
  entry: {
    main: './src/index.js',
    vendor: './src/vendor.js'
  },
  
  // 输出配置
  output: {
    filename: '[name].[contenthash].js',
    path: path.resolve(__dirname, 'dist'),
    publicPath: '/',
    chunkFilename: '[name].[contenthash].chunk.js'
  },
  
  // 开发模式
  mode: process.env.NODE_ENV === 'production' ? 'production' : 'development',
  
  // Source Map配置
  devtool: process.env.NODE_ENV === 'production' 
    ? 'source-map' 
    : 'cheap-module-eval-source-map',
  
  // 开发服务器
  devServer: {
    contentBase: './dist',
    hot: true,
    port: 3000,
    historyApiFallback: true,
    proxy: {
      '/api': {
        target: 'http://localhost:8080',
        changeOrigin: true
      }
    }
  },
  
  // 模块解析
  resolve: {
    extensions: ['.js', '.jsx', '.ts', '.tsx'],
    alias: {
      '@': path.resolve(__dirname, 'src/'),
      '@components': path.resolve(__dirname, 'src/components/'),
      '@utils': path.resolve(__dirname, 'src/utils/')
    }
  },
  
  // 模块处理规则
  module: {
    rules: [
      // JavaScript/TypeScript
      {
        test: /\.(js|jsx|ts|tsx)$/,
        exclude: /node_modules/,
        use: {
          loader: 'babel-loader',
          options: {
            presets: [
              '@babel/preset-env',
              '@babel/preset-react',
              '@babel/preset-typescript'
            ],
            plugins: [
              '@babel/plugin-transform-runtime',
              '@babel/plugin-proposal-class-properties',
              'babel-plugin-lodash' // 按需加载lodash
            ]
          }
        }
      },
      
      // 样式文件
      {
        test: /\.css$/,
        use: [
          'style-loader',
          {
            loader: 'css-loader',
            options: {
              modules: {
                auto: true,
                localIdentName: '[name]__[local]--[hash:base64:5]'
              }
            }
          },
          'postcss-loader'
        ]
      },
      
      // Sass/SCSS
      {
        test: /\.scss$/,
        use: [
          'style-loader',
          'css-loader',
          'postcss-loader',
          'sass-loader'
        ]
      },
      
      // 图片资源
      {
        test: /\.(png|jpg|jpeg|gif|svg)$/,
        use: [
          {
            loader: 'url-loader',
            options: {
              limit: 8192, // 小于8KB转为base64
              name: 'assets/images/[name].[hash:8].[ext]'
            }
          },
          {
            loader: 'image-webpack-loader',
            options: {
              mozjpeg: {
                progressive: true,
                quality: 65
              },
              optipng: {
                enabled: false
              },
              pngquant: {
                quality: [0.65, 0.90],
                speed: 4
              },
              gifsicle: {
                interlaced: false
              }
            }
          }
        ]
      },
      
      // 字体文件
      {
        test: /\.(woff|woff2|eot|ttf|otf)$/,
        use: [
          {
            loader: 'file-loader',
            options: {
              name: 'assets/fonts/[name].[hash:8].[ext]'
            }
          }
        ]
      }
    ]
  },
  
  // 插件
  plugins: [
    new CleanWebpackPlugin(),
    new HtmlWebpackPlugin({
      template: './public/index.html',
      favicon: './public/favicon.ico',
      minify: process.env.NODE_ENV === 'production' ? {
        removeComments: true,
        collapseWhitespace: true,
        removeRedundantAttributes: true,
        useShortDoctype: true,
        removeEmptyAttributes: true,
        removeStyleLinkTypeAttributes: true,
        keepClosingSlash: true,
        minifyJS: true,
        minifyCSS: true,
        minifyURLs: true
      } : false
    })
  ],
  
  // 优化配置
  optimization: {
    moduleIds: 'deterministic',
    runtimeChunk: 'single',
    splitChunks: {
      cacheGroups: {
        vendor: {
          test: /[\\/]node_modules[\\/]/,
          name: 'vendors',
          chunks: 'all'
        },
        common: {
          name: 'common',
          minChunks: 2,
          chunks: 'all',
          priority: 10,
          reuseExistingChunk: true,
          enforce: true
        }
      }
    },
    minimizer: [
      // 使用TerserPlugin压缩JavaScript
      // 使用CssMinimizerPlugin压缩CSS
    ]
  },
  
  // 性能提示
  performance: {
    hints: process.env.NODE_ENV === 'production' ? 'warning' : false,
    maxAssetSize: 250000,
    maxEntrypointSize: 250000
  }
};

3.2 高级Webpack配置技巧

// webpack.config.advanced.js
const webpack = require('webpack');
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin');
const TerserPlugin = require('terser-webpack-plugin');

// 多环境配置
const configs = {
  development: {
    mode: 'development',
    devtool: 'cheap-module-source-map'
  },
  
  production: {
    mode: 'production',
    devtool: 'source-map',
    optimization: {
      minimize: true,
      minimizer: [
        new TerserPlugin({
          parallel: true,
          terserOptions: {
            compress: {
              drop_console: true, // 移除console
              drop_debugger: true
            },
            output: {
              comments: false // 移除注释
            }
          },
          extractComments: false
        }),
        new CssMinimizerPlugin()
      ]
    }
  },
  
  staging: {
    mode: 'production',
    devtool: 'source-map',
    optimization: {
      minimize: true,
      minimizer: [
        new TerserPlugin({
          terserOptions: {
            compress: {
              drop_console: false, // 测试环境保留console
              drop_debugger: true
            }
          }
        })
      ]
    }
  }
};

// 根据环境选择配置
const environment = process.env.NODE_ENV || 'development';
const envConfig = configs[environment];

module.exports = {
  ...envConfig,
  
  plugins: [
    // 环境变量注入
    new webpack.DefinePlugin({
      'process.env.NODE_ENV': JSON.stringify(environment),
      'process.env.API_URL': JSON.stringify(process.env.API_URL || 'http://localhost:3000')
    }),
    
    // 进度条
    new webpack.ProgressPlugin(),
    
    // 提取CSS到单独文件(生产环境)
    ...(environment === 'production' ? [
      new MiniCssExtractPlugin({
        filename: 'css/[name].[contenthash].css',
        chunkFilename: 'css/[name].[contenthash].chunk.css'
      })
    ] : []),
    
    // 打包分析(需要时启用)
    ...(process.env.ANALYZE ? [
      new BundleAnalyzerPlugin({
        analyzerMode: 'static',
        reportFilename: 'bundle-report.html',
        openAnalyzer: false
      })
    ] : []),
    
    // 忽略无用的moment.js语言包
    new webpack.IgnorePlugin({
      resourceRegExp: /^\.\/locale$/,
      contextRegExp: /moment$/
    })
  ],
  
  // 缓存配置(提升构建速度)
  cache: {
    type: 'filesystem',
    buildDependencies: {
      config: [__filename]
    }
  },
  
  // 性能优化
  performance: {
    maxAssetSize: 512000,
    maxEntrypointSize: 512000,
    hints: environment === 'production' ? 'warning' : false
  },
  
  // 排除某些依赖
  externals: {
    // 从CDN引入的库
    react: 'React',
    'react-dom': 'ReactDOM',
    lodash: '_'
  },
  
  // 实验性功能
  experiments: {
    topLevelAwait: true, // 支持顶层await
    lazyCompilation: environment === 'development' // 开发环境懒编译
  }
};

3.3 Vite配置示例

// vite.config.js
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import { visualizer } from 'rollup-plugin-visualizer';

// https://vitejs.dev/config/
export default defineConfig(({ mode }) => ({
  // 基础路径
  base: mode === 'production' ? '/your-base-path/' : '/',
  
  // 插件
  plugins: [
    react({
      // React刷新
      fastRefresh: true,
      // JSX运行时
      jsxRuntime: 'automatic'
    }),
    // 打包分析
    mode === 'analyze' && visualizer({
      open: true,
      gzipSize: true,
      brotliSize: true
    })
  ].filter(Boolean),
  
  // 解析配置
  resolve: {
    alias: {
      '@': '/src',
      '@components': '/src/components',
      '@assets': '/src/assets'
    }
  },
  
  // 开发服务器
  server: {
    port: 3000,
    open: true,
    proxy: {
      '/api': {
        target: 'http://localhost:8080',
        changeOrigin: true,
        rewrite: (path) => path.replace(/^\/api/, '')
      }
    },
    hmr: {
      overlay: true
    }
  },
  
  // 构建配置
  build: {
    outDir: 'dist',
    assetsDir: 'assets',
    sourcemap: mode !== 'production',
    minify: mode === 'production' ? 'terser' : false,
    terserOptions: mode === 'production' ? {
      compress: {
        drop_console: true,
        drop_debugger: true
      }
    } : undefined,
    rollupOptions: {
      output: {
        manualChunks(id) {
          // 将node_modules中的包分块
          if (id.includes('node_modules')) {
            if (id.includes('react') || id.includes('react-dom')) {
              return 'react-vendor';
            }
            if (id.includes('lodash')) {
              return 'lodash';
            }
            return 'vendor';
          }
        },
        chunkFileNames: 'assets/js/[name]-[hash].js',
        entryFileNames: 'assets/js/[name]-[hash].js',
        assetFileNames: ({ name }) => {
          if (/\.(gif|jpe?g|png|svg)$/.test(name ?? '')) {
            return 'assets/images/[name]-[hash][extname]';
          }
          if (/\.css$/.test(name ?? '')) {
            return 'assets/css/[name]-[hash][extname]';
          }
          return 'assets/[name]-[hash][extname]';
        }
      }
    },
    // 分块大小警告阈值
    chunkSizeWarningLimit: 1000
  },
  
  // 预览服务器
  preview: {
    port: 3001,
    open: true
  },
  
  // CSS配置
  css: {
    modules: {
      localsConvention: 'camelCaseOnly',
      generateScopedName: mode === 'production' 
        ? '[hash:base64:8]' 
        : '[name]__[local]--[hash:base64:5]'
    },
    preprocessorOptions: {
      scss: {
        additionalData: `@import "./src/styles/variables.scss";`
      }
    }
  },
  
  // 环境变量
  define: {
    __APP_VERSION__: JSON.stringify(process.env.npm_package_version),
    __BUILD_TIME__: JSON.stringify(new Date().toISOString())
  }
}));

四、代码质量工具

4.1 ESLint配置

// .eslintrc.js
module.exports = {
  // 解析器配置
  parser: '@babel/eslint-parser',
  parserOptions: {
    ecmaVersion: 2022,
    sourceType: 'module',
    ecmaFeatures: {
      jsx: true
    },
    requireConfigFile: false,
    babelOptions: {
      presets: ['@babel/preset-env', '@babel/preset-react']
    }
  },
  
  // 运行环境
  env: {
    browser: true,
    node: true,
    es2022: true,
    jest: true
  },
  
  // 扩展规则集
  extends: [
    'eslint:recommended',
    'plugin:react/recommended',
    'plugin:react-hooks/recommended',
    'plugin:jsx-a11y/recommended',
    'plugin:import/recommended',
    'plugin:prettier/recommended'
  ],
  
  // 插件
  plugins: [
    'react',
    'react-hooks',
    'jsx-a11y',
    'import',
    'prettier'
  ],
  
  // 自定义规则
  rules: {
    // 代码风格
    'prettier/prettier': 'error',
    
    // 最佳实践
    'no-console': ['warn', { allow: ['warn', 'error', 'info'] }],
    'no-debugger': 'warn',
    'no-alert': 'warn',
    'no-var': 'error',
    'prefer-const': 'error',
    'prefer-template': 'error',
    'object-shorthand': 'error',
    
    // import/export
    'import/order': [
      'error',
      {
        groups: [
          'builtin',
          'external',
          'internal',
          'parent',
          'sibling',
          'index'
        ],
        'newlines-between': 'always',
        alphabetize: {
          order: 'asc',
          caseInsensitive: true
        }
      }
    ],
    'import/no-unresolved': 'error',
    'import/named': 'error',
    'import/default': 'error',
    'import/export': 'error',
    
    // React
    'react/prop-types': 'off', // TypeScript项目中可以关闭
    'react/react-in-jsx-scope': 'off', // React 17+不需要
    'react/jsx-uses-react': 'off',
    'react/display-name': 'off',
    'react/jsx-key': 'error',
    'react/jsx-no-target-blank': 'error',
    
    // React Hooks
    'react-hooks/rules-of-hooks': 'error',
    'react-hooks/exhaustive-deps': 'warn',
    
    // 可访问性
    'jsx-a11y/anchor-is-valid': [
      'error',
      {
        components: ['Link'],
        specialLink: ['hrefLeft', 'hrefRight'],
        aspects: ['invalidHref', 'preferButton']
      }
    ]
  },
  
  // 覆盖特定文件的规则
  overrides: [
    {
      files: ['*.ts', '*.tsx'],
      parser: '@typescript-eslint/parser',
      extends: [
        'plugin:@typescript-eslint/recommended',
        'plugin:@typescript-eslint/recommended-requiring-type-checking'
      ],
      parserOptions: {
        project: ['./tsconfig.json']
      },
      rules: {
        '@typescript-eslint/explicit-function-return-type': 'off',
        '@typescript-eslint/explicit-module-boundary-types': 'off',
        '@typescript-eslint/no-explicit-any': 'warn',
        '@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }],
        '@typescript-eslint/no-non-null-assertion': 'warn'
      }
    },
    {
      files: ['*.test.js', '*.test.jsx', '*.test.ts', '*.test.tsx'],
      env: {
        jest: true
      },
      rules: {
        'import/no-extraneous-dependencies': 'off'
      }
    },
    {
      files: ['*.config.js', '*.config.ts'],
      env: {
        node: true
      },
      rules: {
        'import/no-extraneous-dependencies': 'off'
      }
    }
  ],
  
  // 设置
  settings: {
    react: {
      version: 'detect' // 自动检测React版本
    },
    'import/resolver': {
      node: {
        extensions: ['.js', '.jsx', '.ts', '.tsx'],
        moduleDirectory: ['node_modules', 'src/']
      },
      typescript: {
        alwaysTryTypes: true
      }
    }
  }
};

4.2 Prettier配置

// .prettierrc.js
module.exports = {
  // 每行最大字符数
  printWidth: 100,
  
  // 缩进空格数
  tabWidth: 2,
  
  // 使用制表符而不是空格缩进
  useTabs: false,
  
  // 语句末尾是否添加分号
  semi: true,
  
  // 使用单引号
  singleQuote: true,
  
  // 对象属性引号:as-needed(仅在需要时添加)
  quoteProps: 'as-needed',
  
  // JSX中使用单引号
  jsxSingleQuote: false,
  
  // 尾随逗号
  trailingComma: 'es5',
  
  // 对象花括号内的空格
  bracketSpacing: true,
  
  // JSX标签的'>'换行
  jsxBracketSameLine: false,
  
  // 箭头函数参数括号:avoid(能省略时省略)
  arrowParens: 'avoid',
  
  // 换行符:lf(Linux/macOS风格)
  endOfLine: 'lf',
  
  // HTML空白敏感度
  htmlWhitespaceSensitivity: 'css',
  
  // Vue文件脚本和样式标签缩进
  vueIndentScriptAndStyle: false,
  
  // 格式化嵌入的代码
  embeddedLanguageFormatting: 'auto',
  
  // 超过printWidth时是否折行
  proseWrap: 'preserve'
};

// package.json中的脚本配置
/*
{
  "scripts": {
    "format": "prettier --write \"src/**/*.{js,jsx,ts,tsx,css,md,json}\"",
    "format:check": "prettier --check \"src/**/*.{js,jsx,ts,tsx,css,md,json}\"",
    "lint": "eslint \"src/**/*.{js,jsx,ts,tsx}\"",
    "lint:fix": "eslint \"src/**/*.{js,jsx,ts,tsx}\" --fix"
  }
}
*/

4.3 TypeScript配置

// tsconfig.json
{
  "compilerOptions": {
    /* 基本选项 */
    "target": "ES2022", // 编译目标
    "lib": ["DOM", "DOM.Iterable", "ES2022"], // 使用的库
    "module": "ESNext", // 模块系统
    "moduleResolution": "node", // 模块解析策略
    "baseUrl": ".", // 解析非相对模块的基础路径
    "paths": { // 路径映射
      "@/*": ["src/*"],
      "@components/*": ["src/components/*"],
      "@utils/*": ["src/utils/*"]
    },
    
    /* 严格类型检查 */
    "strict": true,
    "noImplicitAny": true,
    "strictNullChecks": true,
    "strictFunctionTypes": true,
    "strictBindCallApply": true,
    "strictPropertyInitialization": true,
    "noImplicitThis": true,
    "alwaysStrict": true,
    
    /* 额外检查 */
    "noUnusedLocals": true, // 未使用的局部变量报错
    "noUnusedParameters": true, // 未使用的参数报错
    "noImplicitReturns": true, // 不是所有路径都返回值时报错
    "noFallthroughCasesInSwitch": true, // switch语句中缺少break报错
    
    /* 模块解析 */
    "allowSyntheticDefaultImports": true, // 允许从没有默认导出的模块进行默认导入
    "esModuleInterop": true, // 启用ES模块互操作性
    "resolveJsonModule": true, // 允许导入JSON模块
    
    /* 源代码映射 */
    "sourceMap": true,
    "declaration": true, // 生成.d.ts声明文件
    "declarationMap": true, // 为声明文件生成sourcemap
    "inlineSources": true, // 将源代码包含在sourcemap中
    "declarationDir": "./dist/types", // 声明文件输出目录
    
    /* 实验性特性 */
    "experimentalDecorators": true, // 启用装饰器
    "emitDecoratorMetadata": true, // 为装饰器发出元数据
    
    /* 高级选项 */
    "skipLibCheck": true, // 跳过库文件的类型检查
    "forceConsistentCasingInFileNames": true, // 强制文件名大小写一致
    "allowJs": true, // 允许编译JavaScript文件
    "checkJs": false, // 在.js文件中报告错误
    "outDir": "./dist", // 输出目录
    "rootDir": "./src", // 输入文件根目录
    "removeComments": true, // 移除注释
    "newLine": "lf", // 换行符
    
    /* JSX支持 */
    "jsx": "react-jsx", // React 17+ JSX转换
    
    /* 编译器性能 */
    "incremental": true, // 启用增量编译
    "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.tsbuildinfo" // 增量编译信息文件
  },
  
  /* 包含和排除的文件 */
  "include": [
    "src/**/*.ts",
    "src/**/*.tsx",
    "src/**/*.js",
    "src/**/*.jsx",
    "types/**/*.d.ts"
  ],
  
  "exclude": [
    "node_modules",
    "dist",
    "build",
    "coverage",
    "**/*.test.ts",
    "**/*.test.tsx",
    "**/*.spec.ts",
    "**/*.spec.tsx"
  ]
}

五、测试工具与策略

5.1 Jest测试配置

// jest.config.js
module.exports = {
  // 测试环境
  testEnvironment: 'jsdom',
  
  // 测试文件匹配模式
  testMatch: [
    '<rootDir>/src/**/__tests__/**/*.{js,jsx,ts,tsx}',
    '<rootDir>/src/**/*.{spec,test}.{js,jsx,ts,tsx}'
  ],
  
  // 测试路径忽略
  testPathIgnorePatterns: [
    '<rootDir>/node_modules/',
    '<rootDir>/dist/',
    '<rootDir>/build/'
  ],
  
  // 模块解析
  moduleNameMapper: {
    // 支持路径别名
    '^@/(.*)$': '<rootDir>/src/$1',
    '^@components/(.*)$': '<rootDir>/src/components/$1',
    '^@utils/(.*)$': '<rootDir>/src/utils/$1',
    // 处理样式文件
    '\\.(css|less|scss|sass)$': 'identity-obj-proxy',
    // 处理静态资源
    '\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$':
      '<rootDir>/__mocks__/fileMock.js'
  },
  
  // 设置文件
  setupFilesAfterEnv: ['<rootDir>/jest.setup.js'],
  
  // 收集测试覆盖率
  collectCoverage: true,
  collectCoverageFrom: [
    'src/**/*.{js,jsx,ts,tsx}',
    '!src/**/*.d.ts',
    '!src/**/*.stories.{js,jsx,ts,tsx}',
    '!src/**/index.{js,jsx,ts,tsx}',
    '!src/**/*.test.{js,jsx,ts,tsx}',
    '!src/**/*.spec.{js,jsx,ts,tsx}',
    '!src/test/**/*'
  ],
  coverageDirectory: 'coverage',
  coverageReporters: ['text', 'lcov', 'html'],
  coverageThreshold: {
    global: {
      branches: 80,
      functions: 80,
      lines: 80,
      statements: 80
    }
  },
  
  // 变换配置
  transform: {
    '^.+\\.(js|jsx|ts|tsx)$': 'babel-jest'
  },
  
  // 变换忽略
  transformIgnorePatterns: [
    'node_modules/(?!(lodash-es|@testing-library)/)' // 需要转换的node_modules
  ],
  
  // 重置模块
  resetMocks: true,
  resetModules: true,
  
  // 清除模拟
  clearMocks: true,
  
  // 快照序列化器
  snapshotSerializers: ['@emotion/jest/serializer'],
  
  // 慢测试阈值(秒)
  slowTestThreshold: 5,
  
  // 测试运行前和运行后的脚本
  globalSetup: '<rootDir>/jest.global-setup.js',
  globalTeardown: '<rootDir>/jest.global-teardown.js'
};

// jest.setup.js
import '@testing-library/jest-dom';
import { jest } from '@jest/globals';

// 全局测试配置
global.jest = jest;

// 模拟window对象方法
Object.defineProperty(window, 'matchMedia', {
  writable: true,
  value: jest.fn().mockImplementation(query => ({
    matches: false,
    media: query,
    onchange: null,
    addListener: jest.fn(),
    removeListener: jest.fn(),
    addEventListener: jest.fn(),
    removeEventListener: jest.fn(),
    dispatchEvent: jest.fn()
  }))
});

// 模拟localStorage
const localStorageMock = {
  getItem: jest.fn(),
  setItem: jest.fn(),
  removeItem: jest.fn(),
  clear: jest.fn(),
  length: 0,
  key: jest.fn()
};

global.localStorage = localStorageMock;

// 模拟sessionStorage
const sessionStorageMock = {
  getItem: jest.fn(),
  setItem: jest.fn(),
  removeItem: jest.fn(),
  clear: jest.fn(),
  length: 0,
  key: jest.fn()
};

global.sessionStorage = sessionStorageMock;

// 模拟IntersectionObserver
global.IntersectionObserver = jest.fn(() => ({
  observe: jest.fn(),
  unobserve: jest.fn(),
  disconnect: jest.fn()
}));

// 模拟ResizeObserver
global.ResizeObserver = jest.fn(() => ({
  observe: jest.fn(),
  unobserve: jest.fn(),
  disconnect: jest.fn()
}));

5.2 测试编写示例

// utils/__tests__/math.test.js
import { add, subtract, divide, multiply } from '../math';

describe('数学工具函数', () => {
  describe('add函数', () => {
    test('两个正数相加', () => {
      expect(add(2, 3)).toBe(5);
    });

    test('正数与负数相加', () => {
      expect(add(5, -3)).toBe(2);
    });

    test('两个负数相加', () => {
      expect(add(-2, -3)).toBe(-5);
    });

    test('浮点数相加', () => {
      expect(add(0.1, 0.2)).toBeCloseTo(0.3);
    });
  });

  describe('subtract函数', () => {
    test('正数相减', () => {
      expect(subtract(5, 3)).toBe(2);
    });

    test('负数相减', () => {
      expect(subtract(-2, -3)).toBe(1);
    });
  });

  describe('divide函数', () => {
    test('正常除法', () => {
      expect(divide(10, 2)).toBe(5);
    });

    test('除数为0', () => {
      expect(() => divide(5, 0)).toThrow('除数不能为零');
    });

    test('浮点数除法', () => {
      expect(divide(1, 3)).toBeCloseTo(0.333333);
    });
  });

  describe('multiply函数', () => {
    test('正常乘法', () => {
      expect(multiply(3, 4)).toBe(12);
    });

    test('乘以0', () => {
      expect(multiply(5, 0)).toBe(0);
    });
  });

  // 参数化测试
  const addCases = [
    [1, 2, 3],
    [0, 0, 0],
    [-1, 1, 0],
    [2.5, 2.5, 5]
  ];

  test.each(addCases)('当参数为 %i 和 %i 时,返回 %i', (a, b, expected) => {
    expect(add(a, b)).toBe(expected);
  });
});

// components/__tests__/Button.test.jsx
import React from 'react';
import { render, screen, fireEvent } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import Button from '../Button';

describe('Button组件', () => {
  const defaultProps = {
    onClick: jest.fn(),
    children: '点击我'
  };

  beforeEach(() => {
    jest.clearAllMocks();
  });

  test('渲染正确的文本', () => {
    render(<Button {...defaultProps} />);
    expect(screen.getByText('点击我')).toBeInTheDocument();
  });

  test('点击按钮触发onClick回调', async () => {
    const user = userEvent.setup();
    render(<Button {...defaultProps} />);
    
    const button = screen.getByRole('button');
    await user.click(button);
    
    expect(defaultProps.onClick).toHaveBeenCalledTimes(1);
  });

  test('禁用状态下不能点击', () => {
    render(<Button {...defaultProps} disabled />);
    
    const button = screen.getByRole('button');
    fireEvent.click(button);
    
    expect(defaultProps.onClick).not.toHaveBeenCalled();
    expect(button).toBeDisabled();
  });

  test('加载状态显示加载指示器', () => {
    render(<Button {...defaultProps} loading />);
    
    expect(screen.getByRole('button')).toBeDisabled();
    expect(screen.getByTestId('loading-spinner')).toBeInTheDocument();
  });

  test('支持不同的变体样式', () => {
    const { rerender } = render(<Button {...defaultProps} variant="primary" />);
    expect(screen.getByRole('button')).toHaveClass('btn-primary');
    
    rerender(<Button {...defaultProps} variant="secondary" />);
    expect(screen.getByRole('button')).toHaveClass('btn-secondary');
  });

  test('快照测试', () => {
    const { asFragment } = render(<Button {...defaultProps} />);
    expect(asFragment()).toMatchSnapshot();
  });
});

// hooks/__tests__/useCounter.test.js
import { renderHook, act } from '@testing-library/react-hooks';
import useCounter from '../useCounter';

describe('useCounter自定义Hook', () => {
  test('使用默认初始值0', () => {
    const { result } = renderHook(() => useCounter());
    
    expect(result.current.count).toBe(0);
  });

  test('使用自定义初始值', () => {
    const { result } = renderHook(() => useCounter(10));
    
    expect(result.current.count).toBe(10);
  });

  test('increment函数增加计数', () => {
    const { result } = renderHook(() => useCounter(5));
    
    act(() => {
      result.current.increment();
    });
    
    expect(result.current.count).toBe(6);
  });

  test('decrement函数减少计数', () => {
    const { result } = renderHook(() => useCounter(5));
    
    act(() => {
      result.current.decrement();
    });
    
    expect(result.current.count).toBe(4);
  });

  test('reset函数重置计数', () => {
    const { result } = renderHook(() => useCounter(5));
    
    act(() => {
      result.current.increment();
      result.current.increment();
    });
    
    expect(result.current.count).toBe(7);
    
    act(() => {
      result.current.reset();
    });
    
    expect(result.current.count).toBe(5);
  });

  test('设置最大限制', () => {
    const { result } = renderHook(() => useCounter(0, { max: 3 }));
    
    act(() => {
      result.current.increment();
      result.current.increment();
      result.current.increment();
      result.current.increment(); // 应该不超过3
    });
    
    expect(result.current.count).toBe(3);
  });

  test('设置最小限制', () => {
    const { result } = renderHook(() => useCounter(5, { min: 3 }));
    
    act(() => {
      result.current.decrement();
      result.current.decrement();
      result.current.decrement(); // 应该不低于3
    });
    
    expect(result.current.count).toBe(3);
  });
});

// e2e/__tests__/login.e2e.test.js
describe('登录端到端测试', () => {
  beforeAll(async () => {
    await page.goto('http://localhost:3000/login');
  });

  test('登录表单正常显示', async () => {
    const emailInput = await page.$('input[type="email"]');
    const passwordInput = await page.$('input[type="password"]');
    const submitButton = await page.$('button[type="submit"]');
    
    expect(emailInput).toBeTruthy();
    expect(passwordInput).toBeTruthy();
    expect(submitButton).toBeTruthy();
  });

  test('登录成功重定向', async () => {
    await page.type('input[type="email"]', 'test@example.com');
    await page.type('input[type="password"]', 'password123');
    
    await Promise.all([
      page.click('button[type="submit"]'),
      page.waitForNavigation()
    ]);
    
    expect(page.url()).toContain('/dashboard');
  });

  test('无效凭证显示错误消息', async () => {
    await page.goto('http://localhost:3000/login');
    
    await page.type('input[type="email"]', 'wrong@example.com');
    await page.type('input[type="password"]', 'wrongpassword');
    await page.click('button[type="submit"]');
    
    await page.waitForSelector('.error-message');
    const errorMessage = await page.$eval('.error-message', el => el.textContent);
    
    expect(errorMessage).toContain('无效的凭证');
  });
});

六、项目结构与架构

6.1 推荐的项目结构

my-app/
├── public/                     # 静态资源
│   ├── favicon.ico
│   ├── index.html
│   └── robots.txt
├── src/                       # 源代码
│   ├── assets/               # 静态资源
│   │   ├── images/
│   │   ├── fonts/
│   │   └── styles/
│   │       ├── base/
│   │       ├── components/
│   │       ├── layouts/
│   │       └── variables.scss
│   ├── components/           # 可复用组件
│   │   ├── common/          # 通用组件
│   │   │   ├── Button/
│   │   │   ├── Input/
│   │   │   └── Modal/
│   │   ├── layout/          # 布局组件
│   │   │   ├── Header/
│   │   │   ├── Footer/
│   │   │   └── Sidebar/
│   │   └── features/        # 功能组件
│   ├── containers/          # 容器组件(连接状态)
│   ├── contexts/            # React Context
│   ├── hooks/              # 自定义Hooks
│   ├── pages/              # 页面组件
│   │   ├── Home/
│   │   ├── Login/
│   │   └── Dashboard/
│   ├── routes/             # 路由配置
│   ├── services/           # API服务
│   │   ├── api/
│   │   ├── auth/
│   │   └── storage/
│   ├── store/              # 状态管理
│   │   ├── slices/         # Redux切片
│   │   └── index.ts
│   ├── utils/              # 工具函数
│   │   ├── helpers/
│   │   ├── validators/
│   │   └── constants.ts
│   ├── types/              # TypeScript类型定义
│   ├── test/               # 测试相关
│   │   ├── __mocks__/
│   │   ├── fixtures/
│   │   └── utils/
│   ├── App.tsx
│   ├── main.tsx
│   └── vite-env.d.ts
├── .husky/                  # Git hooks
├── .github/                 # GitHub配置
│   └── workflows/
├── dist/                    # 构建输出
├── coverage/               # 测试覆盖率报告
├── node_modules/
├── .env                    # 环境变量
├── .env.development
├── .env.production
├── .eslintrc.js
├── .prettierrc.js
├── .babelrc
├── tsconfig.json
├── vite.config.js
├── package.json
└── README.md

6.2 模块化架构示例

// src/services/api/httpClient.js
import axios from 'axios';

class HttpClient {
  constructor(baseURL) {
    this.client = axios.create({
      baseURL,
      timeout: 30000,
      headers: {
        'Content-Type': 'application/json'
      }
    });

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

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

  get(url, config = {}) {
    return this.client.get(url, config);
  }

  post(url, data, config = {}) {
    return this.client.post(url, data, config);
  }

  put(url, data, config = {}) {
    return this.client.put(url, data, config);
  }

  patch(url, data, config = {}) {
    return this.client.patch(url, data, config);
  }

  delete(url, config = {}) {
    return this.client.delete(url, config);
  }
}

export default HttpClient;

// src/services/api/index.js
import HttpClient from './httpClient';

const BASE_URL = import.meta.env.VITE_API_BASE_URL || 'http://localhost:3000/api';
export const httpClient = new HttpClient(BASE_URL);

// 用户服务
export const userService = {
  getProfile: () => httpClient.get('/users/profile'),
  updateProfile: (data) => httpClient.put('/users/profile', data),
  getUsers: (params) => httpClient.get('/users', { params })
};

// 产品服务
export const productService = {
  getProducts: (params) => httpClient.get('/products', { params }),
  getProduct: (id) => httpClient.get(`/products/${id}`),
  createProduct: (data) => httpClient.post('/products', data),
  updateProduct: (id, data) => httpClient.put(`/products/${id}`, data),
  deleteProduct: (id) => httpClient.delete(`/products/${id}`)
};

// 订单服务
export const orderService = {
  getOrders: (params) => httpClient.get('/orders', { params }),
  createOrder: (data) => httpClient.post('/orders', data),
  cancelOrder: (id) => httpClient.post(`/orders/${id}/cancel`)
};

6.3 组件设计模式

// src/components/common/Button/index.jsx
import React from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import './Button.scss';

const Button = React.forwardRef(({
  children,
  variant = 'primary',
  size = 'medium',
  disabled = false,
  loading = false,
  fullWidth = false,
  startIcon,
  endIcon,
  className,
  onClick,
  type = 'button',
  ...props
}, ref) => {
  const handleClick = (event) => {
    if (!disabled && !loading && onClick) {
      onClick(event);
    }
  };

  const buttonClasses = classNames(
    'btn',
    `btn--${variant}`,
    `btn--${size}`,
    {
      'btn--disabled': disabled,
      'btn--loading': loading,
      'btn--full-width': fullWidth
    },
    className
  );

  return (
    <button
      ref={ref}
      type={type}
      className={buttonClasses}
      disabled={disabled || loading}
      onClick={handleClick}
      aria-busy={loading}
      {...props}
    >
      {loading && (
        <span className="btn__loader" aria-label="加载中">
          <span className="btn__loader-dot" />
          <span className="btn__loader-dot" />
          <span className="btn__loader-dot" />
        </span>
      )}
      
      {!loading && startIcon && (
        <span className="btn__icon btn__icon--start">
          {startIcon}
        </span>
      )}
      
      <span className="btn__content">{children}</span>
      
      {!loading && endIcon && (
        <span className="btn__icon btn__icon--end">
          {endIcon}
        </span>
      )}
    </button>
  );
});

Button.propTypes = {
  children: PropTypes.node.isRequired,
  variant: PropTypes.oneOf(['primary', 'secondary', 'outline', 'text', 'danger']),
  size: PropTypes.oneOf(['small', 'medium', 'large']),
  disabled: PropTypes.bool,
  loading: PropTypes.bool,
  fullWidth: PropTypes.bool,
  startIcon: PropTypes.node,
  endIcon: PropTypes.node,
  className: PropTypes.string,
  onClick: PropTypes.func,
  type: PropTypes.oneOf(['button', 'submit', 'reset'])
};

Button.displayName = 'Button';

export default Button;

// src/components/common/Button/Button.scss
.btn {
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  border: none;
  border-radius: 4px;
  font-family: inherit;
  font-weight: 500;
  cursor: pointer;
  transition: all 0.2s ease;
  text-decoration: none;
  user-select: none;
  white-space: nowrap;
  vertical-align: middle;
  outline: none;
  
  &:focus-visible {
    outline: 2px solid var(--color-primary);
    outline-offset: 2px;
  }
  
  &--disabled {
    opacity: 0.6;
    cursor: not-allowed;
  }
  
  &--full-width {
    width: 100%;
  }
  
  // 变体
  &--primary {
    background-color: var(--color-primary);
    color: var(--color-white);
    
    &:hover:not(.btn--disabled) {
      background-color: var(--color-primary-dark);
    }
    
    &:active:not(.btn--disabled) {
      background-color: var(--color-primary-darker);
    }
  }
  
  &--secondary {
    background-color: var(--color-secondary);
    color: var(--color-white);
    
    &:hover:not(.btn--disabled) {
      background-color: var(--color-secondary-dark);
    }
  }
  
  &--outline {
    background-color: transparent;
    border: 1px solid var(--color-border);
    color: var(--color-text);
    
    &:hover:not(.btn--disabled) {
      background-color: var(--color-bg-hover);
    }
  }
  
  &--text {
    background-color: transparent;
    color: var(--color-primary);
    
    &:hover:not(.btn--disabled) {
      background-color: var(--color-bg-hover);
    }
  }
  
  &--danger {
    background-color: var(--color-error);
    color: var(--color-white);
    
    &:hover:not(.btn--disabled) {
      background-color: var(--color-error-dark);
    }
  }
  
  // 尺寸
  &--small {
    padding: 6px 12px;
    font-size: 12px;
    line-height: 1.5;
  }
  
  &--medium {
    padding: 8px 16px;
    font-size: 14px;
    line-height: 1.5;
  }
  
  &--large {
    padding: 12px 24px;
    font-size: 16px;
    line-height: 1.5;
  }
  
  // 加载状态
  &--loading {
    .btn__content {
      visibility: hidden;
    }
  }
  
  &__loader {
    position: absolute;
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 4px;
    
    &-dot {
      width: 6px;
      height: 6px;
      border-radius: 50%;
      background-color: currentColor;
      animation: btn-loader-dot 1.4s ease-in-out infinite;
      
      &:nth-child(2) {
        animation-delay: 0.2s;
      }
      
      &:nth-child(3) {
        animation-delay: 0.4s;
      }
    }
  }
  
  &__icon {
    display: inline-flex;
    align-items: center;
    
    &--start {
      margin-right: 4px;
    }
    
    &--end {
      margin-left: 4px;
    }
  }
}

@keyframes btn-loader-dot {
  0%, 60%, 100% {
    transform: scale(1);
    opacity: 1;
  }
  30% {
    transform: scale(1.2);
    opacity: 0.8;
  }
}

七、性能优化

7.1 代码分割与懒加载

// 路由懒加载
import { lazy, Suspense } from 'react';

const HomePage = lazy(() => import('./pages/Home'));
const AboutPage = lazy(() => import('./pages/About'));
const ContactPage = lazy(() => import('./pages/Contact'));

const LoadingFallback = () => (
  <div className="loading-spinner">
    <div className="spinner" />
    <p>加载中...</p>
  </div>
);

function App() {
  return (
    <Suspense fallback={<LoadingFallback />}>
      <Router>
        <Routes>
          <Route path="/" element={<HomePage />} />
          <Route path="/about" element={<AboutPage />} />
          <Route path="/contact" element={<ContactPage />} />
        </Routes>
      </Router>
    </Suspense>
  );
}

// 组件懒加载与预加载
const HeavyComponent = lazy(() => 
  import('./components/HeavyComponent')
    .then(module => {
      // 可以在这里添加一些初始化逻辑
      console.log('HeavyComponent加载完成');
      return module;
    })
);

// 预加载策略
function PrefetchComponent() {
  const [showComponent, setShowComponent] = useState(false);
  
  // 鼠标悬停时预加载
  const handleMouseEnter = () => {
    import('./components/HeavyComponent');
  };
  
  return (
    <div>
      <button 
        onMouseEnter={handleMouseEnter}
        onClick={() => setShowComponent(true)}
      >
        显示重组件
      </button>
      
      {showComponent && (
        <Suspense fallback={<div>加载中...</div>}>
          <HeavyComponent />
        </Suspense>
      )}
    </div>
  );
}

// 基于路由的代码分割
const router = createBrowserRouter([
  {
    path: "/",
    element: <Layout />,
    children: [
      {
        index: true,
        element: <HomePage />
      },
      {
        path: "dashboard",
        async lazy() {
          const { DashboardPage } = await import("./pages/Dashboard");
          return { Component: DashboardPage };
        }
      },
      {
        path: "analytics",
        async lazy() {
          const { AnalyticsPage } = await import("./pages/Analytics");
          return { 
            Component: AnalyticsPage,
            // 可以同时加载数据
            loader: ({ request }) => {
              return fetchAnalyticsData(request);
            }
          };
        }
      }
    ]
  }
]);

7.2 性能监控与优化

// 性能监控组件
import { useEffect, useRef } from 'react';
import { reportMetrics } from '../services/analytics';

function PerformanceMonitor({ componentName }) {
  const mountTime = useRef(Date.now());
  const renderCount = useRef(0);
  
  useEffect(() => {
    const mountDuration = Date.now() - mountTime.current;
    
    // 报告组件挂载性能
    reportMetrics('component_mount', {
      component: componentName,
      duration: mountDuration,
      timestamp: new Date().toISOString()
    });
    
    return () => {
      // 组件卸载时报告
      reportMetrics('component_unmount', {
        component: componentName,
        mountTime: mountDuration,
        renderCount: renderCount.current,
        timestamp: new Date().toISOString()
      });
    };
  }, [componentName]);
  
  useEffect(() => {
    renderCount.current += 1;
    
    // 报告渲染次数(用于检测不必要的重渲染)
    if (renderCount.current > 10) {
      console.warn(`${componentName} 组件渲染次数过多: ${renderCount.current}`);
    }
  });
  
  return null;
}

// React.memo优化
import React, { memo } from 'react';

const ExpensiveComponent = memo(function ExpensiveComponent({ data, onAction }) {
  console.log('ExpensiveComponent渲染');
  
  // 使用useMemo缓存计算结果
  const processedData = React.useMemo(() => {
    return data.map(item => ({
      ...item,
      processed: heavyComputation(item)
    }));
  }, [data]);
  
  // 使用useCallback缓存函数
  const handleClick = React.useCallback(() => {
    onAction(processedData);
  }, [onAction, processedData]);
  
  return (
    <div>
      {processedData.map(item => (
        <div key={item.id}>{item.name}</div>
      ))}
      <button onClick={handleClick}>执行操作</button>
    </div>
  );
}, (prevProps, nextProps) => {
  // 自定义比较函数
  return prevProps.data === nextProps.data && 
         prevProps.onAction === nextProps.onAction;
});

// 虚拟列表组件(处理大量数据)
import { FixedSizeList, VariableSizeList } from 'react-window';

function VirtualList({ items, itemHeight = 50 }) {
  const Row = ({ index, style }) => (
    <div style={style}>
      行 {index}: {items[index]}
    </div>
  );
  
  return (
    <FixedSizeList
      height={400}
      width={300}
      itemCount={items.length}
      itemSize={itemHeight}
    >
      {Row}
    </FixedSizeList>
  );
}

// 图片懒加载组件
function LazyImage({ src, alt, placeholder, ...props }) {
  const [isLoaded, setIsLoaded] = useState(false);
  const [isInView, setIsInView] = useState(false);
  const imgRef = useRef();
  const observerRef = useRef();
  
  useEffect(() => {
    if (!imgRef.current) return;
    
    observerRef.current = new IntersectionObserver(
      (entries) => {
        entries.forEach(entry => {
          if (entry.isIntersecting) {
            setIsInView(true);
            observerRef.current.unobserve(entry.target);
          }
        });
      },
      { rootMargin: '50px' } // 提前50px加载
    );
    
    observerRef.current.observe(imgRef.current);
    
    return () => {
      if (observerRef.current) {
        observerRef.current.disconnect();
      }
    };
  }, []);
  
  return (
    <div ref={imgRef} className="lazy-image-container">
      {!isLoaded && placeholder && (
        <div className="image-placeholder">
          {placeholder}
        </div>
      )}
      
      {isInView && (
        <img
          src={src}
          alt={alt}
          loading="lazy"
          onLoad={() => setIsLoaded(true)}
          className={`lazy-image ${isLoaded ? 'loaded' : 'loading'}`}
          {...props}
        />
      )}
    </div>
  );
}

八、部署与CI/CD

8.1 Docker配置

# Dockerfile
# 构建阶段
FROM node:18-alpine AS builder

# 安装构建依赖
RUN apk add --no-cache python3 make g++

# 设置工作目录
WORKDIR /app

# 复制包管理文件
COPY package*.json ./

# 安装依赖
RUN npm ci --only=production

# 复制源代码
COPY . .

# 构建应用
RUN npm run build

# 生产阶段
FROM nginx:alpine AS production

# 复制构建产物
COPY --from=builder /app/dist /usr/share/nginx/html

# 复制nginx配置
COPY nginx.conf /etc/nginx/nginx.conf

# 复制健康检查脚本
COPY healthcheck.sh /healthcheck.sh
RUN chmod +x /healthcheck.sh

# 暴露端口
EXPOSE 80

# 健康检查
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
  CMD ["/healthcheck.sh"]

# 启动nginx
CMD ["nginx", "-g", "daemon off;"]
# nginx.conf
worker_processes auto;

events {
    worker_connections 1024;
    use epoll;
    multi_accept on;
}

http {
    include /etc/nginx/mime.types;
    default_type application/octet-stream;

    # 日志格式
    log_format main '$remote_addr - $remote_user [$time_local] "$request" '
                    '$status $body_bytes_sent "$http_referer" '
                    '"$http_user_agent" "$http_x_forwarded_for"';

    access_log /var/log/nginx/access.log main;
    error_log /var/log/nginx/error.log warn;

    # 基础设置
    sendfile on;
    tcp_nopush on;
    tcp_nodelay on;
    keepalive_timeout 65;
    types_hash_max_size 2048;
    client_max_body_size 100m;

    # Gzip压缩
    gzip on;
    gzip_vary on;
    gzip_min_length 1024;
    gzip_proxied any;
    gzip_comp_level 6;
    gzip_types
        text/plain
        text/css
        text/xml
        text/javascript
        application/javascript
        application/xml+rss
        application/json
        application/atom+xml
        image/svg+xml;

    # 缓存设置
    open_file_cache max=1000 inactive=20s;
    open_file_cache_valid 30s;
    open_file_cache_min_uses 2;
    open_file_cache_errors on;

    # 服务器配置
    server {
        listen 80;
        server_name localhost;
        root /usr/share/nginx/html;
        index index.html;

        # 安全头部
        add_header X-Frame-Options "SAMEORIGIN" always;
        add_header X-Content-Type-Options "nosniff" always;
        add_header X-XSS-Protection "1; mode=block" always;
        add_header Referrer-Policy "strict-origin-when-cross-origin" always;
        add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:;" always;

        # 静态文件缓存
        location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
            expires 1y;
            add_header Cache-Control "public, immutable";
        }

        # HTML文件不缓存
        location ~* \.(html)$ {
            expires -1;
            add_header Cache-Control "no-store, no-cache, must-revalidate";
        }

        # 单页应用路由支持
        location / {
            try_files $uri $uri/ /index.html;
        }

        # API代理
        location /api/ {
            proxy_pass http://api-server:3000/;
            proxy_http_version 1.1;
            proxy_set_header Upgrade $http_upgrade;
            proxy_set_header Connection 'upgrade';
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto $scheme;
            proxy_cache_bypass $http_upgrade;
        }

        # 错误页面
        error_page 404 /index.html;
        error_page 500 502 503 504 /50x.html;
        location = /50x.html {
            root /usr/share/nginx/html;
        }
    }
}

8.2 GitHub Actions CI/CD

# .github/workflows/ci-cd.yml
name: CI/CD Pipeline

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

env:
  NODE_VERSION: '18'
  REGISTRY: ghcr.io
  IMAGE_NAME: ${{ github.repository }}

jobs:
  # 代码质量检查
  lint-and-test:
    runs-on: ubuntu-latest
    
    steps:
    - uses: actions/checkout@v3
    
    - name: Setup Node.js
      uses: actions/setup-node@v3
      with:
        node-version: ${{ env.NODE_VERSION }}
        cache: 'npm'
    
    - name: Install dependencies
      run: npm ci
    
    - name: ESLint检查
      run: npm run lint
    
    - name: TypeScript类型检查
      run: npm run type-check
    
    - name: 单元测试
      run: npm run test:unit -- --coverage
    
    - name: 上传测试覆盖率
      uses: codecov/codecov-action@v3
    
    - name: 构建测试
      run: npm run build

  # E2E测试
  e2e-tests:
    runs-on: ubuntu-latest
    needs: lint-and-test
    
    steps:
    - uses: actions/checkout@v3
    
    - name: Setup Node.js
      uses: actions/setup-node@v3
      with:
        node-version: ${{ env.NODE_VERSION }}
        cache: 'npm'
    
    - name: Install dependencies
      run: npm ci
    
    - name: 启动服务并运行E2E测试
      run: |
        npm run test:e2e
    
    - name: 上传E2E测试结果
      uses: actions/upload-artifact@v3
      if: always()
      with:
        name: e2e-test-results
        path: test-results/

  # 构建和推送Docker镜像
  build-and-push:
    runs-on: ubuntu-latest
    needs: [lint-and-test, e2e-tests]
    if: github.event_name == 'push' && github.ref == 'refs/heads/main'
    
    steps:
    - uses: actions/checkout@v3
    
    - name: 设置Docker构建x
      uses: docker/setup-buildx-action@v2
    
    - name: 登录到容器注册表
      uses: docker/login-action@v2
      with:
        registry: ${{ env.REGISTRY }}
        username: ${{ github.actor }}
        password: ${{ secrets.GITHUB_TOKEN }}
    
    - name: 提取元数据
      id: meta
      uses: docker/metadata-action@v4
      with:
        images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
        tags: |
          type=ref,event=branch
          type=ref,event=pr
          type=semver,pattern={{version}}
          type=semver,pattern={{major}}.{{minor}}
          type=sha,prefix={{branch}}-
    
    - name: 构建和推送Docker镜像
      uses: docker/build-push-action@v4
      with:
        context: .
        push: true
        tags: ${{ steps.meta.outputs.tags }}
        labels: ${{ steps.meta.outputs.labels }}
        cache-from: type=gha
        cache-to: type=gha,mode=max

  # 部署到生产环境
  deploy-production:
    runs-on: ubuntu-latest
    needs: build-and-push
    if: github.event_name == 'push' && github.ref == 'refs/heads/main'
    environment: production
    
    steps:
    - name: 部署到Kubernetes
      uses: azure/k8s-deploy@v1
      with:
        namespace: production
        manifests: |
          k8s/deployment.yaml
          k8s/service.yaml
          k8s/ingress.yaml
        images: |
          ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }}
    
    - name: 等待部署完成
      run: |
        kubectl rollout status deployment/my-app -n production --timeout=300s
    
    - name: 运行健康检查
      run: |
        curl -f https://api.example.com/health || exit 1
    
    - name: 发送部署通知
      uses: 8398a7/action-slack@v3
      with:
        status: ${{ job.status }}
        author_name: CI/CD Pipeline
        fields: repo,message,commit,author,action,eventName,ref,workflow
      env:
        SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}

结语

现代JavaScript开发已经远不止是编写代码,它涉及到模块化设计、工程化实践、代码质量保障、性能优化和自动化部署等多个方面。通过本文的学习,你应该能够:

  1. 理解JavaScript模块化的演进和各种模块系统
  2. 掌握现代打包工具(Webpack、Vite)的配置和使用
  3. 建立完整的代码质量保障体系(ESLint、Prettier、TypeScript)
  4. 编写全面的测试套件(单元测试、集成测试、E2E测试)
  5. 设计可维护的项目结构和组件架构
  6. 实施性能优化策略
  7. 搭建自动化CI/CD流水线

这些工程化实践将帮助你构建更健壮、更可维护、更高效的前端应用。记住,好的工程实践是项目成功的关键,持续学习和实践这些技能将使你在前端开发领域保持竞争力。