Skip to content

前言 ​

在现代前端开发中,Webpack 已经成为了不可或缺的构建工具。然而,随着项目规模的扩大,Webpack 的配置往往会变得越来越复杂。本文将深入探讨如何通过模块化的方式组织 Webpack 配置,并结合实际项目经验,详细介绍各个配置项的原理、使用场景和优化策略。

1. 模块化配置设计 ​

1.1 为什么要模块化配置? ​

在实际项目中,我们通常需要针对不同的环境(开发、测试、生产)维护不同的 Webpack 配置。如果将所有配置都写在一个文件中,不仅会导致文件臃肿难以维护,还会增加团队协作的成本。模块化配置带来的好处包括:

  • 🎯 关注点分离:每个配置文件专注于特定的功能场景
  • ♻️ 代码复用:共用配置集中管理,避免重复代码
  • 🌍 环境隔离:不同环境的配置清晰分开
  • 🛠️ 可维护性:配置文件结构清晰,易于理解和修改
  • 👥 团队协作:便于团队成员理解和维护各自负责的配置部分

1.2 配置文件结构 ​

推荐的项目配置文件结构如下:

webpack/
├── webpack.common.js  # 公共配置
├── webpack.dev.js     # 开发环境配置
├── webpack.prod.js    # 生产环境配置
└── paths.js          # 路径配置

2. 完整配置实现 ​

2.1 路径配置(paths.js) ​

首先,我们创建一个统一的路径配置文件:

js
const path = require('path');
const fs = require('fs');

// 获取项目根目录
const appDirectory = fs.realpathSync(process.cwd());
const resolveApp = relativePath => path.resolve(appDirectory, relativePath);

module.exports = {
    appPath: resolveApp('.'),
    appBuild: resolveApp('dist'),
    appPublic: resolveApp('public'),
    appHtml: resolveApp('public/index.html'),
    appIndexJs: resolveApp('src/index.js'),
    appPackageJson: resolveApp('package.json'),
    appSrc: resolveApp('src'),
    appTsConfig: resolveApp('tsconfig.json'),
    appNodeModules: resolveApp('node_modules'),
};

2.2 公共配置(webpack.common.js) ​

js
const paths = require('./paths');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const CopyWebpackPlugin = require('copy-webpack-plugin');
const WebpackBar = require('webpackbar');

module.exports = {
    // 入口配置
    entry: {
        app: paths.appIndexJs,
    },
    
    // 输出配置
    output: {
        path: paths.appBuild,
        filename: 'js/[name].[contenthash:8].js',
        chunkFilename: 'js/[name].[contenthash:8].chunk.js',
        publicPath: '/',
        clean: true,
    },

    // 模块解析配置
    resolve: {
        extensions: ['.js', '.jsx', '.ts', '.tsx', '.json'],
        alias: {
            '@': paths.appSrc,
            'components': `${paths.appSrc}/components`,
            'utils': `${paths.appSrc}/utils`,
        },
        modules: ['node_modules', paths.appNodeModules],
    },

    // 模块规则
    module: {
        rules: [
            // JavaScript/TypeScript 处理
            {
                test: /\.(js|jsx|ts|tsx)$/,
                include: paths.appSrc,
                use: {
                    loader: 'babel-loader',
                    options: {
                        cacheDirectory: true,
                    },
                },
            },
            // 图片处理
            {
                test: /\.(png|jpg|jpeg|gif|svg)$/,
                type: 'asset',
                parser: {
                    dataUrlCondition: {
                        maxSize: 10 * 1024, // 10kb
                    },
                },
                generator: {
                    filename: 'images/[name].[hash:8][ext]',
                },
            },
            // 字体处理
            {
                test: /\.(woff|woff2|eot|ttf|otf)$/,
                type: 'asset/resource',
                generator: {
                    filename: 'fonts/[name].[hash:8][ext]',
                },
            },
        ],
    },

    // 插件配置
    plugins: [
        // 清理输出目录
        new CleanWebpackPlugin(),
        
        // HTML 模板
        new HtmlWebpackPlugin({
            template: paths.appHtml,
            inject: true,
            minify: {
                removeComments: true,
                collapseWhitespace: true,
                removeRedundantAttributes: true,
                useShortDoctype: true,
                removeEmptyAttributes: true,
                removeStyleLinkTypeAttributes: true,
                keepClosingSlash: true,
                minifyJS: true,
                minifyCSS: true,
                minifyURLs: true,
            },
        }),

        // 复制公共资源
        new CopyWebpackPlugin({
            patterns: [
                {
                    from: paths.appPublic,
                    to: paths.appBuild,
                    globOptions: {
                        ignore: ['**/index.html'],
                    },
                },
            ],
        }),

        // 构建进度条
        new WebpackBar({
            name: 'Building',
            color: '#00b3ff',
        }),
    ],
};

2.3 开发环境配置(webpack.dev.js) ​

js
const { merge } = require('webpack-merge');
const common = require('./webpack.common.js');
const paths = require('./paths');

module.exports = merge(common, {
    mode: 'development',
    devtool: 'eval-source-map',

    // 开发服务器配置
    devServer: {
        host: 'localhost',
        port: 3000,
        hot: true,
        open: true,
        historyApiFallback: true,
        compress: true,
        client: {
            overlay: {
                errors: true,
                warnings: false,
            },
            progress: true,
        },
        proxy: {
            '/api': {
                target: 'http://localhost:8080',
                changeOrigin: true,
                pathRewrite: { '^/api': '' },
            },
        },
    },

    // 模块规则
    module: {
        rules: [
            // CSS 处理
            {
                test: /\.(css|scss|sass)$/,
                use: [
                    'style-loader',
                    {
                        loader: 'css-loader',
                        options: {
                            sourceMap: true,
                            importLoaders: 2,
                        },
                    },
                    {
                        loader: 'postcss-loader',
                        options: {
                            sourceMap: true,
                        },
                    },
                    {
                        loader: 'sass-loader',
                        options: {
                            sourceMap: true,
                        },
                    },
                ],
            },
        ],
    },

    optimization: {
        removeAvailableModules: false,
        removeEmptyChunks: false,
        splitChunks: false,
    },
});

2.4 生产环境配置(webpack.prod.js) ​

js
const { merge } = require('webpack-merge');
const common = require('./webpack.common.js');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin');
const TerserPlugin = require('terser-webpack-plugin');
const CompressionPlugin = require('compression-webpack-plugin');
const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer');

module.exports = merge(common, {
    mode: 'production',
    devtool: false,

    // 模块规则
    module: {
        rules: [
            // CSS 处理
            {
                test: /\.(css|scss|sass)$/,
                use: [
                    MiniCssExtractPlugin.loader,
                    {
                        loader: 'css-loader',
                        options: {
                            importLoaders: 2,
                            modules: {
                                localIdentName: '[name]_[local]_[hash:base64:5]',
                            },
                        },
                    },
                    'postcss-loader',
                    'sass-loader',
                ],
            },
        ],
    },

    // 优化配置
    optimization: {
        minimize: true,
        minimizer: [
            // JS 压缩
            new TerserPlugin({
                terserOptions: {
                    compress: {
                        drop_console: true,
                        drop_debugger: true,
                    },
                },
            }),
            // CSS 压缩
            new CssMinimizerPlugin(),
        ],
        // 代码分割
        splitChunks: {
            chunks: 'all',
            minSize: 20000,
            minChunks: 1,
            maxAsyncRequests: 30,
            maxInitialRequests: 30,
            cacheGroups: {
                vendors: {
                    test: /[\\/]node_modules[\\/]/,
                    name: 'vendors',
                    priority: 10,
                    chunks: 'initial',
                },
                common: {
                    name: 'common',
                    minChunks: 2,
                    priority: -10,
                    chunks: 'initial',
                    reuseExistingChunk: true,
                },
            },
        },
        // 运行时代码分离
        runtimeChunk: 'single',
    },

    // 插件配置
    plugins: [
        // 提取 CSS
        new MiniCssExtractPlugin({
            filename: 'css/[name].[contenthash:8].css',
            chunkFilename: 'css/[name].[contenthash:8].chunk.css',
        }),

        // Gzip 压缩
        new CompressionPlugin({
            algorithm: 'gzip',
            test: /\.(js|css|html|svg)$/,
            threshold: 10240,
            minRatio: 0.8,
        }),

        // 包分析
        process.env.ANALYZE && new BundleAnalyzerPlugin(),
    ].filter(Boolean),

    performance: {
        hints: 'warning',
        maxEntrypointSize: 512000,
        maxAssetSize: 512000,
    },
});

3. 使用方法 ​

在 package.json 中添加以下脚本:

json
{
  "scripts": {
    "start": "webpack serve --config webpack/webpack.dev.js",
    "build": "webpack --config webpack/webpack.prod.js",
    "analyze": "ANALYZE=true webpack --config webpack/webpack.prod.js"
  }
}

4. 性能优化要点 ​

  1. 构建性能优化:

    • 使用 cache-loader 或 babel-loader 的 cacheDirectory 选项
    • 使用 thread-loader 进行多线程构建
    • 合理使用 source-map
  2. 产出代码优化:

    • 代码分割(Code Splitting)
    • Tree Shaking
    • 压缩代码(JS/CSS/HTML)
    • 图片优化
    • Gzip 压缩
  3. 缓存优化:

    • 使用 contenthash
    • 提取运行时代码
    • 合理的 splitChunks 配置

5. Create React App Webpack 配置方案 ​

5.1 配置方案对比 ​

在 Create React App (CRA) 项目中,我们有多种方式来自定义 Webpack 配置:

方案优点缺点适用场景实现原理
eject完全控制配置,可深度定制失去 CRA 升级能力,不可逆需要深度定制的大型项目将 CRA 的配置文件暴露到项目中
react-app-rewired无需 eject,配置灵活可能存在兼容性问题中小型项目通过 override 方式修改配置
craco配置更结构化,插件系统完善学习成本略高中大型项目提供统一的配置层和插件系统
customize-craAPI 友好,配置简单功能相对较少简单定制需求基于 react-app-rewired 的封装

5.2 react-app-rewired 配置示例 ​

5.2.1 基础配置 ​

js
// config-overrides.js
const path = require('path');
const {
  override,
  addWebpackAlias,
  addBabelPlugins,
  addDecoratorsLegacy,
  fixBabelImports,
  addLessLoader,
} = require('customize-cra');

module.exports = override(
  // 添加装饰器支持
  addDecoratorsLegacy(),
  
  // 路径别名
  addWebpackAlias({
    '@': path.resolve(__dirname, 'src'),
    'components': path.resolve(__dirname, 'src/components'),
  }),
  
  // 按需加载
  fixBabelImports('import', {
    libraryName: 'antd',
    libraryDirectory: 'es',
    style: true,
  }),
  
  // Less 支持
  addLessLoader({
    lessOptions: {
      javascriptEnabled: true,
      modifyVars: { '@primary-color': '#1DA57A' },
    },
  }),
  
  // Babel 插件
  ...addBabelPlugins(
    '@babel/plugin-proposal-optional-chaining',
    '@babel/plugin-proposal-nullish-coalescing-operator'
  )
);

5.2.2 高级配置示例 ​

js
// config-overrides.js
const {
  override,
  addWebpackPlugin,
  setWebpackOptimizationSplitChunks,
} = require('customize-cra');
const webpack = require('webpack');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer');

module.exports = override(
  // 配置 webpack 优化
  (config) => {
    // 生产环境特定配置
    if (process.env.NODE_ENV === 'production') {
      // 优化分包策略
      setWebpackOptimizationSplitChunks(config)({
        chunks: 'all',
        cacheGroups: {
          vendors: {
            name: 'vendors',
            test: /[\\/]node_modules[\\/]/,
            priority: 10,
            chunks: 'initial',
          },
          commons: {
            name: 'commons',
            minChunks: 2,
            priority: -10,
            chunks: 'initial',
            reuseExistingChunk: true,
          },
        },
      });

      // 添加 Gzip 压缩
      addWebpackPlugin(new CompressionPlugin())(config);
      
      // 添加包分析工具
      if (process.env.ANALYZE) {
        addWebpackPlugin(new BundleAnalyzerPlugin())(config);
      }
    }

    // 配置缓存
    config.cache = {
      type: 'filesystem',
      buildDependencies: {
        config: [__filename],
      },
    };

    return config;
  }
);

5.3 CRACO 配置示例 ​

CRACO (Create React App Configuration Override) 提供了更结构化的配置方式:

js
// craco.config.js
const path = require('path');
const CracoLessPlugin = require('craco-less');
const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer');

module.exports = {
  // webpack 配置
  webpack: {
    alias: {
      '@': path.resolve(__dirname, 'src'),
    },
    plugins: [
      // 条件添加插件
      process.env.ANALYZE && new BundleAnalyzerPlugin(),
    ].filter(Boolean),
    configure: (webpackConfig, { env, paths }) => {
      // 修改 webpack 配置
      if (env === 'production') {
        // 优化分包配置
        webpackConfig.optimization.splitChunks = {
          chunks: 'all',
          cacheGroups: {
            vendors: {
              name: 'vendors',
              test: /[\\/]node_modules[\\/]/,
              priority: 10,
            },
          },
        };
      }
      return webpackConfig;
    },
  },
  
  // 插件配置
  plugins: [
    {
      plugin: CracoLessPlugin,
      options: {
        lessLoaderOptions: {
          lessOptions: {
            modifyVars: { '@primary-color': '#1DA57A' },
            javascriptEnabled: true,
          },
        },
      },
    },
  ],
  
  // 开发服务器配置
  devServer: {
    proxy: {
      '/api': {
        target: 'http://localhost:3000',
        changeOrigin: true,
      },
    },
  },
  
  // Jest 配置
  jest: {
    configure: {
      moduleNameMapper: {
        '^@/(.*)$': '<rootDir>/src/$1',
      },
    },
  },
};

5.4 配置原理解析 ​

  1. 配置覆盖机制:

    • react-app-rewired 通过重写 react-scripts 的配置加载过程实现配置修改
    • CRACO 提供了一个配置层,在 webpack 配置生成过程中进行拦截和修改
  2. 运行时配置注入:

js
// react-app-rewired 的实现原理
const rewire = require('react-app-rewired');
const config = rewire.loadConfig(process.cwd());
const webpackConfig = rewire.webpack(config);

// CRACO 的实现原理
const { createWebpackDevConfig } = require('@craco/craco');
const cracoConfig = require('./craco.config.js');
const webpackConfig = createWebpackDevConfig(cracoConfig);
  1. 插件系统:
js
// CRACO 插件示例
module.exports = {
  overrideWebpackConfig: ({ webpackConfig, cracoConfig, pluginOptions }) => {
    // 修改 webpack 配置
    return webpackConfig;
  },
  overrideDevServerConfig: ({ devServerConfig, cracoConfig, pluginOptions }) => {
    // 修改 devServer 配置
    return devServerConfig;
  },
};

5.5 最佳实践建议 ​

  1. 选择合适的配置方案:

    • 简单定制:使用 react-app-rewired + customize-cra
    • 复杂项目:使用 CRACO
    • 完全控制:执行 eject
  2. 配置组织:

project/
├── craco.config.js          # 主配置文件
├── config/
│   ├── webpack.common.js    # 通用配置
│   ├── webpack.dev.js      # 开发环境配置
│   └── webpack.prod.js     # 生产环境配置
└── src/
  1. 性能优化配置:
js
module.exports = {
  webpack: {
    configure: (webpackConfig) => {
      // 启用持久化缓存
      webpackConfig.cache = {
        type: 'filesystem',
        buildDependencies: {
          config: [__filename],
        },
      };
      
      // 优化分包策略
      webpackConfig.optimization = {
        splitChunks: {
          chunks: 'all',
          cacheGroups: {
            vendors: {
              test: /[\\/]node_modules[\\/]/,
              priority: -10,
              reuseExistingChunk: true,
            },
          },
        },
      };
      
      return webpackConfig;
    },
  },
};

总结 ​

通过本文的配置实践,我们实现了一个完整的、可扩展的 Webpack 配置体系。这套配置不仅适用于中小型项目,通过适当的调整和扩展,也完全能够满足大型项目的需求。在实际使用中,可以根据项目的具体需求,选择性地添加或移除相关配置。

参考资料 ​