发布于

Webpack 5 完全配置指南:现代前端构建工具详解

作者

Webpack 5 完全配置指南:现代前端构建工具详解

Webpack 5 带来了许多激动人心的新特性,包括模块联邦、更好的缓存策略、Tree Shaking 优化等。本文将深入探讨 Webpack 5 的配置和最佳实践。

Webpack 5 新特性概览

1. 模块联邦(Module Federation)

// webpack.config.js - 主应用
const ModuleFederationPlugin = require('@module-federation/webpack')

module.exports = {
  mode: 'development',
  devServer: {
    port: 3000,
  },
  plugins: [
    new ModuleFederationPlugin({
      name: 'shell',
      remotes: {
        mfApp: 'mfApp@http://localhost:3001/remoteEntry.js',
        userApp: 'userApp@http://localhost:3002/remoteEntry.js',
      },
    }),
  ],
}

// webpack.config.js - 微前端应用
module.exports = {
  mode: 'development',
  devServer: {
    port: 3001,
  },
  plugins: [
    new ModuleFederationPlugin({
      name: 'mfApp',
      filename: 'remoteEntry.js',
      exposes: {
        './Button': './src/components/Button',
        './Header': './src/components/Header',
      },
      shared: {
        react: { singleton: true },
        'react-dom': { singleton: true },
      },
    }),
  ],
}

2. 持久化缓存

module.exports = {
  cache: {
    type: 'filesystem',
    buildDependencies: {
      config: [__filename],
    },
    cacheDirectory: path.resolve(__dirname, '.webpack-cache'),
    compression: 'gzip',
  },

  // 优化缓存策略
  optimization: {
    moduleIds: 'deterministic',
    chunkIds: 'deterministic',
  },
}

基础配置结构

1. 完整的配置文件

const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin')
const TerserPlugin = require('terser-webpack-plugin')

module.exports = (env, argv) => {
  const isProduction = argv.mode === 'production'

  return {
    entry: {
      main: './src/index.js',
      vendor: ['react', 'react-dom'],
    },

    output: {
      path: path.resolve(__dirname, 'dist'),
      filename: isProduction ? '[name].[contenthash:8].js' : '[name].js',
      chunkFilename: isProduction ? '[name].[contenthash:8].chunk.js' : '[name].chunk.js',
      clean: true,
      publicPath: '/',
    },

    resolve: {
      extensions: ['.js', '.jsx', '.ts', '.tsx', '.json'],
      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', { useBuiltIns: 'usage', corejs: 3 }],
                '@babel/preset-react',
                '@babel/preset-typescript',
              ],
              plugins: [
                '@babel/plugin-proposal-class-properties',
                '@babel/plugin-syntax-dynamic-import',
              ],
            },
          },
        },

        // CSS/SCSS
        {
          test: /\.(css|scss|sass)$/,
          use: [
            isProduction ? MiniCssExtractPlugin.loader : 'style-loader',
            {
              loader: 'css-loader',
              options: {
                modules: {
                  auto: true,
                  localIdentName: isProduction
                    ? '[hash:base64:8]'
                    : '[name]__[local]--[hash:base64:5]',
                },
                importLoaders: 2,
              },
            },
            'postcss-loader',
            'sass-loader',
          ],
        },

        // 图片和字体
        {
          test: /\.(png|jpe?g|gif|svg|webp)$/i,
          type: 'asset',
          parser: {
            dataUrlCondition: {
              maxSize: 8 * 1024, // 8KB
            },
          },
          generator: {
            filename: 'images/[name].[hash:8][ext]',
          },
        },

        {
          test: /\.(woff|woff2|eot|ttf|otf)$/i,
          type: 'asset/resource',
          generator: {
            filename: 'fonts/[name].[hash:8][ext]',
          },
        },
      ],
    },

    plugins: [
      new HtmlWebpackPlugin({
        template: './public/index.html',
        minify: isProduction
          ? {
              removeComments: true,
              collapseWhitespace: true,
              removeRedundantAttributes: true,
              useShortDoctype: true,
              removeEmptyAttributes: true,
              removeStyleLinkTypeAttributes: true,
              keepClosingSlash: true,
              minifyJS: true,
              minifyCSS: true,
              minifyURLs: true,
            }
          : false,
      }),

      ...(isProduction
        ? [
            new MiniCssExtractPlugin({
              filename: '[name].[contenthash:8].css',
              chunkFilename: '[name].[contenthash:8].chunk.css',
            }),
          ]
        : []),
    ],

    optimization: {
      minimize: isProduction,
      minimizer: [
        new TerserPlugin({
          terserOptions: {
            compress: {
              drop_console: isProduction,
            },
          },
        }),
        new CssMinimizerPlugin(),
      ],

      splitChunks: {
        chunks: 'all',
        cacheGroups: {
          vendor: {
            test: /[\\/]node_modules[\\/]/,
            name: 'vendors',
            chunks: 'all',
            priority: 10,
          },
          common: {
            name: 'common',
            minChunks: 2,
            chunks: 'all',
            priority: 5,
            reuseExistingChunk: true,
          },
        },
      },

      runtimeChunk: {
        name: 'runtime',
      },
    },

    devServer: {
      port: 3000,
      hot: true,
      open: true,
      historyApiFallback: true,
      compress: true,
      static: {
        directory: path.join(__dirname, 'public'),
      },
    },

    devtool: isProduction ? 'source-map' : 'eval-cheap-module-source-map',
  }
}

2. PostCSS 配置

// postcss.config.js
module.exports = {
  plugins: [
    require('autoprefixer'),
    require('postcss-preset-env')({
      stage: 3,
      features: {
        'nesting-rules': true,
        'custom-properties': true,
      },
    }),
    ...(process.env.NODE_ENV === 'production'
      ? [
          require('cssnano')({
            preset: [
              'default',
              {
                discardComments: { removeAll: true },
              },
            ],
          }),
        ]
      : []),
  ],
}

高级配置技巧

1. 环境变量配置

const webpack = require('webpack')
const dotenv = require('dotenv')

// 加载环境变量
const env = dotenv.config().parsed || {}
const envKeys = Object.keys(env).reduce((prev, next) => {
  prev[`process.env.${next}`] = JSON.stringify(env[next])
  return prev
}, {})

module.exports = {
  plugins: [
    new webpack.DefinePlugin({
      ...envKeys,
      'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV),
    }),
  ],
}

2. 多页面应用配置

const glob = require('glob')

function getEntries() {
  const entries = {}
  const pages = glob.sync('./src/pages/*/index.js')

  pages.forEach((page) => {
    const pageName = page.match(/\/pages\/(.+)\/index\.js$/)[1]
    entries[pageName] = page
  })

  return entries
}

function getHtmlPlugins() {
  const entries = getEntries()

  return Object.keys(entries).map(
    (name) =>
      new HtmlWebpackPlugin({
        template: `./src/pages/${name}/index.html`,
        filename: `${name}.html`,
        chunks: [name, 'vendor', 'common'],
        minify: process.env.NODE_ENV === 'production',
      })
  )
}

module.exports = {
  entry: getEntries(),
  plugins: [...getHtmlPlugins()],
}

3. 代码分割策略

module.exports = {
  optimization: {
    splitChunks: {
      chunks: 'all',
      minSize: 20000,
      maxSize: 244000,
      cacheGroups: {
        // 第三方库
        vendor: {
          test: /[\\/]node_modules[\\/]/,
          name: 'vendors',
          priority: 10,
          reuseExistingChunk: true,
        },

        // React 相关
        react: {
          test: /[\\/]node_modules[\\/](react|react-dom)[\\/]/,
          name: 'react',
          priority: 20,
          reuseExistingChunk: true,
        },

        // UI 库
        antd: {
          test: /[\\/]node_modules[\\/]antd[\\/]/,
          name: 'antd',
          priority: 15,
          reuseExistingChunk: true,
        },

        // 工具库
        utils: {
          test: /[\\/]node_modules[\\/](lodash|moment|dayjs)[\\/]/,
          name: 'utils',
          priority: 15,
          reuseExistingChunk: true,
        },

        // 公共代码
        common: {
          name: 'common',
          minChunks: 2,
          priority: 5,
          reuseExistingChunk: true,
        },
      },
    },
  },
}

性能优化配置

1. Tree Shaking 优化

module.exports = {
  mode: 'production',
  optimization: {
    usedExports: true,
    sideEffects: false, // 或者指定具体文件 ['*.css', '*.scss']
  },

  // 确保 babel 不转换 ES6 模块
  module: {
    rules: [
      {
        test: /\.js$/,
        use: {
          loader: 'babel-loader',
          options: {
            presets: [
              ['@babel/preset-env', { modules: false }], // 关键配置
            ],
          },
        },
      },
    ],
  },
}

2. 构建分析和优化

const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin
const SpeedMeasurePlugin = require('speed-measure-webpack-plugin')

const smp = new SpeedMeasurePlugin()

module.exports = smp.wrap({
  plugins: [
    // 只在分析时启用
    ...(process.env.ANALYZE
      ? [
          new BundleAnalyzerPlugin({
            analyzerMode: 'server',
            openAnalyzer: true,
          }),
        ]
      : []),
  ],

  // 优化构建性能
  resolve: {
    modules: [path.resolve(__dirname, 'src'), 'node_modules'],
    symlinks: false,
  },

  module: {
    rules: [
      {
        test: /\.js$/,
        include: path.resolve(__dirname, 'src'),
        use: {
          loader: 'babel-loader',
          options: {
            cacheDirectory: true,
          },
        },
      },
    ],
  },
})

3. 预加载和预获取

// 在代码中使用魔法注释
import(
  /* webpackChunkName: "lodash" */
  /* webpackPreload: true */
  'lodash'
).then(({ default: _ }) => {
  // 使用 lodash
})

// 预获取
import(
  /* webpackChunkName: "chart" */
  /* webpackPrefetch: true */
  './Chart'
).then((Chart) => {
  // 使用图表组件
})

开发体验优化

1. 热更新配置

module.exports = {
  devServer: {
    hot: true,
    liveReload: false, // 禁用 live reload,只使用 HMR
  },

  plugins: [new webpack.HotModuleReplacementPlugin()],
}

// 在应用代码中
if (module.hot) {
  module.hot.accept('./App', () => {
    // 重新渲染应用
    render()
  })
}

2. 错误处理和调试

module.exports = {
  stats: {
    colors: true,
    modules: false,
    children: false,
    chunks: false,
    chunkModules: false,
  },

  plugins: [
    new webpack.ProgressPlugin((percentage, message, ...args) => {
      console.log(`${Math.round(percentage * 100)}%`, message, ...args)
    }),
  ],

  // 开发环境的 source map
  devtool: 'eval-cheap-module-source-map',
}

部署配置

1. 生产环境优化

const CompressionPlugin = require('compression-webpack-plugin')

module.exports = {
  mode: 'production',

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

    // 清理输出目录
    new CleanWebpackPlugin(),
  ],

  optimization: {
    minimize: true,
    minimizer: [
      new TerserPlugin({
        terserOptions: {
          compress: {
            drop_console: true,
            drop_debugger: true,
          },
        },
      }),
    ],
  },
}

2. CDN 配置

module.exports = {
  output: {
    publicPath: process.env.NODE_ENV === 'production' ? 'https://cdn.example.com/assets/' : '/',
  },

  externals: {
    react: 'React',
    'react-dom': 'ReactDOM',
    lodash: '_',
  },

  plugins: [
    new HtmlWebpackPlugin({
      template: './public/index.html',
      templateParameters: {
        cdnLinks:
          process.env.NODE_ENV === 'production'
            ? [
                'https://unpkg.com/react@17/umd/react.production.min.js',
                'https://unpkg.com/react-dom@17/umd/react-dom.production.min.js',
                'https://unpkg.com/lodash@4/lodash.min.js',
              ]
            : [],
      },
    }),
  ],
}

最佳实践总结

1. 配置文件组织

// webpack/webpack.common.js
// webpack/webpack.dev.js
// webpack/webpack.prod.js

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

module.exports = merge(common, {
  mode: 'development',
  // 开发环境特定配置
})

2. 性能监控

// 添加构建时间监控
const start = Date.now()

module.exports = {
  plugins: [
    {
      apply: (compiler) => {
        compiler.hooks.done.tap('BuildTimePlugin', () => {
          console.log(`构建完成,耗时: ${Date.now() - start}ms`)
        })
      },
    },
  ],
}

总结

Webpack 5 为现代前端开发提供了强大的构建能力:

  1. 模块联邦:微前端架构的完美解决方案
  2. 持久化缓存:显著提升构建性能
  3. 更好的 Tree Shaking:减少包体积
  4. 资源模块:内置的资源处理能力
  5. 改进的代码分割:更智能的分包策略

掌握这些配置技巧,你就能构建出高性能、可维护的现代前端应用!


Webpack 5 是现代前端工程化的核心工具,值得深入学习和实践。