Skip to content

Webpack - 功能最强大的模块打包器

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-loadercss-loaderstyle-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 模式会启用:

development 模式会启用:

完整配置示例#

一个典型的 React 项目配置:

webpack.config.js
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#

移除未使用的代码,减小产物体积。

基本要求#

  1. 使用 ES Module(import/export
  2. 生产模式构建
  3. 包的 package.json 正确标记 sideEffects

sideEffects 配置#

告诉 Webpack 哪些文件有副作用:

package.json
{
"sideEffects": false
}
// 或者指定有副作用的文件
{
"sideEffects": ["*.css", "*.scss", "./src/polyfills.js"]
}

检查 Tree Shaking 效果#

webpack.config.js
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 是一个函数,接收源代码,返回转换后的代码:

my-loader.js
module.exports = function (source) {
// this 是 Loader Context
const options = this.getOptions()
// 同步返回
return source.replace(/console\.log\(.*?\);?/g, '')
}
// 异步 Loader
module.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#

markdown-loader.js
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
compilationcompilation 创建后SyncHook
emit输出到目录前AsyncSeriesHook
afterEmit输出后AsyncSeriesHook
done构建完成AsyncSeriesHook

Module Federation#

Webpack 5 的杀手级功能,让多个应用可以共享代码。

暴露模块(Remote)#

app1/webpack.config.js
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)#

app2/webpack.config.js
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 },
},
}),
],
}

使用远程模块:

app2/src/App.js
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 预编译(大型项目)

webpack.dll.js
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

babel.config.js
module.exports = {
presets: [
[
'@babel/preset-env',
{
useBuiltIns: 'usage',
corejs: 3,
},
],
],
}

常见问题#

1. 构建慢#

检查几个方向:

2. 热更新慢或不生效#

// 检查 HMR 配置
devServer: {
hot: true
}
// 确保有 HMR 运行时
if (module.hot) {
module.hot.accept('./App', () => {
// 重新渲染
})
}

3. 打包体积过大#

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/*"]
}
}
}

与其他工具对比#

对比项WebpackViteRollupesbuild
构建速度中等极快
配置复杂度中等
插件生态最丰富丰富丰富发展中
代码分割强大良好基础基础
HMR支持极快需插件不支持
Module Fed支持需插件不支持不支持
适用场景复杂应用现代应用简单应用、库

参考资料#