Webpack 是前端工程化的基石。虽然 Vite 这些新工具很火,但 Webpack 依然是最成熟、生态最丰富的打包工具。很多大型项目、企业级应用还是用 Webpack。
它能做的事情太多了:打包 JS/CSS/图片、代码分割、Tree Shaking、HMR、Module Federation……几乎你能想到的构建需求,Webpack 都能搞定。代价就是配置复杂,上手门槛高。
这篇文章从基础概念讲起,一步步带你搞懂 Webpack 5 的核心配置和优化技巧。
核心概念#
Entry(入口)#
告诉 Webpack 从哪个文件开始构建依赖图:
// 单入口module.exports = { entry: './src/index.js',}
// 多入口module.exports = { entry: { main: './src/index.js', admin: './src/admin.js', },}
// 带依赖的入口module.exports = { entry: { main: { import: './src/index.js', dependOn: 'shared', }, admin: { import: './src/admin.js', dependOn: 'shared', }, shared: ['lodash', 'dayjs'], },}Output(输出)#
配置打包结果输出到哪里:
const path = require('path')
module.exports = { output: { // 输出目录(绝对路径) path: path.resolve(__dirname, 'dist'),
// 输出文件名 filename: '[name].[contenthash:8].js',
// 非入口 chunk 的文件名 chunkFilename: '[name].[contenthash:8].chunk.js',
// 静态资源的公共路径 publicPath: '/',
// 清理输出目录 clean: true,
// 静态资源文件名 assetModuleFilename: 'assets/[name].[hash:8][ext]', },}文件名占位符:
| 占位符 | 说明 |
|---|---|
| [name] | 模块名称 |
| [id] | 模块 ID |
| [hash] | 模块标识符的 hash |
| [chunkhash] | chunk 内容的 hash |
| [contenthash] | 文件内容的 hash(最常用) |
| [ext] | 资源扩展名 |
Loader(加载器)#
Webpack 只认识 JavaScript 和 JSON。其他类型的文件需要用 Loader 转换:
module.exports = { module: { rules: [ // 处理 JavaScript/TypeScript { test: /\.(js|jsx|ts|tsx)$/, exclude: /node_modules/, use: 'babel-loader', },
// 处理 CSS { test: /\.css$/, use: ['style-loader', 'css-loader'], },
// 处理 Sass { test: /\.scss$/, use: ['style-loader', 'css-loader', 'sass-loader'], },
// 处理图片 { test: /\.(png|jpg|gif|svg)$/, type: 'asset', parser: { dataUrlCondition: { maxSize: 8 * 1024, // 8KB 以下转 base64 }, }, },
// 处理字体 { test: /\.(woff|woff2|eot|ttf|otf)$/, type: 'asset/resource', }, ], },}Loader 执行顺序是从后往前,所以 CSS 处理是:sass-loader → css-loader → style-loader。
Plugin(插件)#
Loader 处理特定类型的文件,Plugin 可以介入构建过程的各个阶段:
const HtmlWebpackPlugin = require('html-webpack-plugin')const MiniCssExtractPlugin = require('mini-css-extract-plugin')const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer')
module.exports = { plugins: [ // 生成 HTML 文件 new HtmlWebpackPlugin({ template: './public/index.html', filename: 'index.html', minify: { collapseWhitespace: true, removeComments: true, }, }),
// 提取 CSS 到单独文件 new MiniCssExtractPlugin({ filename: 'css/[name].[contenthash:8].css', }),
// 构建分析(开发时用) process.env.ANALYZE && new BundleAnalyzerPlugin(), ].filter(Boolean),}Mode(模式)#
告诉 Webpack 使用哪种模式的内置优化:
module.exports = { mode: 'production', // 或 'development' 或 'none'}production 模式会启用:
- 代码压缩(TerserPlugin)
- Tree Shaking
- 作用域提升(Scope Hoisting)
- 模块 ID 优化
development 模式会启用:
- 更详细的错误信息
- 模块热替换(HMR)
- 更快的构建速度
完整配置示例#
一个典型的 React 项目配置:
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')
const isDev = process.env.NODE_ENV !== 'production'
module.exports = { mode: isDev ? 'development' : 'production',
entry: './src/index.tsx',
output: { path: path.resolve(__dirname, 'dist'), filename: isDev ? '[name].js' : '[name].[contenthash:8].js', chunkFilename: isDev ? '[name].chunk.js' : '[name].[contenthash:8].chunk.js', publicPath: '/', clean: true, },
resolve: { extensions: ['.tsx', '.ts', '.jsx', '.js', '.json'], alias: { '@': path.resolve(__dirname, 'src'), }, },
module: { rules: [ // TypeScript/JavaScript { test: /\.(ts|tsx|js|jsx)$/, exclude: /node_modules/, use: { loader: 'babel-loader', options: { presets: [ ['@babel/preset-env', { modules: false }], ['@babel/preset-react', { runtime: 'automatic' }], '@babel/preset-typescript', ], plugins: [isDev && require.resolve('react-refresh/babel')].filter( Boolean ), }, }, },
// CSS { test: /\.css$/, use: [ isDev ? 'style-loader' : MiniCssExtractPlugin.loader, { loader: 'css-loader', options: { modules: { auto: /\.module\.css$/, localIdentName: isDev ? '[name]__[local]--[hash:base64:5]' : '[hash:base64:8]', }, }, }, 'postcss-loader', ], },
// Sass { test: /\.scss$/, use: [ isDev ? 'style-loader' : MiniCssExtractPlugin.loader, { loader: 'css-loader', options: { modules: { auto: /\.module\.scss$/, localIdentName: isDev ? '[name]__[local]--[hash:base64:5]' : '[hash:base64:8]', }, }, }, 'postcss-loader', 'sass-loader', ], },
// 图片 { test: /\.(png|jpg|jpeg|gif|webp)$/i, type: 'asset', parser: { dataUrlCondition: { maxSize: 8 * 1024, }, }, generator: { filename: 'images/[name].[hash:8][ext]', }, },
// SVG { test: /\.svg$/, use: ['@svgr/webpack', 'url-loader'], },
// 字体 { test: /\.(woff|woff2|eot|ttf|otf)$/i, type: 'asset/resource', generator: { filename: 'fonts/[name].[hash:8][ext]', }, }, ], },
plugins: [ new HtmlWebpackPlugin({ template: './public/index.html', favicon: './public/favicon.ico', }),
!isDev && new MiniCssExtractPlugin({ filename: 'css/[name].[contenthash:8].css', chunkFilename: 'css/[name].[contenthash:8].chunk.css', }),
isDev && new (require('@pmmmwh/react-refresh-webpack-plugin'))(), ].filter(Boolean),
optimization: { minimize: !isDev, minimizer: [ new TerserPlugin({ terserOptions: { compress: { drop_console: true, }, }, }), new CssMinimizerPlugin(), ], splitChunks: { chunks: 'all', cacheGroups: { vendor: { test: /[\\/]node_modules[\\/]/, name: 'vendors', chunks: 'all', }, }, }, runtimeChunk: 'single', },
devServer: { port: 3000, hot: true, open: true, historyApiFallback: true, proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true, }, }, },
devtool: isDev ? 'eval-cheap-module-source-map' : 'source-map',
cache: { type: 'filesystem', buildDependencies: { config: [__filename], }, },}对应的 package.json 脚本:
{ "scripts": { "dev": "webpack serve --mode development", "build": "webpack --mode production", "analyze": "ANALYZE=true webpack --mode production" }}代码分割#
代码分割是 Webpack 最强大的功能之一,可以显著提升应用加载速度。
入口分割#
多入口自动分割:
module.exports = { entry: { main: './src/index.js', admin: './src/admin.js', },}动态导入#
使用 import() 语法触发分割:
// 路由懒加载const routes = [ { path: '/dashboard', component: React.lazy(() => import('./pages/Dashboard')), }, { path: '/settings', component: React.lazy(() => import('./pages/Settings')), },]
// 条件加载button.addEventListener('click', async () => { const { Chart } = await import('./components/Chart') new Chart()})给 chunk 命名:
import(/* webpackChunkName: "chart" */ './components/Chart')SplitChunks#
自动抽取公共代码:
module.exports = { optimization: { splitChunks: { chunks: 'all', // all | async | initial minSize: 20000, // 最小体积 minRemainingSize: 0, minChunks: 1, // 被引用次数 maxAsyncRequests: 30, maxInitialRequests: 30, enforceSizeThreshold: 50000, cacheGroups: { // React 相关库 react: { test: /[\\/]node_modules[\\/](react|react-dom|react-router)[\\/]/, name: 'react-vendor', chunks: 'all', priority: 20, }, // UI 库 antd: { test: /[\\/]node_modules[\\/]antd[\\/]/, name: 'antd', chunks: 'all', priority: 15, }, // 其他第三方库 vendors: { test: /[\\/]node_modules[\\/]/, name: 'vendors', chunks: 'all', priority: 10, }, // 公共代码 common: { minChunks: 2, name: 'common', chunks: 'all', priority: 5, reuseExistingChunk: true, }, }, }, // 运行时代码单独提取 runtimeChunk: 'single', },}Tree Shaking#
移除未使用的代码,减小产物体积。
基本要求#
- 使用 ES Module(
import/export) - 生产模式构建
- 包的
package.json正确标记sideEffects
sideEffects 配置#
告诉 Webpack 哪些文件有副作用:
{ "sideEffects": false}
// 或者指定有副作用的文件{ "sideEffects": ["*.css", "*.scss", "./src/polyfills.js"]}检查 Tree Shaking 效果#
module.exports = { optimization: { usedExports: true, // 标记未使用的导出 minimize: true, // 删除未使用的代码 },}构建后查看产物,确认未使用的代码被删除。
缓存优化#
Webpack 5 内置了持久化缓存,二次构建速度大幅提升。
文件系统缓存#
module.exports = { cache: { type: 'filesystem', version: '1.0.0', // 版本变化时失效缓存 cacheDirectory: path.resolve(__dirname, 'node_modules/.cache/webpack'), buildDependencies: { // 配置文件变化时失效缓存 config: [__filename], }, },}内容 hash#
用 [contenthash] 确保文件内容不变时,文件名也不变:
module.exports = { output: { filename: '[name].[contenthash:8].js', },}模块 ID 稳定#
避免模块 ID 变化导致 hash 变化:
module.exports = { optimization: { moduleIds: 'deterministic', // 基于模块路径生成稳定 ID chunkIds: 'deterministic', },}Loader 开发#
Loader 是一个函数,接收源代码,返回转换后的代码:
module.exports = function (source) { // this 是 Loader Context const options = this.getOptions()
// 同步返回 return source.replace(/console\.log\(.*?\);?/g, '')}
// 异步 Loadermodule.exports = function (source) { const callback = this.async()
someAsyncOperation(source).then((result) => { callback(null, result) })}使用自定义 Loader:
module.exports = { module: { rules: [ { test: /\.js$/, use: [path.resolve(__dirname, 'my-loader.js')], }, ], },}实战:Markdown 转 HTML#
const marked = require('marked')
module.exports = function (source) { const html = marked.parse(source)
return `export default ${JSON.stringify(html)}`}Plugin 开发#
Plugin 是一个类,包含 apply 方法:
class MyPlugin { constructor(options) { this.options = options }
apply(compiler) { // 编译开始 compiler.hooks.compile.tap('MyPlugin', (params) => { console.log('编译开始') })
// 输出 asset 到目录之前 compiler.hooks.emit.tapAsync('MyPlugin', (compilation, callback) => { // 添加文件 compilation.assets['version.txt'] = { source: () => 'v1.0.0', size: () => 6, } callback() })
// 构建完成 compiler.hooks.done.tap('MyPlugin', (stats) => { console.log('构建完成') }) }}
module.exports = MyPlugin常用的 Compiler Hooks:
| Hook | 说明 | 类型 |
|---|---|---|
| beforeRun | 开始执行前 | AsyncSeriesHook |
| run | 开始编译 | AsyncSeriesHook |
| compile | 创建 compilation 前 | SyncHook |
| compilation | compilation 创建后 | SyncHook |
| emit | 输出到目录前 | AsyncSeriesHook |
| afterEmit | 输出后 | AsyncSeriesHook |
| done | 构建完成 | AsyncSeriesHook |
Module Federation#
Webpack 5 的杀手级功能,让多个应用可以共享代码。
暴露模块(Remote)#
const { ModuleFederationPlugin } = require('webpack').container
module.exports = { plugins: [ new ModuleFederationPlugin({ name: 'app1', filename: 'remoteEntry.js', exposes: { './Button': './src/components/Button', './utils': './src/utils', }, shared: { 'react': { singleton: true }, 'react-dom': { singleton: true }, }, }), ],}消费模块(Host)#
const { ModuleFederationPlugin } = require('webpack').container
module.exports = { plugins: [ new ModuleFederationPlugin({ name: 'app2', remotes: { app1: 'app1@http://localhost:3001/remoteEntry.js', }, shared: { 'react': { singleton: true }, 'react-dom': { singleton: true }, }, }), ],}使用远程模块:
import React, { Suspense } from 'react'
const RemoteButton = React.lazy(() => import('app1/Button'))
function App() { return ( <Suspense fallback="Loading..."> <RemoteButton /> </Suspense> )}性能优化#
构建速度优化#
1. 缩小处理范围
module.exports = { module: { rules: [ { test: /\.js$/, include: path.resolve(__dirname, 'src'), exclude: /node_modules/, use: 'babel-loader', }, ], },}2. 使用缓存
module.exports = { cache: { type: 'filesystem', }, module: { rules: [ { test: /\.js$/, use: { loader: 'babel-loader', options: { cacheDirectory: true, }, }, }, ], },}3. 多线程处理
const TerserPlugin = require('terser-webpack-plugin')
module.exports = { optimization: { minimizer: [ new TerserPlugin({ parallel: true, }), ], },}4. DLL 预编译(大型项目)
const webpack = require('webpack')
module.exports = { entry: { vendor: ['react', 'react-dom', 'lodash'], }, output: { path: path.resolve(__dirname, 'dll'), filename: '[name].dll.js', library: '[name]_dll', }, plugins: [ new webpack.DllPlugin({ name: '[name]_dll', path: path.resolve(__dirname, 'dll/[name].manifest.json'), }), ],}产物体积优化#
1. 分析工具
const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer')
module.exports = { plugins: [ new BundleAnalyzerPlugin({ analyzerMode: 'static', openAnalyzer: false, }), ],}2. 压缩配置
const TerserPlugin = require('terser-webpack-plugin')const CssMinimizerPlugin = require('css-minimizer-webpack-plugin')
module.exports = { optimization: { minimizer: [ new TerserPlugin({ terserOptions: { compress: { drop_console: true, drop_debugger: true, }, }, }), new CssMinimizerPlugin(), ], },}3. 按需加载 polyfill
module.exports = { presets: [ [ '@babel/preset-env', { useBuiltIns: 'usage', corejs: 3, }, ], ],}常见问题#
1. 构建慢#
检查几个方向:
- 开启持久化缓存
- 缩小 Loader 处理范围
- 使用更快的工具(swc-loader 替代 babel-loader)
- 检查是否有大量小文件
2. 热更新慢或不生效#
// 检查 HMR 配置devServer: { hot: true}
// 确保有 HMR 运行时if (module.hot) { module.hot.accept('./App', () => { // 重新渲染 })}3. 打包体积过大#
- 使用 BundleAnalyzer 分析
- 检查是否有重复依赖
- 按需加载大型库
- 检查 Tree Shaking 是否生效
4. 样式隔离问题#
使用 CSS Modules:
{ loader: 'css-loader', options: { modules: { auto: /\.module\.\w+$/, localIdentName: '[name]__[local]--[hash:base64:5]' } }}5. 路径别名 TypeScript 不识别#
同时配置 tsconfig.json:
{ "compilerOptions": { "baseUrl": ".", "paths": { "@/*": ["src/*"] } }}与其他工具对比#
| 对比项 | Webpack | Vite | Rollup | esbuild |
|---|---|---|---|---|
| 构建速度 | 慢 | 快 | 中等 | 极快 |
| 配置复杂度 | 高 | 低 | 中等 | 低 |
| 插件生态 | 最丰富 | 丰富 | 丰富 | 发展中 |
| 代码分割 | 强大 | 良好 | 基础 | 基础 |
| HMR | 支持 | 极快 | 需插件 | 不支持 |
| Module Fed | 支持 | 需插件 | 不支持 | 不支持 |
| 适用场景 | 复杂应用 | 现代应用 | 库 | 简单应用、库 |