外观
Webpack 系统学习文档
适合人群:想真正理解“前端构建工具到底在做什么”的学习者。
学习目标:从最小配置开始,逐步掌握
entry / output / loader / plugin / module / chunk / tree shaking / code splitting / cache。核对时间:2026-06-24。Webpack 生态版本变化较快,具体安装版本请以官方文档和项目实际依赖为准。2026-07-04 已补充常用 loader/plugin 源码阅读导向,详见同目录
AST与Webpack自定义Loader和Plugin系统学习文档.md第 16 章。2026-07-09 已按 Webpack 5.108.4、Tapable 2.3.3 补充 Compiler、Compilation、loader-runner、enhanced-resolve、生命周期与源码级工作原理。2026-09-01 已按 Webpack 5.110.x 官方文档补充 Tapable 钩子类型、模块解析决策树、Tree Shaking 双层机制与持久化缓存细节。
目录
- Webpack 是什么
- 为什么现在还要学 Webpack
- 学习前置知识
- 快速开始
- 最小可运行配置
- 核心概念:entry、output、module、loader、plugin
- Loader 深入理解
- Plugin 深入理解
- 开发环境配置
- 处理 CSS、图片、字体与静态资源
- 在 Webpack 中使用 TypeScript
- 生产环境优化
- 代码分割与缓存策略
- Tree Shaking 与副作用
- 配置拆分与工程化组织方式
- 常见进阶主题
- Webpack 与 Vite 如何选择
- 初学者常见误区
- 推荐学习顺序
- 高级阶段:配置项全注释实战
- 核心机制深度补全
- 学习资源
1. Webpack 是什么
Webpack 是一个模块打包工具。
它最核心的事情是:
- 从入口文件开始。
- 分析项目里的模块依赖关系。
- 根据规则处理 JavaScript、CSS、图片、字体等资源。
- 生成浏览器可运行的构建产物。
这里的模块不只是 JavaScript。
在 Webpack 的视角里,下面这些都可以被纳入模块系统:
- JavaScript。
- TypeScript。
- CSS。
- Sass / Less。
- 图片。
- 字体。
- JSON。
- WebAssembly。
这也是为什么 Webpack 经常被叫作 bundler。它会把项目中互相关联的资源组织成最终可发布的文件。
1.1 一个简单类比
你可以把 Webpack 理解成一个“前端构建工厂”:
entry告诉工厂从哪里开始。loader告诉工厂不同材料怎么加工。plugin告诉工厂在整个生产流程中做哪些额外动作。output告诉工厂成品放在哪里、叫什么名字。optimization告诉工厂如何压缩、拆分、缓存。
2. 为什么现在还要学 Webpack
很多人会问:
现在很多新项目都用 Vite,为什么还要学 Webpack?
原因有三个。
2.1 大量存量项目仍然依赖 Webpack
很多公司项目,尤其是中大型后台、老业务、微前端、复杂定制项目,仍然基于 Webpack。
你进入真实团队后,很可能会遇到:
- Vue CLI 项目。
- React CRA 老项目。
- 自研脚手架。
- 微前端主应用或子应用。
- 复杂多页面构建系统。
这些项目背后经常都有 Webpack。
2.2 Webpack 能训练工程化底层理解
学 Vite 更像先把现代项目跑起来。
学 Webpack 更像拆开构建工具看内部结构。
Webpack 能帮助你真正理解:
- 什么是依赖图。
- 为什么 CSS 也能被 import。
- loader 为什么有执行顺序。
- plugin 为什么能改变构建结果。
- chunk 和 bundle 有什么关系。
- tree shaking 为什么有时不生效。
- 长缓存为什么依赖 contenthash。
这些理解反过来会帮助你更好地使用 Vite、Rollup、Rspack、Turbopack 等工具。
2.3 复杂定制能力依然强
Webpack 的生态非常成熟:
- loader 生态丰富。
- plugin 生态丰富。
- 构建流程可定制程度高。
- 对老浏览器和复杂兼容场景经验丰富。
- 大量企业内部工具链基于 Webpack 二次封装。
所以 Webpack 不只是“旧工具”,它仍然是理解前端工程化的重要入口。
3. 学习前置知识
学习 Webpack 前,建议先掌握:
- JavaScript 模块:
import、export、CommonJSrequire。 - Node.js 基础:
package.json、npm scripts、路径处理。 - 浏览器基础:HTML、CSS、JS 的加载方式。
- 基础命令行操作。
- TypeScript 基础,如果项目使用 TS。
如果你还不理解“模块”,先看这个例子:
js
// src/math.js
export function add(a, b) {
return a + b
}js
// src/index.js
import { add } from './math.js'
console.log(add(1, 2))Webpack 会从 index.js 开始,发现它依赖 math.js,然后把它们纳入同一张依赖图。
4. 快速开始
初始化项目:
bash
mkdir webpack-demo
cd webpack-demo
npm init -y
npm install webpack webpack-cli --save-dev创建目录:
text
webpack-demo/
├── src/
│ └── index.js
├── package.json
└── webpack.config.jssrc/index.js:
js
const title = document.createElement('h1')
title.textContent = 'Hello Webpack'
document.body.appendChild(title)package.json:
json
{
"scripts": {
"build": "webpack"
}
}运行:
bash
npm run build默认会生成 dist/main.js。
5. 最小可运行配置
webpack.config.js:
js
const path = require('node:path')
module.exports = {
mode: 'development',
entry: './src/index.js',
output: {
filename: 'main.js',
path: path.resolve(__dirname, 'dist'),
clean: true,
},
}如果项目使用 ESM 配置,也可以写成:
js
import path from 'node:path'
import { fileURLToPath } from 'node:url'
const __filename = fileURLToPath(import.meta.url)
const __dirname = path.dirname(__filename)
export default {
mode: 'development',
entry: './src/index.js',
output: {
filename: 'main.js',
path: path.resolve(__dirname, 'dist'),
clean: true,
},
}5.1 这段配置做了什么
mode: 'development':使用开发模式默认配置。entry:入口文件。output.filename:输出文件名。output.path:输出目录,必须是绝对路径。output.clean:构建前清理输出目录。
5.2 为什么 output.path 要用绝对路径
Webpack 需要明确知道产物写到哪里。相对路径容易受运行目录影响,所以官方配置通常使用:
js
path.resolve(__dirname, 'dist')6. 核心概念:entry、output、module、loader、plugin
6.1 entry
入口。Webpack 从这里开始分析依赖。
单入口:
js
entry: './src/index.js'多入口:
js
entry: {
main: './src/index.js',
admin: './src/admin.js',
}多入口常用于多页面应用。
6.2 output
输出配置。
js
output: {
filename: '[name].[contenthash].js',
path: path.resolve(__dirname, 'dist'),
clean: true,
}常见占位符:
[name]:入口名或 chunk 名。[contenthash]:基于文件内容生成 hash。[id]:chunk id。
生产环境常用 [contenthash] 做长期缓存。
6.3 module
模块处理规则集合。
js
module: {
rules: [
{
test: /\.css$/i,
use: ['style-loader', 'css-loader'],
},
],
}Webpack 默认能处理 JavaScript 和 JSON。其他类型资源通常需要 loader 或内置 asset modules。
6.4 loader
loader 用于转换某类文件。
例如处理 CSS:
js
{
test: /\.css$/i,
use: ['style-loader', 'css-loader'],
}css-loader 让 Webpack 能理解 CSS 中的 @import 和 url()。
style-loader 把 CSS 注入页面的 <style> 标签。
6.5 plugin
plugin 参与整个构建流程。
例如自动生成 HTML:
bash
npm install html-webpack-plugin -Djs
const HtmlWebpackPlugin = require('html-webpack-plugin')
module.exports = {
plugins: [
new HtmlWebpackPlugin({
template: './public/index.html',
}),
],
}loader 更像“处理文件”,plugin 更像“介入流程”。
7. Loader 深入理解
7.1 loader 的执行顺序
Webpack 中多个 loader 的执行顺序是从右到左、从下到上。
js
use: ['style-loader', 'css-loader']实际顺序:
css-loader先处理 CSS。style-loader再把结果注入页面。
所以不要写反:
js
use: ['css-loader', 'style-loader']这通常会出错。
7.2 loader 的核心作用
loader 本质是一个转换函数:
text
源文件内容 -> loader 处理 -> 新内容例如:
- Sass 转 CSS。
- TypeScript 转 JavaScript。
- Vue 单文件组件转 JavaScript 模块。
- 图片转资源路径或内联 base64。
7.3 常见 loader
处理 CSS:
bash
npm install style-loader css-loader -D处理 Sass:
bash
npm install sass sass-loader -D处理 TypeScript:
bash
npm install ts-loader typescript -D处理现代 JS:
bash
npm install babel-loader @babel/core @babel/preset-env -D7.4 include 和 exclude
为了提升构建性能,loader 通常不要处理整个项目。
js
{
test: /\.js$/,
include: path.resolve(__dirname, 'src'),
use: 'babel-loader',
}常见写法:
js
exclude: /node_modules/但如果某些第三方包发布了过新的语法,也可能需要把它们纳入转译。
8. Plugin 深入理解
Plugin 比 loader 更强。它可以参与构建生命周期。
常见 plugin:
html-webpack-plugin:生成 HTML。mini-css-extract-plugin:提取 CSS 文件。copy-webpack-plugin:复制静态资源。webpack-bundle-analyzer:分析构建体积。define-plugin:定义编译时常量,Webpack 内置。
8.1 HtmlWebpackPlugin
js
const HtmlWebpackPlugin = require('html-webpack-plugin')
module.exports = {
plugins: [
new HtmlWebpackPlugin({
template: './public/index.html',
title: 'Webpack App',
}),
],
}它会自动把构建出的 JS/CSS 注入 HTML。
8.2 DefinePlugin
js
const webpack = require('webpack')
module.exports = {
plugins: [
new webpack.DefinePlugin({
__APP_VERSION__: JSON.stringify('1.0.0'),
}),
],
}业务代码:
js
console.log(__APP_VERSION__)注意:这是编译时替换,不是运行时读取。
8.3 MiniCssExtractPlugin
开发环境可以用 style-loader,生产环境通常提取 CSS 文件。
bash
npm install mini-css-extract-plugin -Djs
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
module.exports = {
module: {
rules: [
{
test: /\.css$/i,
use: [MiniCssExtractPlugin.loader, 'css-loader'],
},
],
},
plugins: [
new MiniCssExtractPlugin({
filename: 'css/[name].[contenthash].css',
}),
],
}9. 开发环境配置
开发环境重视:
- 启动速度。
- 热更新。
- source map。
- 本地代理。
- 错误提示。
9.1 webpack-dev-server
安装:
bash
npm install webpack-dev-server -Dpackage.json:
json
{
"scripts": {
"dev": "webpack serve",
"build": "webpack"
}
}配置:
js
module.exports = {
devServer: {
host: '0.0.0.0',
port: 8080,
open: true,
hot: true,
historyApiFallback: true,
},
}9.2 source map
开发环境推荐:
js
devtool: 'eval-cheap-module-source-map'生产环境可以根据需要:
js
devtool: 'source-map'或者关闭:
js
devtool: falsesource map 的作用是让浏览器错误能映射回源码位置。
9.3 本地代理
js
devServer: {
proxy: [
{
context: ['/api'],
target: 'http://localhost:3000',
changeOrigin: true,
pathRewrite: { '^/api': '' },
},
],
}业务代码:
js
fetch('/api/users')注意:开发代理只在本地开发服务器生效,生产环境需要 Nginx、网关或后端服务配置。
9.4 history 路由刷新
SPA 使用 history 路由时,本地刷新子路由可能 404。
解决:
js
devServer: {
historyApiFallback: true,
}生产环境也要配置 fallback。
10. 处理 CSS、图片、字体与静态资源
10.1 CSS
开发环境:
js
{
test: /\.css$/i,
use: ['style-loader', 'css-loader'],
}生产环境:
js
{
test: /\.css$/i,
use: [MiniCssExtractPlugin.loader, 'css-loader'],
}10.2 Sass
bash
npm install sass sass-loader -Djs
{
test: /\.s[ac]ss$/i,
use: ['style-loader', 'css-loader', 'sass-loader'],
}执行顺序:
sass-loader:Sass 转 CSS。css-loader:处理 CSS 依赖。style-loader:注入页面。
10.3 PostCSS
bash
npm install postcss postcss-loader autoprefixer -Dpostcss.config.js:
js
module.exports = {
plugins: {
autoprefixer: {},
},
}Webpack rule:
js
{
test: /\.css$/i,
use: ['style-loader', 'css-loader', 'postcss-loader'],
}10.4 Asset Modules
Webpack 5 内置了资源模块,不再必须使用 file-loader、url-loader。
js
{
test: /\.(png|jpe?g|gif|svg)$/i,
type: 'asset',
parser: {
dataUrlCondition: {
maxSize: 8 * 1024,
},
},
generator: {
filename: 'images/[name].[contenthash][ext]',
},
}type: 'asset' 会自动在资源文件和 base64 内联之间选择。
其他类型:
asset/resource:输出单独文件。asset/inline:转成 base64。asset/source:导出源代码字符串。
10.5 字体
js
{
test: /\.(woff2?|eot|ttf|otf)$/i,
type: 'asset/resource',
generator: {
filename: 'fonts/[name].[contenthash][ext]',
},
}11. 在 Webpack 中使用 TypeScript
常见方案有三种。
11.1 ts-loader
安装:
bash
npm install typescript ts-loader -D配置:
js
{
test: /\.tsx?$/,
use: 'ts-loader',
exclude: /node_modules/,
}优点:
- 和 TypeScript 编译器结合紧密。
- 配置直观。
缺点:
- 大项目中可能较慢。
11.2 Babel 转译 + TypeScript 类型检查
bash
npm install babel-loader @babel/core @babel/preset-env @babel/preset-typescript -D.babelrc:
json
{
"presets": ["@babel/preset-env", "@babel/preset-typescript"]
}Webpack:
js
{
test: /\.tsx?$/,
use: 'babel-loader',
exclude: /node_modules/,
}Babel 只负责转译,不负责完整类型检查。需要额外脚本:
json
{
"scripts": {
"type-check": "tsc --noEmit"
}
}11.3 ForkTsCheckerWebpackPlugin
大项目中常用它把类型检查放到独立进程。
bash
npm install fork-ts-checker-webpack-plugin -Djs
const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin')
module.exports = {
plugins: [
new ForkTsCheckerWebpackPlugin(),
],
}11.4 路径别名
Webpack:
js
resolve: {
extensions: ['.ts', '.tsx', '.js'],
alias: {
'@': path.resolve(__dirname, 'src'),
},
}TypeScript:
json
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]
}
}
}只配 Webpack 不配 TypeScript,编辑器可能报错。
12. 生产环境优化
生产环境重视:
- 文件体积。
- 缓存命中。
- 首屏加载。
- 代码拆分。
- 稳定产物。
12.1 mode: 'production'
js
module.exports = {
mode: 'production',
}生产模式默认启用很多优化,例如压缩、tree shaking 相关优化。
12.2 文件名加 hash
js
output: {
filename: 'js/[name].[contenthash].js',
chunkFilename: 'js/[name].[contenthash].chunk.js',
path: path.resolve(__dirname, 'dist'),
clean: true,
}内容不变,hash 尽量不变,浏览器缓存就能命中。
12.3 提取 CSS
js
new MiniCssExtractPlugin({
filename: 'css/[name].[contenthash].css',
chunkFilename: 'css/[name].[contenthash].chunk.css',
})这样 CSS 可以独立缓存,也不会依赖 JS 注入。
12.4 压缩
生产模式默认会压缩 JS。
如果需要自定义:
bash
npm install terser-webpack-plugin css-minimizer-webpack-plugin -Djs
const TerserPlugin = require('terser-webpack-plugin')
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin')
module.exports = {
optimization: {
minimizer: [
new TerserPlugin(),
new CssMinimizerPlugin(),
],
},
}12.5 构建体积分析
bash
npm install webpack-bundle-analyzer -Djs
const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer')
module.exports = {
plugins: [
new BundleAnalyzerPlugin(),
],
}用于发现:
- 大型依赖。
- 重复依赖。
- 不必要的全量引入。
- 可懒加载模块。
12.6 Babel 转译体积优化
Babel 不是压缩器。真正压缩 JS 通常交给 TerserPlugin 或 Webpack 生产模式默认优化。Babel 影响体积的重点是:
- 不要把 ES Module 转成 CommonJS,否则会影响 tree shaking。
- 不要为每个文件重复注入 Babel helper。
- 不要引入超出目标浏览器需要的 polyfill。
- 不要把浏览器已经支持的语法过度降级。
方式一:应用项目按需注入 polyfill
适合普通业务应用:
bash
npm install babel-loader @babel/core @babel/preset-env core-js -Djs
{
loader: 'babel-loader',
options: {
cacheDirectory: true,
presets: [
[
'@babel/preset-env',
{
// targets: 告诉 Babel 要兼容哪些浏览器。
// 真实项目建议放到 package.json 的 browserslist 或 .browserslistrc 中统一管理。
targets: '> 0.25%, not dead',
// modules: false 表示保留 ES Module。
// 这样 Webpack 才更容易做 tree shaking。
modules: false,
// useBuiltIns: 'usage' 表示按代码实际用到的 API 自动引入 polyfill。
// 比一次性引入整个 core-js 体积更可控。
// 注意:Babel 8 起该选项路线有变化,新项目要以当前 Babel 文档为准。
useBuiltIns: 'usage',
// corejs: 指定 core-js 主版本。
// 与项目安装的 core-js 版本保持一致。
corejs: 3,
},
],
],
},
}关键配置项:
targets:兼容目标越老,转换和 polyfill 越多,体积越大。modules: false:保留 ESM,避免破坏 Webpack 的 tree shaking。useBuiltIns: 'usage':按使用情况注入 polyfill,适合应用项目。corejs: 3:告诉 Babel 使用 core-js 3 的 polyfill 映射。cacheDirectory: true:开启 Babel 缓存,主要优化构建速度,不直接减少体积。
方式二:复用 Babel helper
适合组件库、工具库,或者大型应用中 helper 重复明显的场景:
bash
npm install @babel/plugin-transform-runtime -D
npm install @babel/runtimejs
{
loader: 'babel-loader',
options: {
presets: [
[
'@babel/preset-env',
{
targets: '> 0.25%, not dead',
modules: false,
// 重要:启用 @babel/plugin-transform-runtime 时,不要同时使用 useBuiltIns。
// transform-runtime 更适合复用 helper 和避免污染全局环境。
useBuiltIns: false,
},
],
],
plugins: [
[
'@babel/plugin-transform-runtime',
{
// helpers: 把每个文件里重复内联的 helper 改成从 @babel/runtime 引入。
helpers: true,
// regenerator: 复用 async/generator 所需运行时代码。
regenerator: true,
// corejs: false 表示不通过 transform-runtime 注入 polyfill。
// 如果需要局部 polyfill,要进一步评估 @babel/runtime-corejs3。
corejs: false,
},
],
],
},
}经验判断:
- 应用项目优先用
@babel/preset-env+ browserslist 控制兼容目标。 - 库项目优先考虑
@babel/plugin-transform-runtime,避免污染使用方全局环境。 - 不要同时使用
useBuiltIns和@babel/plugin-transform-runtime来处理 polyfill。 - 体积变化要用 bundle analyzer 验证,不要只看配置是否“高级”。
13. 代码分割与缓存策略
代码分割的目标是:
- 首屏少加载。
- 后续按需加载。
- 公共依赖单独缓存。
13.1 动态导入
js
button.addEventListener('click', async () => {
const module = await import('./heavy-module')
module.run()
})Webpack 会为动态导入生成独立 chunk。
路由懒加载:
js
const UserPage = () => import('./pages/UserPage')13.2 splitChunks
js
optimization: {
splitChunks: {
chunks: 'all',
},
}更细配置:
js
optimization: {
splitChunks: {
chunks: 'all',
cacheGroups: {
vendors: {
test: /[\\/]node_modules[\\/]/,
name: 'vendors',
priority: -10,
},
common: {
minChunks: 2,
name: 'common',
priority: -20,
},
},
},
}不要盲目把所有依赖都拆得很碎。拆包会减少单文件体积,但也可能增加请求数量和运行调度成本。
13.3 runtimeChunk
js
optimization: {
runtimeChunk: 'single',
}Webpack runtime 单独抽离后,可以减少业务 chunk hash 的连锁变化。
13.4 长缓存策略
构建产物:
text
dist/
├── js/main.a1b2c3.js
├── js/vendors.d4e5f6.js
├── css/main.aaa111.css
└── index.html推荐缓存:
- JS/CSS/图片字体:长缓存,因为文件名带 hash。
index.html:短缓存或不缓存,因为它引用最新资源。
Nginx 示例:
nginx
location /assets/ {
expires 1y;
add_header Cache-Control "public, immutable";
}
location / {
try_files $uri $uri/ /index.html;
add_header Cache-Control "no-cache";
}13.5 preload 与 prefetch
Webpack 可以通过动态导入的 magic comments 输出资源提示。
prefetch 表示“未来可能会用到”:
js
const UserSettings = () =>
import(
/* webpackChunkName: "user-settings" */
/* webpackPrefetch: true */
'./pages/UserSettings'
)作用:
- 父 chunk 加载完成后,浏览器空闲时再下载这个异步 chunk。
- 适合“用户大概率下一步会访问”的页面,例如登录后可能进入设置页。
- 优先级较低,不应该影响当前页面关键资源。
preload 表示“当前导航马上会用到”:
js
const ChartPanel = () =>
import(
/* webpackChunkName: "chart-panel" */
/* webpackPreload: true */
'./components/ChartPanel'
)作用:
- 与父 chunk 并行加载,优先级高于 prefetch。
- 适合同一页面中很快就会渲染的异步模块。
- 不能滥用,否则会抢占首屏关键资源带宽。
常见 magic comments:
webpackChunkName:指定异步 chunk 名称,便于排查和缓存观察。webpackPrefetch:生成rel="prefetch"资源提示。webpackPreload:生成rel="preload"资源提示。webpackFetchPriority:给资源请求附加优先级提示,例如"high"。webpackInclude/webpackExclude:限制动态表达式导入时可能被打包的文件范围。
使用建议:
- 路由懒加载先做好,再考虑 prefetch/preload。
prefetch适合下一屏、下一步、用户即将访问的资源。preload适合当前页面马上需要的资源,不适合大面积加。- 加完后打开浏览器 Network,查看是否出现
prefetch/preload请求以及是否影响首屏资源。
14. Tree Shaking 与副作用
Tree Shaking 的目标是移除没有用到的代码。
14.1 基本条件
通常需要:
- 使用 ES Modules。
- 生产模式。
- 代码没有不可判断的副作用。
- 包正确声明
sideEffects。
例子:
js
// math.js
export function add(a, b) {
return a + b
}
export function multiply(a, b) {
return a * b
}js
// index.js
import { add } from './math'
console.log(add(1, 2))如果 multiply 没有被使用,生产构建时有机会被移除。
14.2 sideEffects
package.json:
json
{
"sideEffects": false
}意思是项目模块没有副作用,未使用导出可以安全删除。
但 CSS import 是副作用:
js
import './style.css'如果项目里有 CSS,需要写:
json
{
"sideEffects": ["*.css"]
}否则 CSS 可能被误删。
14.3 为什么 tree shaking 不生效
常见原因:
- 使用 CommonJS。
- 引入方式导致全量导入。
- 包没有正确声明 ESM 入口。
- 代码有副作用。
- Babel 把 ESM 转成 CommonJS。
错误示例:
js
const utils = require('./utils')更推荐:
js
import { formatDate } from './utils'15. 配置拆分与工程化组织方式
项目复杂后,不建议把所有配置写在一个文件里。
常见结构:
text
build/
├── webpack.common.js
├── webpack.dev.js
└── webpack.prod.js
webpack.config.js安装:
bash
npm install webpack-merge -Dwebpack.common.js:
js
const path = require('node:path')
const HtmlWebpackPlugin = require('html-webpack-plugin')
module.exports = {
entry: './src/index.ts',
resolve: {
extensions: ['.ts', '.tsx', '.js'],
alias: {
'@': path.resolve(__dirname, '../src'),
},
},
plugins: [
new HtmlWebpackPlugin({
template: './public/index.html',
}),
],
}webpack.dev.js:
js
const { merge } = require('webpack-merge')
const common = require('./webpack.common')
module.exports = merge(common, {
mode: 'development',
devtool: 'eval-cheap-module-source-map',
devServer: {
port: 8080,
hot: true,
historyApiFallback: true,
},
})webpack.prod.js:
js
const { merge } = require('webpack-merge')
const common = require('./webpack.common')
module.exports = merge(common, {
mode: 'production',
devtool: false,
output: {
filename: 'js/[name].[contenthash].js',
chunkFilename: 'js/[name].[contenthash].chunk.js',
},
})package.json:
json
{
"scripts": {
"dev": "webpack serve --config build/webpack.dev.js",
"build": "webpack --config build/webpack.prod.js"
}
}15.1 ESLint 接入 Webpack
Webpack 中现在不推荐继续使用老的 eslint-loader,更常见的是使用 eslint-webpack-plugin。
安装:
bash
npm install eslint eslint-webpack-plugin -D配置:
js
const ESLintPlugin = require('eslint-webpack-plugin')
module.exports = {
plugins: [
new ESLintPlugin({
// context: 从哪个目录开始检查。
context: path.resolve(__dirname, 'src'),
// extensions: 检查哪些扩展名。
extensions: ['js', 'jsx', 'ts', 'tsx'],
// exclude: 排除目录。相对于 context。
exclude: ['node_modules', 'dist'],
// cache: 开启 ESLint 缓存,提高二次检查速度。
cache: true,
// cacheLocation: 缓存文件位置。
cacheLocation: path.resolve(__dirname, 'node_modules/.cache/eslint-webpack-plugin/.eslintcache'),
// fix: 是否自动修复。
// 会直接修改源文件,团队项目建议放在单独 npm script 中,而不是每次构建都开启。
fix: false,
// lintDirtyModulesOnly: 开发环境只检查改动模块。
// 大项目启动更快,但首次启动不会全量检查。
lintDirtyModulesOnly: process.env.NODE_ENV === 'development',
// failOnError: 有 error 时是否让构建失败。
failOnError: process.env.NODE_ENV === 'production',
// failOnWarning: 有 warning 时是否让构建失败。
// 严格团队可以在 CI 中开启。
failOnWarning: false,
}),
],
}每个配置项的意义:
context:缩小检查范围,避免扫到构建产物或脚本目录。extensions:决定哪些源码文件会被 ESLint 检查。exclude:排除不需要检查的目录。cache/cacheLocation:缓存 lint 结果,改善开发体验。fix:自动修复,但会改源文件,建议谨慎使用。lintDirtyModulesOnly:开发时只检查变更模块,适合大型项目。failOnError:生产构建或 CI 中建议开启,避免带明显错误上线。failOnWarning:适合规范要求非常严格的团队。
工程实践里,通常同时保留独立脚本:
json
{
"scripts": {
"lint": "eslint \"src/**/*.{js,jsx,ts,tsx}\"",
"lint:fix": "eslint \"src/**/*.{js,jsx,ts,tsx}\" --fix"
}
}原因是:Webpack 插件适合开发时即时反馈,独立 lint 脚本适合 CI、提交前检查和批量修复。
16. 常见进阶主题
16.1 多页面应用
多入口:
js
entry: {
index: './src/pages/index/main.js',
admin: './src/pages/admin/main.js',
}多个 HTML:
js
plugins: [
new HtmlWebpackPlugin({
filename: 'index.html',
chunks: ['index'],
template: './public/index.html',
}),
new HtmlWebpackPlugin({
filename: 'admin.html',
chunks: ['admin'],
template: './public/admin.html',
}),
]16.2 externals
某些依赖不打进 bundle,而是通过 CDN 提供。
js
externals: {
vue: 'Vue',
}HTML 中需要引入:
html
<script src="https://cdn.example.com/vue.global.prod.js"></script>使用 externals 要保证生产环境 CDN 可用。
16.3 Module Federation
Module Federation 常用于微前端或跨应用共享模块。
它解决的问题是:
- 一个应用可以在运行时加载另一个应用暴露的模块。
- 多个应用可以共享依赖。
初学者不建议一开始就学它。先把 entry、loader、plugin、chunk 学透。
16.4 性能排查
构建慢时,按下面顺序排查:
- loader 是否处理了过多文件。
- 是否没有排除
node_modules。 - source map 是否太重。
- TypeScript 类型检查是否阻塞主构建。
- 是否有过多大型插件。
- 缓存是否开启。
Webpack 5 支持 filesystem cache:
js
cache: {
type: 'filesystem',
}16.5 PWA 与 Workbox
PWA 的核心不是“加一个插件就完事”,而是引入 Service Worker,让应用具备离线缓存、资源预缓存、弱网可用等能力。Webpack 项目里常用 workbox-webpack-plugin。
安装:
bash
npm install workbox-webpack-plugin -D最简单方式:GenerateSW
js
const { GenerateSW } = require('workbox-webpack-plugin')
module.exports = {
plugins: [
new GenerateSW({
// clientsClaim: 新 Service Worker 激活后尽快接管页面。
clientsClaim: true,
// skipWaiting: 新 Service Worker 安装后跳过等待阶段。
// 开启后更新更快,但要处理好“用户正在使用旧页面”的情况。
skipWaiting: true,
// maximumFileSizeToCacheInBytes: 超过该大小的资源不进入预缓存。
maximumFileSizeToCacheInBytes: 5 * 1024 * 1024,
// navigateFallback: SPA 离线或刷新时回退到 index.html。
navigateFallback: '/index.html',
// runtimeCaching: 运行时缓存策略。
runtimeCaching: [
{
urlPattern: /^https:\/\/api\.example\.com\//,
handler: 'NetworkFirst',
options: {
cacheName: 'api-cache',
expiration: {
maxEntries: 50,
maxAgeSeconds: 5 * 60,
},
},
},
],
}),
],
}关键配置项:
clientsClaim:让激活后的 Service Worker 尽快控制页面。skipWaiting:跳过等待旧 Service Worker 退出的阶段。maximumFileSizeToCacheInBytes:限制预缓存资源大小,避免把大视频、大地图包塞进缓存。navigateFallback:SPA 离线访问路由时回退到入口 HTML。runtimeCaching:配置运行时缓存,例如接口、图片、字体、CDN 资源。urlPattern:匹配要缓存的请求。handler:缓存策略,例如NetworkFirst、CacheFirst、StaleWhileRevalidate。cacheName:缓存桶名称,方便 DevTools 中排查。expiration:缓存数量和时间限制。
更可控方式:InjectManifest
js
const { InjectManifest } = require('workbox-webpack-plugin')
module.exports = {
plugins: [
new InjectManifest({
// swSrc: 自己编写的 Service Worker 源文件。
swSrc: './src/service-worker.js',
// swDest: 构建后输出的 Service Worker 文件名。
swDest: 'service-worker.js',
}),
],
}选择建议:
GenerateSW:适合快速接入、缓存策略简单的项目。InjectManifest:适合需要自定义 Service Worker 逻辑、Web Push、复杂缓存策略的项目。
注意事项:
- Service Worker 通常要求 HTTPS 或 localhost。
- PWA 缓存可能导致“用户一直看到旧版本”,上线前要设计更新提示。
- 不要缓存所有接口,尤其是用户权限、订单、支付、实时数据。
- 每次修改 PWA 配置后,都要在 Chrome DevTools 的 Application 面板检查 Service Worker 和 Cache Storage。
17. Webpack 与 Vite 如何选择
17.1 新项目
如果没有历史包袱,Vue、React 等现代项目通常可以优先考虑 Vite。
原因:
- 开发体验好。
- 配置少。
- 启动快。
- 现代生态支持好。
17.2 老项目
如果项目已经基于 Webpack:
- 不要为了追新盲目迁移。
- 先评估 loader/plugin 依赖。
- 评估构建脚本、部署流程、环境变量、代理、资源路径。
- 再决定是否迁移到 Vite。
17.3 学习角度
推荐两个都学:
- Vite 用来快速搭现代项目。
- Webpack 用来理解工程化底层。
当你理解 Webpack 后,再看 Vite、Rollup、Rspack,会更容易。
18. 初学者常见误区
18.1 以为 Webpack 只是压缩代码
Webpack 不只是压缩。它的核心是构建依赖图并处理各种资源。
18.2 loader 和 plugin 分不清
记住:
- loader 处理文件内容。
- plugin 参与构建流程。
18.3 忽略 loader 执行顺序
use: ['style-loader', 'css-loader'] 是从右到左执行。
18.4 只追求配置多
Webpack 配置不是越多越好。能用默认值就别过度配置。
18.5 开发配置和生产配置混在一起
开发环境和生产环境目标不同:
- 开发重速度和调试。
- 生产重体积和缓存。
配置应该拆分。
18.6 以为 source map 越完整越好
完整 source map 有调试价值,但生产环境公开 source map 可能泄露源码结构。
19. 推荐学习顺序
第一阶段:跑起来
目标:
- 手写最小配置。
- 理解 entry 和 output。
- 能成功构建 JS。
练习:
- 创建
webpack-demo。 - 写
src/index.js。 - 运行
npm run build。
第二阶段:处理资源
目标:
- 会处理 CSS。
- 会处理 Sass。
- 会处理图片和字体。
- 会生成 HTML。
练习:
- 添加
html-webpack-plugin。 - 添加 CSS。
- 添加图片资源。
第三阶段:开发体验
目标:
- 会配置 dev server。
- 会配置 source map。
- 会配置代理。
- 会处理 history 路由刷新。
练习:
- 启动
webpack serve。 - 模拟接口代理。
- 刷新子路由不 404。
第四阶段:生产优化
目标:
- 会提取 CSS。
- 会加 contenthash。
- 会配置 splitChunks。
- 理解 tree shaking。
练习:
- 构建生产产物。
- 使用 bundle analyzer 看体积。
- 调整拆包策略。
第五阶段:工程化
目标:
- 会拆分配置。
- 会接入 TypeScript。
- 会处理环境变量。
- 会排查构建问题。
练习:
- 拆成 common/dev/prod 三份配置。
- 接入 TypeScript。
- 加类型检查脚本。
20. 高级阶段:配置项全注释实战
Webpack 的高级阶段不是“背更多配置”,而是能回答三个问题:
- 这个配置参与构建流程的哪个阶段。
- 它影响的是模块解析、资源转换、代码生成、开发服务器、缓存,还是优化策略。
- 改完后应该用什么现象验证它真的生效。
20.1 一份偏中大型项目的 webpack.config.js
下面这份配置偏学习用途。真实项目应该按需要拆成 webpack.common.js、webpack.dev.js、webpack.prod.js,不要机械复制全量配置。
js
const path = require('node:path')
const webpack = require('webpack')
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 ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin')
const ESLintPlugin = require('eslint-webpack-plugin')
const { GenerateSW } = require('workbox-webpack-plugin')
const { ModuleFederationPlugin } = require('webpack').container
module.exports = (env = {}, argv = {}) => {
// argv.mode 来自命令行 --mode。
// 没传时默认按 development 处理,避免配置里到处写 undefined 判断。
const mode = argv.mode || 'development'
const isProd = mode === 'production'
const isDev = !isProd
return {
// context: 入口和 loader 相对路径的基础目录,默认是 process.cwd()。
// 固定 context 可以减少在不同目录执行命令导致的路径歧义。
context: __dirname,
// mode: Webpack 内置模式。
// development 会偏向构建速度和调试信息。
// production 会默认开启压缩、tree shaking、作用域提升等优化。
// none 表示不套用默认优化,适合研究内部行为。
mode,
// target: 构建产物运行环境。
// 'web' 面向浏览器;'node' 面向 Node;也可以写 browserslist。
target: 'web',
// entry: 构建入口。Webpack 从这里开始建立依赖图。
// 字符串适合单入口;对象适合多入口或多页面。
entry: {
app: './src/main.ts',
},
output: {
// path: 构建产物输出目录,必须是绝对路径。
path: path.resolve(__dirname, 'dist'),
// filename: 入口 chunk 的文件名。
// [name] 来自 entry 的 key;[contenthash] 根据文件内容生成,适合长期缓存。
filename: isProd ? 'assets/js/[name].[contenthash:8].js' : 'assets/js/[name].js',
// chunkFilename: 非入口 chunk 的文件名。
// 动态 import、splitChunks 拆出来的包会使用它。
chunkFilename: isProd ? 'assets/js/[name].[contenthash:8].chunk.js' : 'assets/js/[name].chunk.js',
// assetModuleFilename: Asset Modules 默认输出文件名。
// 图片、字体等资源如果没有在 rule.generator.filename 中覆盖,就用这里。
assetModuleFilename: 'assets/media/[name].[contenthash:8][ext][query]',
// publicPath: 浏览器加载资源时使用的公共路径。
// '/' 适合根路径部署;'/admin/' 适合子路径部署;'auto' 常用于微前端和动态 CDN 场景。
publicPath: env.PUBLIC_PATH || '/',
// clean: 构建前清理 output.path。
// true 会删除旧产物,避免旧 hash 文件残留。
clean: true,
// crossOriginLoading: 给异步脚本加 crossorigin。
// 需要收集跨域脚本错误详情或配合 SRI/CDN 时会用到。
crossOriginLoading: 'anonymous',
// uniqueName: 当前构建的唯一名称。
// Module Federation、多个 Webpack 应用共存时用于避免 runtime 全局变量冲突。
uniqueName: 'learning-app',
},
// devtool: source map 策略。
// 开发环境推荐 eval-cheap-module-source-map,速度和定位较平衡。
// 生产环境可用 hidden-source-map 上传监控平台,但不公开给浏览器引用。
devtool: isProd ? 'hidden-source-map' : 'eval-cheap-module-source-map',
cache: {
// type: filesystem 表示使用文件系统持久化缓存。
// 二次构建速度会明显改善,适合中大型项目。
type: 'filesystem',
// name: 缓存命名。不同环境分开,避免 dev/prod 缓存互相污染。
name: mode,
// cacheDirectory: 缓存目录。
cacheDirectory: path.resolve(__dirname, 'node_modules/.cache/webpack'),
// buildDependencies: 这些文件变化时,让 Webpack 主动失效缓存。
// 修改构建配置后如果缓存没有失效,常见原因就是这里漏了配置文件。
buildDependencies: {
config: [__filename],
},
// compression: 是否压缩缓存数据。
// gzip 可节省磁盘空间,但会增加一点 CPU 成本。
compression: 'gzip',
},
resolve: {
// extensions: import 省略扩展名时按顺序尝试。
// 顺序越靠前优先级越高。不要放太多,避免解析成本和歧义。
extensions: ['.ts', '.tsx', '.js', '.jsx', '.json'],
// alias: 路径别名。
// $ 结尾表示精确匹配,例如 vue$ 只匹配 import 'vue'。
alias: {
'@': path.resolve(__dirname, 'src'),
'@components': path.resolve(__dirname, 'src/components'),
},
// modules: 解析第三方模块时查找的目录。
// 默认包含 node_modules;加入 src 后可以从 src 做绝对导入。
modules: [path.resolve(__dirname, 'src'), 'node_modules'],
// mainFields: 解析包入口字段顺序。
// 浏览器项目优先 browser,再 module,最后 main。
mainFields: ['browser', 'module', 'main'],
// conditionNames: 解析 package.json exports 时使用的条件。
// import/browser/development/production 会影响命中哪个导出文件。
// 注意:显式设置会覆盖默认条件集(默认含 webpack 条件),如需保留默认值要使用 "..." 继承。
conditionNames: ['import', 'browser', isProd ? 'production' : 'development'],
// symlinks: true 表示解析符号链接到真实路径。
// Monorepo 或 npm link 中如果出现重复 React/Vue 实例,可以尝试调整。
symlinks: true,
// fallback: 浏览器环境中 Node 内置模块的替代实现。
// Webpack 5 不再默认 polyfill Node 核心模块,因此需要显式配置。
fallback: {
crypto: false,
stream: false,
path: false,
},
// extensionAlias: 允许请求 .js 时优先尝试 .ts。
// 迁移 TS 或维护 ESM 包时偶尔有用,普通业务项目不是必需。
extensionAlias: {
'.js': ['.ts', '.tsx', '.js'],
},
},
module: {
// noParse: 跳过对大型、无依赖的库做依赖解析。
// 适合已经打包好的 UMD 文件。误用会导致其中的 import/require 不被处理。
noParse: /[\\/]node_modules[\\/]lodash[\\/]lodash\.js$/,
rules: [
{
// oneOf: 命中其中一个 rule 后停止继续匹配。
// 能避免同一个资源被多个互斥规则重复处理。
oneOf: [
{
// test: 匹配 TS/JS 文件。
test: /\.[cm]?[tj]sx?$/,
// include: 只处理 src,提高构建速度。
include: path.resolve(__dirname, 'src'),
// exclude: 排除 node_modules。
exclude: /node_modules/,
use: [
{
// loader: 使用 babel-loader 转译 JS/TS。
loader: 'babel-loader',
options: {
// cacheDirectory: Babel 自己的缓存。
cacheDirectory: true,
// presets: 预设转换规则。
presets: [
[
'@babel/preset-env',
{
// targets: 目标浏览器。
// 配合 browserslist 可统一项目兼容目标。
targets: 'defaults',
// modules: false 保留 ESM,便于 Webpack tree shaking。
modules: false,
// useBuiltIns/corejs: 按需注入 polyfill。
// 需要安装 core-js,并确认浏览器兼容策略。
// 如果改用 @babel/plugin-transform-runtime 管理运行时 helper,就不要同时用 useBuiltIns。
useBuiltIns: 'usage',
corejs: 3,
},
],
[
'@babel/preset-typescript',
{
// allowDeclareFields: 支持 TS declare class fields。
allowDeclareFields: true,
},
],
],
// plugins: Babel 插件列表。
// 体积优化常见选择是 @babel/plugin-transform-runtime:
// - helpers: 复用 Babel helper,避免每个文件重复注入。
// - regenerator: 复用 async/generator 运行时代码。
// - corejs: false 表示不在这里处理 polyfill。
// 注意:如果启用 transform-runtime,通常要把 preset-env 的 useBuiltIns 改为 false。
// plugins: [
// [
// '@babel/plugin-transform-runtime',
// {
// helpers: true,
// regenerator: true,
// corejs: false,
// },
// ],
// ],
},
},
],
},
{
// resourceQuery: 匹配 import './x.css?inline' 这类请求。
// 可针对同一种文件的不同查询参数走不同处理逻辑。
resourceQuery: /inline/,
type: 'asset/source',
},
{
// test: 普通 CSS。
test: /\.css$/,
use: [
// 开发用 style-loader 把 CSS 注入页面,HMR 体验好。
// 生产用 MiniCssExtractPlugin.loader 抽离 CSS 文件,利于缓存和并行加载。
isDev ? 'style-loader' : MiniCssExtractPlugin.loader,
{
loader: 'css-loader',
options: {
// importLoaders: CSS 中 @import 的资源在 css-loader 后还要经过几个 loader。
importLoaders: 1,
// modules.auto: 只有 .module.css 启用 CSS Modules。
modules: {
auto: /\.module\.css$/,
localIdentName: isDev ? '[path][name]__[local]' : '[hash:base64:8]',
},
},
},
{
loader: 'postcss-loader',
options: {
// postcssOptions: PostCSS 插件配置。
// 也可以放到 postcss.config.js。
postcssOptions: {
plugins: [['postcss-preset-env', {}]],
},
},
},
],
},
{
// test: Sass/SCSS。
test: /\.s[ac]ss$/,
use: [
isDev ? 'style-loader' : MiniCssExtractPlugin.loader,
'css-loader',
'postcss-loader',
{
loader: 'sass-loader',
options: {
// implementation: 指定 Sass 实现。
// 可用 sass 或 sass-embedded。
implementation: require('sass'),
// additionalData: 注入全局变量/mixin。
// 不要注入实际 CSS 规则,否则会重复输出。
additionalData: '@use "@/styles/variables.scss" as *;',
},
},
],
},
{
// test: 图片资源。
test: /\.(png|jpe?g|gif|webp|avif|svg)$/i,
// type: asset 会按大小自动选择 inline 或 resource。
type: 'asset',
parser: {
dataUrlCondition: {
// maxSize: 小于该值的图片转成 base64 内联。
// 小图减少请求,大图内联会撑大 JS/CSS。
maxSize: 4 * 1024,
},
},
generator: {
// filename: 图片输出路径和命名。
filename: 'assets/images/[name].[contenthash:8][ext]',
},
},
{
// test: 字体资源。
test: /\.(woff2?|eot|ttf|otf)$/i,
type: 'asset/resource',
generator: {
filename: 'assets/fonts/[name].[contenthash:8][ext]',
},
},
],
},
],
},
plugins: [
new HtmlWebpackPlugin({
// template: HTML 模板。
// 插件会自动把生成的 JS/CSS 注入模板。
template: './public/index.html',
// filename: 输出 HTML 文件名。
filename: 'index.html',
// inject: script 注入位置。body 通常能避免阻塞首屏 HTML 解析。
inject: 'body',
// minify: 生产环境压缩 HTML。
minify: isProd
? {
removeComments: true,
collapseWhitespace: true,
removeRedundantAttributes: true,
}
: false,
}),
new webpack.DefinePlugin({
// DefinePlugin: 编译时文本替换,不是在浏览器运行时赋值。
// 字符串值必须 JSON.stringify。
__APP_VERSION__: JSON.stringify(process.env.npm_package_version || '0.0.0'),
__BUILD_MODE__: JSON.stringify(mode),
}),
new webpack.EnvironmentPlugin({
// EnvironmentPlugin: 把 process.env 中的变量注入到客户端构建。
// 只放可以公开的变量,不要放密钥。
NODE_ENV: mode,
}),
new MiniCssExtractPlugin({
// filename: 入口 CSS 文件名。
filename: isProd ? 'assets/css/[name].[contenthash:8].css' : 'assets/css/[name].css',
// chunkFilename: 异步 CSS chunk 文件名。
chunkFilename: isProd ? 'assets/css/[name].[contenthash:8].chunk.css' : 'assets/css/[name].chunk.css',
}),
new ESLintPlugin({
// context: ESLint 检查起点。
context: path.resolve(__dirname, 'src'),
// extensions: 要检查的文件类型。
extensions: ['js', 'jsx', 'ts', 'tsx'],
// exclude: 排除目录。
exclude: ['node_modules', 'dist'],
// cache/cacheLocation: 缓存 lint 结果,提高二次构建速度。
cache: true,
cacheLocation: path.resolve(__dirname, 'node_modules/.cache/eslint-webpack-plugin/.eslintcache'),
// lintDirtyModulesOnly: 开发环境只检查变化模块。
lintDirtyModulesOnly: isDev,
// failOnError: 生产构建遇到 lint error 时失败。
failOnError: isProd,
// failOnWarning: 是否把 warning 也作为构建失败。
failOnWarning: false,
}),
new ForkTsCheckerWebpackPlugin({
// async: 开发环境异步报错,不阻塞构建;生产环境建议阻塞,避免带类型错误上线。
async: isDev,
typescript: {
// configFile: 指定 tsconfig。
configFile: path.resolve(__dirname, 'tsconfig.json'),
},
}),
env.MF === 'true' &&
new ModuleFederationPlugin({
// name: 当前容器名。
name: 'host',
// filename: 当前应用作为 remote 暴露时的入口文件。
filename: 'remoteEntry.js',
// remotes: 引用其他远程应用。
remotes: {
remoteApp: 'remoteApp@http://localhost:3001/remoteEntry.js',
},
// exposes: 把当前应用的模块暴露给别人。
exposes: {
'./Button': './src/components/Button',
},
// shared: 共享依赖,避免多个应用重复加载框架。
shared: {
react: { singleton: true, requiredVersion: false },
'react-dom': { singleton: true, requiredVersion: false },
},
}),
isProd &&
env.PWA === 'true' &&
new GenerateSW({
// clientsClaim: 新 Service Worker 激活后尽快接管页面。
clientsClaim: true,
// skipWaiting: 新 Service Worker 安装后跳过等待阶段。
// 更新更快,但要设计好页面刷新或更新提示。
skipWaiting: true,
// maximumFileSizeToCacheInBytes: 限制进入预缓存的单文件大小。
maximumFileSizeToCacheInBytes: 5 * 1024 * 1024,
// navigateFallback: SPA 离线路由回退入口。
navigateFallback: '/index.html',
// runtimeCaching: 运行时缓存策略。
runtimeCaching: [
{
// urlPattern: 匹配需要缓存的请求。
urlPattern: /^https:\/\/api\.example\.com\//,
// handler: NetworkFirst 优先请求网络,失败时读缓存。
handler: 'NetworkFirst',
options: {
// cacheName: 缓存桶名称。
cacheName: 'api-cache',
expiration: {
// maxEntries: 最多缓存多少条请求。
maxEntries: 50,
// maxAgeSeconds: 缓存最长保留多久。
maxAgeSeconds: 5 * 60,
},
},
},
],
}),
].filter(Boolean),
devServer: {
// host: 开发服务器监听地址。
host: '0.0.0.0',
// port: 开发服务器端口。
port: Number(env.PORT || 8080),
// hot: 启用 HMR。
hot: true,
// liveReload: HMR 无法处理时是否整页刷新。
liveReload: true,
// open: 启动后自动打开浏览器。
open: false,
// compress: 对开发服务器响应启用 gzip。
compress: true,
// allowedHosts: 允许访问开发服务器的 Host。
// 'all' 方便局域网和代理调试,但公共网络环境要谨慎。
allowedHosts: 'all',
static: {
// directory: 静态资源目录。
directory: path.resolve(__dirname, 'public'),
// publicPath: 这些静态资源挂载到哪个 URL 路径。
publicPath: '/',
// watch: public 目录变化后是否触发刷新。
watch: true,
},
client: {
// overlay: 编译错误是否用浏览器遮罩层展示。
overlay: {
errors: true,
warnings: false,
},
// logging: 浏览器控制台中 dev-server 客户端日志级别。
logging: 'info',
// progress: 是否在浏览器中显示编译进度。
progress: false,
},
historyApiFallback: {
// index: SPA history 路由刷新时回退到哪个 HTML。
index: '/index.html',
// disableDotRule: 带点路径也走 fallback,某些前端路由会需要。
disableDotRule: true,
},
proxy: {
'/api': {
// target: 后端服务地址。
target: env.API_TARGET || 'http://localhost:3000',
// changeOrigin: 修改请求头 Host 为 target。
changeOrigin: true,
// secure: 代理自签名 HTTPS 时设 false。
secure: false,
// pathRewrite: 重写路径。
pathRewrite: {
'^/api': '',
},
// ws: 代理 WebSocket。
ws: true,
},
},
headers: {
// headers: 给开发响应增加头信息。
'Cache-Control': 'no-store',
},
watchFiles: {
// paths: 额外监听的文件。
paths: ['src/**/*', 'public/**/*'],
// options: chokidar 监听配置。
options: {
ignored: ['**/node_modules/**', '**/dist/**'],
},
},
},
optimization: {
// runtimeChunk: 把 Webpack runtime 单独抽出来。
// 有利于业务 chunk 的 contenthash 稳定。
runtimeChunk: 'single',
// moduleIds/chunkIds: 稳定模块和 chunk id。
// production 默认通常已较合理,显式写出便于理解缓存策略。
moduleIds: 'deterministic',
chunkIds: 'deterministic',
// usedExports: 标记哪些导出被使用,是 tree shaking 的前置分析。
usedExports: true,
// sideEffects: 根据 package.json sideEffects 和源码分析副作用。
// 错误标记副作用会导致样式或初始化代码被误删。
sideEffects: true,
// concatenateModules: 作用域提升,把模块合并到更少闭包中。
// 生产环境默认常会启用,有助于运行性能和压缩。
concatenateModules: isProd,
// realContentHash: 基于最终资源内容生成 hash,避免无意义缓存失效。
realContentHash: isProd,
// minimize: 是否压缩产物。
minimize: isProd,
minimizer: [
new TerserPlugin({
terserOptions: {
compress: {
// drop_console: 删除 console。
// 真实项目要考虑线上排障和日志策略。
drop_console: false,
drop_debugger: true,
},
format: {
// comments: 是否保留注释。
comments: false,
},
},
// extractComments: 是否把许可证注释抽离到单独文件。
extractComments: false,
}),
new CssMinimizerPlugin(),
],
splitChunks: {
// chunks: 对哪些 chunk 做拆分。
// 'all' 表示同步和异步 chunk 都参与。
chunks: 'all',
// minSize: 生成新 chunk 的最小体积。
minSize: 20 * 1024,
// maxSize: 尝试把超过该值的 chunk 再拆小。
// 这是提示,不是强制精确切割。
maxSize: 244 * 1024,
// minChunks: 模块至少被几个 chunk 共享才拆出。
minChunks: 1,
// maxInitialRequests: 入口初始并行请求上限。
maxInitialRequests: 30,
// maxAsyncRequests: 异步加载并行请求上限。
maxAsyncRequests: 30,
// automaticNameDelimiter: 自动生成 chunk 名称的连接符。
automaticNameDelimiter: '~',
cacheGroups: {
// cacheGroups: 拆包分组规则。优先级越高越先匹配。
framework: {
test: /[\\/]node_modules[\\/](react|react-dom|vue)[\\/]/,
name: 'framework',
priority: 40,
enforce: true,
},
vendors: {
test: /[\\/]node_modules[\\/]/,
name: 'vendors',
priority: 10,
reuseExistingChunk: true,
},
commons: {
minChunks: 2,
name: 'commons',
priority: 0,
reuseExistingChunk: true,
},
},
},
},
performance: {
// hints: 资源过大时如何提示。
// 生产可以设 warning,CI 严格项目可设 error。
hints: isProd ? 'warning' : false,
// maxEntrypointSize: 入口总资源体积阈值。
maxEntrypointSize: 512 * 1024,
// maxAssetSize: 单个资源体积阈值。
maxAssetSize: 512 * 1024,
},
stats: {
// preset: 控制命令行构建信息详细程度。
preset: 'normal',
// colors: 输出彩色日志。
colors: true,
// errors/warnings: 显示错误和警告。
errors: true,
warnings: true,
// assets/chunks/modules: 是否展示资源、chunk、模块信息。
assets: true,
chunks: false,
modules: false,
// timings: 展示构建耗时。
timings: true,
},
externals: {
// externals: 不把某些依赖打进 bundle,运行时从外部获取。
// 适合 CDN 注入的大库,但必须保证 HTML 中已经提供全局变量。
// lodash: '_',
},
experiments: {
// lazyCompilation: 开发时懒编译动态导入和入口。
// 大项目可提升启动速度,但首次访问某页面会触发编译等待。
lazyCompilation: false,
// futureDefaults: 尝试未来默认配置。
// 学习和实验可打开,生产项目升级前要谨慎验证。
futureDefaults: false,
},
watchOptions: {
// ignored: watch 模式忽略目录。
ignored: /node_modules/,
// aggregateTimeout: 文件变化后延迟多久再重新构建,用来合并连续保存。
aggregateTimeout: 200,
// poll: 轮询监听。Docker、WSL、网络磁盘事件不稳定时可开启。
// poll: 1000,
},
infrastructureLogging: {
// level: Webpack 内部基础设施日志级别。
level: 'warn',
},
}
}20.2 Webpack 高级配置的理解框架
resolve 解决“这个 import 到底指向哪个文件”。路径别名、扩展名、省略扩展名、exports 条件、Node polyfill 都属于解析问题。
module.rules 解决“这个文件内容如何变成 Webpack 能处理的模块”。loader 是从右到左、从下到上执行;test/include/exclude/resourceQuery/issuer/oneOf 决定规则是否命中。
plugins 解决“构建流程中要插入什么额外动作”。生成 HTML、抽离 CSS、定义环境变量、类型检查、模块联邦都属于插件能力。
output 解决“最终产物叫什么、放哪里、浏览器从哪里加载”。hash、publicPath、chunkFilename、assetModuleFilename 会直接影响缓存和部署。
optimization 解决“如何压缩、拆包、tree shaking、稳定 hash”。高级阶段要重点观察构建产物,而不是只看配置是否写得复杂。
devServer 只服务开发环境。它的代理、history fallback、HMR、静态目录不会自动进入生产环境。线上要由 Nginx、CDN、网关、后端或部署平台承接。
Babel 解决“源码语法和运行时兼容怎么降级”。它不是压缩器,体积控制重点在 targets、modules: false、polyfill 策略和 runtime helper 复用。
ESLintPlugin 解决“构建时如何把代码规范反馈接入开发体验”。大型项目中它通常和独立 lint 脚本一起使用。
Workbox 解决“Service Worker 如何生成、预缓存哪些资源、运行时缓存哪些请求”。PWA 配置必须在浏览器 Application 面板验证。
20.3 常见高级问题排查顺序
- 入口没打进去:检查
context、entry、路径大小写、文件扩展名。 - loader 没生效:检查
test、include、exclude、oneOf顺序、loader 执行方向。 - 别名没生效:检查
resolve.alias是否绝对路径,TS 项目是否同步tsconfig.paths。 - 资源 404:检查
output.publicPath、部署子路径、HTML 注入路径、CDN 前缀。 - hash 总变化:检查
runtimeChunk、moduleIds、chunkIds、构建时间戳注入、插件输出。 - tree shaking 不生效:检查 ESM、
sideEffects、Babel 是否把模块转成 CommonJS、是否有副作用导入。 - 构建慢:打开 filesystem cache,缩小 loader include,检查 source map,分析大依赖,必要时拆分类型检查。
- devServer 代理正常但线上失败:记住开发代理只在本地存在,生产需要服务端反向代理或后端 CORS。
- prefetch/preload 不出现:确认是动态导入、magic comment 写法正确,并在构建产物页面里观察 Network。
- ESLint 没生效:检查
context、extensions、ESLint 配置文件类型,以及插件是否只在某个环境启用。 - PWA 更新异常:检查 Service Worker 生命周期、
skipWaiting、缓存策略和 Application 面板中的 Cache Storage。
21. 核心机制深度补全
本章补全 Webpack 学习中最容易只停留在“配置会写”的部分。真正掌握 Webpack,要能解释一次构建从 entry 到 asset 的过程,知道 loader 和 plugin 分别在什么层面介入,能用构建产物验证缓存、拆包、tree shaking 是否生效。
21.1 一次 Webpack 构建发生了什么
简化流程:
text
读取配置
-> 创建 compiler
-> 根据 entry 创建 compilation
-> 从入口模块开始解析依赖
-> 对每个模块匹配 loader 并转换源码
-> 生成模块依赖图 module graph
-> 根据入口和动态导入生成 chunk graph
-> 优化 chunk/module/runtime
-> 生成 assets
-> 写入 output.path核心对象:
| 对象 | 含义 |
|---|---|
| compiler | 整个 Webpack 实例,代表完整构建环境 |
| compilation | 一次具体构建,包含模块、chunk、asset、依赖图 |
| module | 被 Webpack 管理的一个模块 |
| chunk | 一组模块的集合,最终可能生成一个或多个文件 |
| asset | 最终输出资源,例如 JS、CSS、图片、字体 |
理解这几个对象后,loader/plugin 的位置就清楚了:
- loader 处理单个模块的源码转换。
- plugin 介入整个构建生命周期。
21.2 entry、module、chunk、bundle 的关系
text
entry
-> module graph
-> chunk graph
-> bundle/assets示例:
javascript
// main.js
import { createApp } from "vue";
import App from "./App.vue";
createApp(App).mount("#app");
button.addEventListener("click", async () => {
const { showDialog } = await import("./dialog");
showDialog();
});这里:
main.js是入口模块。vue、App.vue、dialog都是模块。main.js相关同步依赖进入初始 chunk。import("./dialog")生成异步 chunk。- 最终输出
main.[contenthash].js、dialog.[contenthash].js等 asset。
21.3 Loader 执行链
loader 的职责是把“Webpack 不认识或需要转换的文件”变成 JavaScript 模块或可被继续处理的内容。
javascript
module.exports = {
module: {
rules: [
{
test: /\.scss$/,
use: [
"style-loader",
"css-loader",
"sass-loader",
],
},
],
},
};执行顺序从右到左:
text
style.scss
-> sass-loader:Sass 转 CSS
-> css-loader:解析 CSS import/url,转 JS 模块
-> style-loader:运行时把 CSS 注入 style 标签生产环境通常用 MiniCssExtractPlugin.loader 替代 style-loader:
text
style.scss
-> sass-loader
-> css-loader
-> MiniCssExtractPlugin.loader:抽成独立 CSS 文件21.4 pitch loader
loader 有 pitch 阶段和 normal 阶段。大多数业务不需要写 pitch loader,但理解它有助于看懂复杂 loader。
text
pitch: style-loader -> css-loader -> sass-loader
normal: sass-loader -> css-loader -> style-loader如果某个 pitch 返回了结果,后续链路可能被截断。这也是某些 loader 能“接管”后续处理的原因。
自定义 loader 简化示例:
javascript
module.exports = function sourceLoader(source) {
const callback = this.async();
transformAsync(source)
.then(result => callback(null, result))
.catch(error => callback(error));
};loader 编写原则:
- 输入输出保持清晰。
- 不要在 loader 中做全局构建决策。
- 使用
this.cacheable()利用缓存。 - 异步操作使用
this.async()。 - source map 要正确传递。
21.5 Plugin 与 Tapable 钩子
Webpack 插件基于 Tapable 事件钩子。插件可以在构建生命周期中读取和修改 compilation。
更完整的 compiler.hooks.* 与 compilation.hooks.* 定义、触发时机和选择规则,见同目录 AST与Webpack自定义Loader和Plugin系统学习文档.md 第 9.5 节。
最小插件:
javascript
class BuildInfoPlugin {
apply(compiler) {
compiler.hooks.thisCompilation.tap("BuildInfoPlugin", (compilation) => {
compilation.hooks.processAssets.tap(
{
name: "BuildInfoPlugin",
stage: compilation.constructor.PROCESS_ASSETS_STAGE_ADDITIONS,
},
(assets) => {
compilation.emitAsset(
"build-info.json",
new compiler.webpack.sources.RawSource(
JSON.stringify({ time: new Date().toISOString() }, null, 2)
)
);
}
);
});
}
}
module.exports = BuildInfoPlugin;使用:
javascript
plugins: [
new BuildInfoPlugin(),
]常见插件介入点:
- HTML 生成。
- CSS 抽取。
- 环境变量替换。
- 类型检查。
- 资源压缩。
- 产物分析。
- Module Federation。
- PWA Service Worker。
21.5.1 Tapable 钩子类型:选错钩法会直接出错
Webpack 的钩子不是同一种。Tapable 导出 9 个 Hook 类,理解它们的差异才能解释"为什么这个钩子要 tap 而那个要 tapAsync"。
javascript
const {
SyncHook,
SyncBailHook,
SyncWaterfallHook,
SyncLoopHook,
AsyncParallelHook,
AsyncParallelBailHook,
AsyncSeriesHook,
AsyncSeriesBailHook,
AsyncSeriesWaterfallHook,
} = require("tapable");按两个维度分类:
| 维度 | 取值 | 含义 |
|---|---|---|
| 同步 / 异步 | Sync* / Async* | 同步钩子只能用 tap 注册;异步钩子支持 tap、tapAsync(callback 风格)、tapPromise(返回 Promise) |
| 行为语义 | Hook / Bail / Waterfall / Loop | 顺序执行 / 提前熔断 / 流水线传值 / 循环重试 |
各类型行为:
| 钩子类型 | 执行方式 | 返回值语义 |
|---|---|---|
SyncHook | 按注册顺序依次执行 | 忽略所有返回值 |
SyncBailHook | 顺序执行,某个 tap 返回非 undefined 就停止 | 返回值作为整个钩子的结果(熔断) |
SyncWaterfallHook | 顺序执行,前一个 tap 的返回值传给下一个 | 最终返回最后一个 tap 的返回值 |
SyncLoopHook | 某个 tap 返回非 undefined 时,从头重新执行整组 | 全部返回 undefined 才结束 |
AsyncSeriesHook | 串行 await 每个 tap | 忽略返回值 |
AsyncSeriesBailHook | 串行执行,提前熔断 | 同 SyncBailHook |
AsyncSeriesWaterfallHook | 串行流水线传值 | 同 SyncWaterfallHook |
AsyncParallelHook | 并发执行所有 tap | 忽略返回值 |
AsyncParallelBailHook | 并发执行,提前熔断 | 同 SyncBailHook |
注册时必须传名字,这是硬性约定:
javascript
compiler.hooks.compilation.tap("MyPlugin", (compilation) => {
// ...
});
compiler.hooks.emit.tapPromise("MyPlugin", async (compilation) => {
// 异步工作
});名字用于标识插件来源:错误堆栈、性能分析(profile)和调试输出都靠它定位是谁注册的回调。
对应到 Webpack 里几个高频钩子:
| 钩子 | 类型 | 为什么是这个类型 |
|---|---|---|
compiler.hooks.compilation | SyncHook | 通知型:只需要拿到新建的 compilation,不改变它 |
compiler.hooks.emit | AsyncSeriesHook | 产物写出前允许异步修改,所有插件按序执行完才继续 |
compiler.hooks.done | AsyncSeriesHook | 构建完成后做异步收尾(上传、通知) |
compilation.hooks.processAssets | AsyncSeriesHook | 按 stage 串行处理产物,压缩、注入、统计各占一个阶段 |
⚠️ 给同步钩子用 tapAsync/tapPromise 注册、或给异步钩子传同步语义的返回值(比如该返回 Promise 时忘了返回),是自定义插件最常见的运行时报错来源。写插件前先确认目标钩子的类型,再看用哪种注册方式。
21.6 resolve:模块解析不是简单拼路径
当你写:
javascript
import Button from "@/components/Button";Webpack 要判断:
@是不是 alias。- 是否补全扩展名:
.js、.ts、.vue。 - 是否读取 package.json 的
exports、module、main。 - 是否匹配条件导出。
- 是否跟随 symlink。
配置示例:
javascript
resolve: {
alias: {
"@": path.resolve(__dirname, "src"),
},
extensions: [".ts", ".tsx", ".js", ".vue", ".json"],
mainFields: ["browser", "module", "main"],
}TS 项目必须同步 tsconfig.json:
json
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]
}
}
}否则会出现“Webpack 能跑,TypeScript 报错”或“IDE 找不到路径”的问题。
21.7 splitChunks 的思考方式
代码分割目标不是“chunk 越多越好”,而是平衡:
- 首屏加载。
- 缓存命中。
- 请求数量。
- 复用程度。
- 变更频率。
典型策略:
javascript
optimization: {
runtimeChunk: "single",
splitChunks: {
chunks: "all",
cacheGroups: {
vue: {
test: /[\\/]node_modules[\\/](vue|vue-router|pinia)[\\/]/,
name: "vue-vendor",
priority: 30,
},
vendors: {
test: /[\\/]node_modules[\\/]/,
name: "vendors",
priority: 10,
},
},
},
}为什么要拆 runtime:
- runtime 包含模块加载映射。
- 业务代码变化可能导致 runtime 变化。
- 把 runtime 单独拆出,有助于 vendor chunk 长缓存稳定。
验证方式:
- 构建两次。
- 只改业务代码。
- 观察 vendor 文件 hash 是否保持稳定。
- 如果 vendor hash 也变,检查 runtime、moduleIds、chunkIds、插件注入内容。
21.8 contenthash、chunkhash、hash
| 占位符 | 含义 | 推荐场景 |
|---|---|---|
[hash] | 整次构建 hash | 较少用于长期缓存 |
[chunkhash] | chunk 级 hash | JS chunk |
[contenthash] | 内容级 hash | JS/CSS/资源长期缓存 |
生产环境推荐:
javascript
output: {
filename: "js/[name].[contenthash:8].js",
chunkFilename: "js/[name].[contenthash:8].js",
assetModuleFilename: "assets/[name].[contenthash:8][ext]",
}缓存策略:
text
index.html:短缓存或不缓存
*.js/*.css/assets:长缓存 + immutable原因:index.html 负责引用最新 hash 文件,不能长期缓存;hash 资源内容变了文件名会变,可以长缓存。
21.9 Tree Shaking 为什么不生效
Tree Shaking 依赖静态分析。常见条件:
- 使用 ES Module。
- 生产模式。
- 未被 Babel 转成 CommonJS。
- package.json 正确声明
sideEffects。 - 代码没有无法分析的动态副作用。
示例:
javascript
// math.js
export function add(a, b) {
return a + b;
}
export function heavy() {
console.log("heavy");
}
// main.js
import { add } from "./math";
console.log(add(1, 2));如果配置正确,heavy 有机会被删除。
副作用示例:
javascript
import "./global.css";
import "./polyfill";这类导入不能随便删除。package.json 可以这样声明:
json
{
"sideEffects": [
"*.css",
"./src/polyfill.js"
]
}不要盲目写:
json
{
"sideEffects": false
}如果项目里有全局样式、polyfill、全局注册组件,错误声明会导致生产环境功能丢失。
21.9.1 sideEffects 与 usedExports 是两套不同机制
官方文档专门澄清:sideEffects 和 usedExports(常被笼统称为 tree shaking)是两个不同的优化,效果层级不同:
| 机制 | 粒度 | 效果 |
|---|---|---|
sideEffects | 模块级 | 可以跳过整个模块/文件乃至其完整依赖子树(跳过评估) |
usedExports | 语句级 | 标记未使用的导出,依赖 terser 判断语句是否有副作用,无法跳过依赖子树 |
sideEffects 更有效,因为它允许直接跳过整个模块。usedExports 依赖 terser 去判断每条语句是否有副作用——这在 JavaScript 里是出了名的难题,效果不如显式的 sideEffects 标记;而且按规范,副作用必须被求值,所以它无法跳过依赖子树。导出函数通常没问题,但 React 的高阶组件(HOC)这类场景就比较棘手。
sideEffects 对模块的三种处理方式:
text
include(包含) :引入该模块、求值、继续分析其依赖。
skip over(越过):不引入、不求值,但继续分析其依赖(用于被 re-export 中转的模块)。
exclude(排除) :不引入、不求值、也不再分析依赖。一个典型的 sideEffects: false 误删案例:组件库 Button/index.js 里写了 import "./Button.css",如果包声明了 sideEffects: false,而消费方只 import { Button },Webpack 会认为 CSS 这个导入也没有副作用,于是把 Button.css 一起剔除——组件能渲染,样式却丢了。正确写法是把 CSS 标为有副作用:
json
{
"name": "awesome-ui",
"sideEffects": ["**/*.css"]
}21.9.2 用 /*#__PURE__*/ 帮 terser 判断
sideEffects 是模块级声明,/*#__PURE__*/ 是语句级注释,作用是把某条语句标记为无副作用,让 terser 敢于删除:
javascript
const Button$1 = /* #__PURE__ */ withAppProvider()(Button);没有这个注释时,terser 面对 withAppProvider()(Button) 这种"调用函数再调用返回值"的写法很难确定有没有副作用(merge、hoistStatics 里会不会改全局?赋值会不会触发 setter?)。多数成熟的库在发布产物里已经由工具自动补上 /*#__PURE__*/,理解它有助于读懂压缩后的产物为何能被删掉。
21.9.3 开发模式验证:usedExports
tree shaking 的完整删除主要发生在生产模式,但可以在开发模式提前验证"标记"是否正确。给配置加 optimization.usedExports: true 并用开发模式构建,产物里会出现类似注释:
javascript
/* unused harmony export square */这表示 Webpack 已经识别出 square 未被使用。注意此时函数体仍在产物里(真正删除交给生产模式的 terser),所以开发模式看到 unused 注释、生产模式体积没降,要去查 sideEffects 声明和是否有隐藏副作用,而不是怀疑标记没生效。
21.10 Source Map 选择
source map 是源码和构建产物之间的映射。
常见选择:
| 场景 | 推荐 |
|---|---|
| 开发调试 | eval-cheap-module-source-map 或类似快速配置 |
| 生产错误定位 | source-map,上传监控平台后不公开 |
| 生产安全敏感 | 关闭或使用 hidden source map |
| 构建速度优先 | cheap/eval 类 |
生产环境不要无脑公开 source map,因为它可能暴露源码、接口路径、业务逻辑。
21.11 filesystem cache
Webpack 5 支持文件系统缓存:
javascript
cache: {
type: "filesystem",
buildDependencies: {
config: [__filename],
},
}优化点:
- 缩短二次构建时间。
- 配合 loader 缓存减少重复转换。
- CI 环境可结合缓存目录持久化。
排查:
- 配置变化但缓存未失效:检查
buildDependencies。 - 缓存过大:定期清理。
- loader 输出依赖外部文件:确保把依赖声明给 Webpack。
21.11.1 缓存默认值与关键配置项
先记住一个容易出错的前提:cache 的默认值取决于 mode。
| mode | cache 默认 |
|---|---|
production | false |
development | { type: 'memory' } |
none | false |
也就是说,生产构建默认不缓存,要手动开启 filesystem 才能享受二次构建加速;开发模式默认是内存缓存(进程退出即失效),和文件系统缓存不是一回事。cache: true 是 cache: { type: 'memory' } 的别名。
常用配置项:
| 配置 | 默认 | 说明 |
|---|---|---|
cache.type | 见上表 | 'memory' 或 'filesystem' |
cache.buildDependencies | webpack/lib | 构建代码依赖(含配置文件本身),其内容变化会使文件系统缓存失效;config: [__filename] 的写法就是把配置文件本身声明进去 |
cache.cacheDirectory | node_modules/.cache/webpack | 缓存根目录 |
cache.cacheLocation | cacheDirectory/name | 实际缓存文件位置 |
cache.name | ${config.name}-${config.mode} | 不同名字对应互不干扰的独立缓存;多份配置需要独立缓存时显式设置 |
cache.version | '' | 手动版本号,不同版本不会复用缓存;配置变化到"无法复用旧缓存"的程度时改它 |
cache.idleTimeout | 60000 | 空闲多少毫秒后写盘 |
cache.idleTimeoutAfterLargeChanges | 1000 | 检测到大变更后,多快写盘 |
cache.idleTimeoutForInitialStore | 5000 | 首次缓存写盘的等待时间 |
cache.maxAge | 5184000000(约一个月) | 未使用的缓存条目保留多久 |
cache.compression | false | 可选 'gzip'、'brotli'、'zstd',压缩缓存文件 |
cache.managedPaths | ['./node_modules'] | 包管理器管理的路径;Webpack 假定其版本唯一,直接用快照而不逐文件校验 |
cache.version 的典型用途:某次配置改动在语义上已经不能复用旧缓存,但 buildDependencies 的 hash 又没变(例如改了外部工具的行为),这时改版本号强制失效。
21.11.2 CI 中共享文件系统缓存
官方文档给出的两条前提:
- CI 平台要支持在多次构建之间共享缓存目录。
- CI 任务必须在相同的绝对路径下运行——因为 Webpack 的缓存文件里存的是绝对路径,路径变了缓存就等于作废。
GitHub Actions 的常见做法是用 actions/cache 缓存 node_modules/.cache/webpack,并配置 restore-keys 回退到主分支的缓存:
yaml
- uses: actions/cache@v4
with:
path: node_modules/.cache/webpack
key: ${{ runner.os }}-webpack-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-webpack-⚠️ 两个实践中会遇到的坑:一是不要在同一个 job 里先跑 npm ci 再依赖缓存目录——npm ci 会删掉并重建 node_modules,把里面的缓存也清掉;二是多份配置如果显式设置了相同的 cache.name,缓存会互相污染,必要时给它们不同的名字(默认名字已包含配置名和 mode)。
21.12 构建性能优化
优先级:
- 缩小 loader 命中范围:
include: path.resolve(__dirname, "src")。 - 开启 filesystem cache。
- 减少不必要插件。
- 开发环境使用更快 source map。
- 类型检查放独立进程或 CI。
- 大依赖按需引入。
- 使用 bundle analyzer 看真实体积。
- 需要时考虑 SWC、esbuild-loader、Rspack 等替代链路。
示例:
javascript
{
test: /\.[jt]sx?$/,
include: path.resolve(__dirname, "src"),
use: {
loader: "babel-loader",
options: {
cacheDirectory: true,
},
},
}21.13 开发环境与生产环境不要混淆
开发环境关注:
- 启动速度。
- HMR。
- source map。
- 本地代理。
- 错误提示。
生产环境关注:
- 体积。
- 压缩。
- 长缓存。
- 代码分割。
- 安全。
- 兼容性。
典型拆分:
text
webpack.common.js
webpack.dev.js
webpack.prod.js配置合并:
javascript
const { merge } = require("webpack-merge");
const common = require("./webpack.common");
module.exports = merge(common, {
mode: "production",
});21.14 Webpack 排查方法
遇到问题时,不要只盯着配置文件。按产物和链路排查:
- 看终端错误:是解析失败、loader 失败、插件失败还是压缩失败。
- 看 dist:文件是否生成,路径是否符合预期。
- 看 Network:资源是否 404,publicPath 是否正确。
- 看 source map:是否能定位源码。
- 看 stats:模块是否被打入预期 chunk。
- 看 analyzer:大包来自哪里。
- 看缓存:改一行业务代码时 vendor hash 是否变化。
- 看环境差异:devServer 正常不代表生产 Nginx 正常。
生成 stats:
bash
npx webpack --profile --json > stats.json然后用分析工具查看模块、chunk、体积和依赖关系。
21.15 版本基线与源码阅读入口
本文本次补全按 2026-07-09 核对的 npm 版本书写:
| 包 | 核对版本 | 说明 |
|---|---|---|
webpack | 5.108.4 | Webpack 主包 |
tapable | 2.3.3 | Webpack 生命周期钩子系统 |
💡 2026-09-01 复核:npm 上 webpack latest 为 5.110.2,本文新增的钩子类型、模块解析规则、tree shaking 机制与持久化缓存内容均按 5.110.x 官方文档核实。小版本迭代请以官方 changelog 为准。
Webpack 发布包保留了大量 lib/*.js 源码,读原理时可以直接从 node_modules/webpack/lib 下定位。
推荐入口:
| 文件 | 重点 |
|---|---|
lib/webpack.js | webpack 函数入口,创建 compiler |
lib/Compiler.js | 总调度器,定义 run/watch/compile/emit/done 等生命周期 |
lib/Compilation.js | 一次构建的状态容器,管理 modules/chunks/assets/dependencies |
lib/NormalModuleFactory.js | 普通模块创建,负责 resolve、loader、parser 等链路 |
lib/NormalModule.js | 一个普通模块如何 build、run loaders、parse、code generation |
lib/JavascriptParser.js | JS 依赖分析,识别 import、require、dynamic import 等 |
lib/optimize/* | splitChunks、module ids、chunk ids、tree shaking 相关优化 |
lib/cache/* | filesystem cache、memory cache |
schemas/WebpackOptions.json | 配置 schema |
读源码时先抓住三个对象:
Compiler:代表整个 Webpack 编译器和完整构建环境。Compilation:代表一次具体构建,watch 模式下每次变更都会产生新的 compilation。Module:代表被纳入依赖图的模块,可能是 JS、CSS、图片、字体、WASM 等。
21.16 Webpack 完整工作原理
Webpack 的核心不是“把文件拼起来”,而是构建一张依赖图,然后把依赖图切成 chunk,最后把 chunk 渲染成 assets。
完整链路:
text
读取配置
-> 创建 Compiler
-> 应用 plugins
-> run/watch
-> 创建 Compilation
-> 根据 entry 创建入口依赖
-> resolve 模块路径
-> 匹配 module.rules
-> 执行 loaders
-> parse AST,收集依赖
-> 递归构建 ModuleGraph
-> seal 阶段冻结模块图
-> 生成 ChunkGraph
-> tree shaking、splitChunks、runtime 处理
-> code generation
-> render chunk
-> processAssets
-> emit 到 output.path
-> 输出 stats21.16.1 Compiler 做什么
Compiler 负责“整条流水线的调度”。它持有:
- 配置对象。
- 输入输出文件系统。
- resolver factory。
- cache。
- hooks。
- records。
- watch 状态。
它本身不代表某一个模块,也不代表某一次构建结果。它更像 Webpack 运行时的主控制器。
源码级简化示例,非逐字复制:
js
class Compiler {
run(callback) {
this.hooks.beforeRun.callAsync(this, () => {
this.hooks.run.callAsync(this, () => {
this.compile((err, compilation) => {
if (this.hooks.shouldEmit.call(compilation) === false) {
return done();
}
this.emitAssets(compilation, () => {
this.emitRecords(() => {
const stats = new Stats(compilation);
this.hooks.done.callAsync(stats, callback);
});
});
});
});
});
}
}对应源码定位:
webpack/lib/Compiler.js- 关键方法:
run、watch、compile、newCompilation、emitAssets
21.16.2 Compilation 做什么
Compilation 负责“一次构建的所有中间状态和产物”。它持有:
modules:所有模块。chunks:代码块。assets:最终要输出的文件。moduleGraph:模块之间的依赖关系。chunkGraph:模块和 chunk 的关系。errors/warnings。hooks。
watch 模式下,文件改一次,通常会生成一次新的 Compilation。这也是为什么插件里不能把上一次 compilation 的临时状态无脑复用到下一次。
21.16.3 ModuleGraph 与 ChunkGraph
ModuleGraph 回答的是:
- A 模块 import 了谁?
- 谁 import 了 A?
- 某个 export 是否被使用?
- 模块之间的依赖边是什么?
ChunkGraph 回答的是:
- 哪些模块被放进哪个 chunk?
- 某个 chunk 依赖哪些 runtime?
- 动态导入应该拆成哪个异步 chunk?
- chunk id 和 hash 怎么生成?
两者关系:
text
源码依赖关系 -> ModuleGraph
构建输出分组 -> ChunkGraph21.17 Webpack 生命周期:每个周期做什么
Webpack 生命周期由 Tapable hooks 组织。插件本质上就是在合适的 hook 上注册函数。
21.17.1 Compiler 级生命周期
| 阶段 | Hook | 做什么 |
|---|---|---|
| 环境初始化 | environment | Node 环境、文件系统等初始化后触发 |
| 环境初始化后 | afterEnvironment | 环境准备完成,可做补充初始化 |
| 配置入口 | entryOption | 处理 entry 配置 |
| 执行前 | beforeRun | 单次构建 run 前,适合准备缓存、日志 |
| 执行 | run | 构建正式开始 |
| watch 执行 | watchRun | watch 模式每轮构建前 |
| 编译参数 | beforeCompile | 创建 compilation 参数前后 |
| 编译开始 | compile | 即将创建 compilation |
| 创建 compilation | thisCompilation | 当前 compilation 创建,适合挂 compilation hooks |
| compilation 已创建 | compilation | 所有插件都可感知当前 compilation |
| 构建依赖图 | make | 从 entry 开始构建模块图 |
| make 结束 | finishMake | 模块收集基本完成 |
| 编译结束前 | afterCompile | compilation 构建后、emit 前 |
| 是否输出 | shouldEmit | 可以阻止输出 |
| 输出前 | emit | assets 即将写入磁盘,旧插件常用 |
| 单文件输出 | assetEmitted | 某个 asset 写出后 |
| 输出后 | afterEmit | 所有 assets 输出后 |
| 完成 | done | stats 生成,构建完成 |
| 完成后 | afterDone | done 后同步通知 |
| watch 失效 | invalid | 文件变化导致当前构建失效 |
| watch 关闭 | watchClose | watch 模式关闭 |
常用选择:
- 想新增/修改最终文件:优先用
compilation.hooks.processAssets,不要只依赖旧的compiler.hooks.emit。 - 想观察构建耗时:用
beforeRun/run/done。 - 想为每次构建挂内部钩子:用
thisCompilation。 - 想处理 watch 文件变化:用
invalid或watchRun。
21.17.2 Compilation 级生命周期
| 阶段 | Hook | 做什么 |
|---|---|---|
| 模块构建前 | buildModule | 某个模块开始 build |
| 模块构建成功 | succeedModule | 模块 build 成功 |
| 模块构建失败 | failedModule | 模块 build 失败 |
| 所有模块完成 | finishModules | 模块图构建基本完成 |
| seal 开始 | seal | 开始冻结依赖图并生成 chunk |
| 优化依赖 | optimizeDependencies | 优化依赖关系 |
| 优化模块 | optimizeModules | 优化 modules |
| 优化 chunks | optimizeChunks | 优化 chunks |
| after 优化 | afterOptimizeChunks | chunk 优化结束 |
| 产物处理 | processAssets | 新增、修改、压缩、分析 assets |
| seal 结束 | afterSeal | seal 阶段完成 |
processAssets 有多个 stage,适合精细控制插件顺序:
js
class BuildInfoPlugin {
apply(compiler) {
compiler.hooks.thisCompilation.tap('BuildInfoPlugin', (compilation) => {
compilation.hooks.processAssets.tap(
{
name: 'BuildInfoPlugin',
stage: compiler.webpack.Compilation.PROCESS_ASSETS_STAGE_ADDITIONS,
},
() => {
compilation.emitAsset(
'build-info.json',
new compiler.webpack.sources.RawSource(
JSON.stringify({ time: new Date().toISOString() }, null, 2)
)
);
}
);
});
}
}21.18 Loader 工作原理:为什么从右到左
Loader 的职责是“把某种资源转换成 JavaScript 或 Webpack 能继续处理的模块内容”。
对于:
js
import './style.less';配置:
js
module.exports = {
module: {
rules: [
{
test: /\.less$/,
use: ['style-loader', 'css-loader', 'less-loader'],
},
],
},
};普通执行阶段从右到左:
text
style.less
-> less-loader:Less 转 CSS
-> css-loader:CSS 转 JS module,并处理 @import/url()
-> style-loader:运行时把 CSS 注入 style 标签21.18.1 pitch 阶段
Loader 还有 pitch 阶段,方向从左到右:
text
pitch: style-loader -> css-loader -> less-loader
normal: less-loader -> css-loader -> style-loader如果某个 loader 的 pitch 返回了结果,后面的 pitch 和部分 normal 链路可能被短路。真实业务里你不常写 pitch,但理解它有助于读 style-loader、thread-loader 等源码。
源码级简化示例,非逐字复制:
js
async function runLoaders(resource, loaders) {
for (const loader of loaders) {
const pitched = await loader.pitch?.(resource);
if (pitched) return runNormalLoadersBefore(loader, pitched);
}
let content = await readFile(resource);
for (const loader of loaders.reverse()) {
content = await loader.normal(content);
}
return content;
}相关源码定位:
loader-runner/lib/LoaderRunner.js- Webpack 中普通模块构建入口:
webpack/lib/NormalModule.js
21.18.2 Loader 上下文
Loader 函数中的 this 不是普通对象,而是 loader context。常见能力:
| API | 作用 |
|---|---|
this.async() | 切换为异步 loader |
this.callback() | 返回异步结果 |
this.getOptions() | 读取 loader options |
this.emitFile() | 生成额外文件 |
this.addDependency() | 声明额外文件依赖,watch 时会监听 |
this.resourcePath | 当前资源路径 |
this.sourceMap | 是否需要 source map |
21.19 Resolve 与 alias 的实现原理
Webpack 的路径解析主要由 enhanced-resolve 承担。它不是简单拼路径,而是一套插件化 resolver。
解析一个请求时会考虑:
context:从哪个目录开始解析。resolve.alias:路径别名。resolve.modules:查找模块目录,默认类似node_modules。resolve.extensions:省略扩展名时尝试的后缀。resolve.mainFields:package 入口字段优先级。resolve.conditionNames:package exports 条件。resolve.symlinks:是否解析符号链接真实路径。resolve.fallback:浏览器环境 polyfill 或替代模块。
alias 示例:
js
const path = require('node:path');
module.exports = {
resolve: {
alias: {
'@': path.resolve(__dirname, 'src'),
react$: path.resolve(__dirname, 'node_modules/react/index.js'),
},
extensions: ['.tsx', '.ts', '.jsx', '.js', '.json'],
},
};解析过程:
text
import Button from "@/components/Button"
-> alias 命中 @
-> 替换为 /project/src/components/Button
-> 尝试 Button.tsx / Button.ts / Button.jsx / Button.js / Button.json
-> 找到真实文件
-> 创建 NormalModulereact$ 中的 $ 表示精确匹配,只匹配 react,不匹配 react/jsx-runtime。
21.19.1 官方解析规则:三类路径与文件/目录分支
Webpack 官方把解析器要处理的路径分成三类,理解这个分类能解释大部分"解析失败"报错:
text
1. 绝对路径(/home/me/file、C:\Users\me\file)
-> 已经是绝对路径,无需进一步解析。
2. 相对路径(../src/file1、./file2)
-> 以 import/require 所在源文件目录为 context,
把相对路径拼接成绝对路径。
3. 模块路径(my-module、my-module/lib/file)
-> 在 resolve.modules 指定的所有目录中查找(默认逐层向上找 node_modules)
-> 如果包里有 package.json,按 resolve.exportsFields 指定的字段
(默认 exports)决定包的可导出入口
-> 也可以先用 resolve.alias 把原模块路径替换成别名路径拿到绝对路径后,还要判断它指向文件还是目录,两条分支规则不同:
text
指向文件:
-> 路径已带扩展名:直接打包。
-> 没有扩展名:按 resolve.extensions 顺序尝试(默认 ['.js', '.json', '.wasm']),
命中第一个存在的文件。
指向目录:
-> 目录里有 package.json:按 resolve.mainFields 顺序读取字段
(web 目标默认 ['browser', 'module', 'main']),取第一个给出有效路径的字段。
-> 没有 package.json,或 mainFields 都没给出有效路径:
按 resolve.mainFiles 顺序找目录下的默认文件名(默认 ['index'])。
-> 最后同样用 resolve.extensions 确定扩展名。几个与默认行为相关的常见配置项:
| 配置 | 默认 | 说明 |
|---|---|---|
resolve.extensions | ['.js', '.json', '.wasm'] | 同名多文件时按数组顺序取第一个命中的 |
resolve.mainFiles | ['index'] | 目录入口的默认文件名 |
resolve.exportsFields | ['exports'] | 读 package.json 中哪些字段作为包导出入口 |
resolve.conditionNames | 动态 | 见下 |
resolve.byDependency | 按依赖类型微调 | 让 esm、commonjs、url 等不同导入方式用不同解析规则 |
conditionNames 的默认值不是写死的,而是由 mode 和 target 动态组合:基础条件恒有 "webpack",加上 "production" 或 "development"(mode 为 "none" 时按 "production" 处理);再按 target 追加——web 对应 "browser"、node 对应 "node"、webworker 对应 "worker" 等。所以 target: "web" + mode: "production" 的默认条件大致是 ["webpack", "production", "browser"]。这也解释了为什么同一个包在 dev 和 prod 下可能解析到不同入口:exports 里的 development/production 条件会被分别命中。
💡 别名还有两个容易忽略的官方行为:alias 值设为 false 表示让 Webpack 忽略该模块(常用于把只在 Node 下可用的模块在浏览器构建里替换为空);键名加 $ 是精确匹配,不加 $ 则 react/jsx-runtime 这类子路径也会继续走正常解析。
21.20 Parser、Dependency 与依赖图
Webpack 构建 JS 模块时,会解析 AST,并把不同语法转换为 Dependency 对象。
常见对应关系:
| 源码语法 | Dependency 类型方向 | 影响 |
|---|---|---|
import x from './a' | ESM import dependency | 静态依赖,参与 tree shaking |
export { x } from './a' | re-export dependency | 影响 export usage 分析 |
require('./a') | CommonJS require dependency | 同步依赖,tree shaking 能力较弱 |
import('./a') | dynamic import dependency/block | 形成异步 chunk |
require.context() | context dependency | 生成上下文模块 |
简化理解:
text
源码字符串
-> parser 生成 AST
-> parser hooks 识别 import/require
-> 创建 Dependency
-> 加入当前 Module 的 dependencies
-> ModuleGraph 记录边
-> 递归构建被依赖模块这解释了为什么:
- 静态
import更容易 tree shaking。 - 动态
import()会天然拆包。 - 变量拼接路径会导致 context module,可能把一批文件都打进去。
21.21 Seal、优化与产物生成
make 阶段重点是“把模块图建出来”。seal 阶段重点是“把模块图变成可输出的 chunk 和 asset”。
简化链路:
text
finishModules
-> seal
-> optimizeDependencies
-> create chunks
-> optimizeModules
-> optimizeChunks
-> assign module ids / chunk ids
-> codeGeneration
-> createChunkAssets
-> processAssets
-> afterSeal21.21.1 Tree Shaking 在哪发生
Tree Shaking 不是一个单独按钮,而是多阶段协作:
- Parser 识别 ESM import/export。
- ModuleGraph 记录 export usage。
- package.json 的
sideEffects帮助判断模块是否可删除。 - optimization 阶段标记未使用导出。
- minimizer 删除死代码。
所以 tree shaking 不生效时,常见原因是:
- 使用 CommonJS。
- 包没有正确声明
sideEffects。 - 代码有顶层副作用。
- Babel/TS 把 ESM 提前转成 CommonJS。
- 动态访问导出,静态分析失败。
21.21.2 splitChunks 在哪发生
splitChunks 发生在 chunk 优化阶段。它不是按文件夹机械拆包,而是根据模块在 chunk 间的复用关系、大小、请求数、cache group 等规则决定。
典型判断:
text
模块是否来自 node_modules?
模块被多少 chunk 共享?
体积是否达到 minSize?
拆出来后请求数是否超过限制?
cacheGroups 优先级谁更高?21.22 Webpack 源码级生命周期简表
下面是一次普通 webpack --mode production 的高层顺序:
text
webpack(options)
-> createCompiler(options)
-> apply plugins
-> compiler.run()
-> beforeRun
-> run
-> compile()
-> beforeCompile
-> compile
-> newCompilation
-> thisCompilation / compilation
-> make
-> finishMake
-> compilation.finish()
-> compilation.seal()
-> afterCompile
-> shouldEmit
-> emit
-> assetEmitted
-> afterEmit
-> emitRecords
-> done
-> afterDoneWatch 模式的差异:
text
compiler.watch()
-> watchRun
-> compile
-> done
-> 文件变化
-> invalid
-> 新一轮 watchRun / compile / done开发服务器的差异:
webpack-dev-server会在内存文件系统中服务构建产物。- HMR 需要
HotModuleReplacementPlugin、runtime、dev server websocket/client 协作。 - 和 Vite 的“浏览器按需请求源码模块”不同,Webpack dev server 仍围绕 bundle/chunk 构建模型工作,只是通过缓存、增量编译、内存输出提升开发体验。
21.23 Webpack 与 Vite 原理对照
| 维度 | Webpack | Vite |
|---|---|---|
| 开发启动 | 先构建依赖图和 bundle/chunk | 先启动服务器,浏览器请求哪个模块再转换哪个模块 |
| 模块服务 | dev server 服务 bundle/chunk | dev server 服务 ESM URL |
| HMR 边界 | 依赖图、runtime、HMR plugin | module graph、import.meta.hot、WebSocket |
| 插件模型 | Tapable hooks | Rolldown/Rollup 风格 hooks + Vite server hooks |
| loader/transform | loader 链处理模块源码 | plugin transform 处理模块源码 |
| 路径解析 | enhanced-resolve | Vite resolve plugin + Rolldown resolver 能力 |
| 生产构建 | Webpack 自己的 chunk graph 和 runtime | Vite 8 使用 Rolldown |
| 适合场景 | 存量项目、复杂定制、成熟生态 | 现代应用、快速开发、轻配置 |
理解差异后,配置迁移就不会只停留在“这个配置叫什么名字”:
- Webpack 的
loader大多迁移到 Vite 插件、框架插件或原生能力。 - Webpack 的
resolve.alias对应 Vite 的resolve.alias,但 TS paths 仍要同步。 - Webpack 的
DefinePlugin对应 Vite 的define,但环境变量推荐用import.meta.env。 - Webpack 的
devServer.proxy对应 Vite 的server.proxy。 - Webpack 的
splitChunks思路对应 Vite/Rolldown 的advancedChunks(旧manualChunks已废弃/移除)或默认拆包策略,但不要机械照搬。
22. 学习资源
- Webpack 官方文档:https://webpack.js.org/
- Webpack Concepts:https://webpack.js.org/concepts/
- Webpack Configuration:https://webpack.js.org/configuration/
- Webpack Loaders:https://webpack.js.org/loaders/
- Webpack Plugins:https://webpack.js.org/plugins/
- Webpack Compiler Hooks:https://webpack.js.org/api/compiler-hooks/
- Webpack Compilation Hooks:https://webpack.js.org/api/compilation-hooks/
- Webpack Loader API:https://webpack.js.org/api/loaders/
- Webpack Resolve 配置:https://webpack.js.org/configuration/resolve/
- Webpack Code Splitting:https://webpack.js.org/guides/code-splitting/
- Webpack Tree Shaking:https://webpack.js.org/guides/tree-shaking/
- Webpack GitHub 源码:https://github.com/webpack/webpack
- Tapable:https://github.com/webpack/tapable
- loader-runner:https://github.com/webpack/loader-runner
- enhanced-resolve:https://github.com/webpack/enhanced-resolve
- 常用 loader/plugin 源码阅读补全:
AST与Webpack自定义Loader和Plugin系统学习文档.md第 16 章 - ESLint Webpack Plugin:https://webpack.js.org/plugins/eslint-webpack-plugin/
- Workbox Webpack Plugin:https://developer.chrome.com/docs/workbox/modules/workbox-webpack-plugin
- Babel preset-env:https://babeljs.io/docs/babel-preset-env
- Babel transform-runtime:https://babeljs.io/docs/babel-plugin-transform-runtime
学习建议:
- 不要只看配置片段,要自己手写最小项目。
- 每加一个 loader,都问自己“它把什么转换成了什么”。
- 每加一个 plugin,都问自己“它介入了哪个构建流程”。
- 每做一个生产优化,都用构建产物验证效果。