POST · 前端 · 文章
Webpack 从入门到进阶
2026/11/13 · by ZHANG XING YUN · 阅读 1 次 · 3879 字 · 预计阅读 10 分钟
本文整理 Webpack 学习路径:从核心概念与最小配置起步,逐层深入 Loader、Plugin、模块解析、构建性能、产物优化、多环境配置、模块联邦,最后给出迁移建议、常见问题。
- 适用场景:Web / SPA / 微前端 / Electron 渲染层的工程化构建
- 技术栈:Webpack 5,框架无关(Vue / React 均可接入)
- 目标:构建可配置、可优化、可维护、可迁移
目录
- 一、先明确它在解决什么问题
- 二、入门:环境与最小配置
- 三、核心概念:五个关键词
- 四、Loader:让 Webpack 认识一切
- 五、Plugin:介入构建全流程
- 六、模块解析与依赖图
- 七、构建性能优化
- 八、产物优化
- 九、多环境与配置拆分
- 十、代码分割与懒加载
- 十一、模块联邦(微前端)
- 十二、调试与产物分析
- 十三、从 4 到 5 的迁移要点
- 十四、方案对比与选型
- 十五、常见问题
- 十六、FAQ
- 附录:标准配置模板与检查清单
一、先明确它在解决什么问题
Webpack 的核心定位是模块打包器:把项目里所有资源(JS、CSS、图片、字体、甚至 HTML)都当作模块,从入口出发构建依赖图,最终输出浏览器可运行的文件。
| 问题 | Webpack 的答案 |
|---|---|
| 浏览器不支持模块化/新语法? | Loader 转译 + 打包 |
| 资源类型多,管理混乱? | 一切皆模块,统一处理 |
| 首屏加载慢? | 代码分割、懒加载、Tree Shaking |
| 多环境配置不同? | 配置拆分 + DefinePlugin |
| 多团队共享代码? | 模块联邦 |
代价必须说清楚:
| 代价 | 说明 |
|---|---|
| 配置复杂 | 概念多、插件生态庞大,学习曲线陡 |
| 构建速度 | 大型项目冷启动慢,需要缓存与并行优化 |
| 产物冗余 | 不当配置会引入重复依赖与无用代码 |
判断是否适合:中大型项目、需要精细控制产物、已有成熟配置体系——Webpack 依然稳妥;新项目追求极简与速度——可评估 Vite / Rspack / Turbopack。
三个原则:
- 先理解依赖图,再写配置:所有配置都是围绕"入口 → 模块 → 产物"展开。
- Loader 管转换,Plugin 管流程:职责不要混。
- 开发与生产分离:开发求快,生产求小,配置必须拆开。
二、入门:环境与最小配置
2.1 环境准备
Node.js(推荐 LTS)+ npm。验证:
node -v && npm -v
2.2 最小可运行项目
npm init -y
npm install webpack webpack-cli --save-dev
目录结构:
webpack-demo/
├─ src/
│ └─ index.js
├─ public/
│ └─ index.html
└─ webpack.config.js
src/index.js:
const el = document.createElement("h1");
el.textContent = "Hello Webpack";
document.body.appendChild(el);
webpack.config.js:
const path = require("node:path");
module.exports = {
mode: "development",
entry: "./src/index.js",
output: {
path: path.resolve(__dirname, "dist"),
filename: "bundle.[contenthash].js",
clean: true
},
devtool: "eval-cheap-module-source-map"
};
package.json 加脚本:
{
"scripts": {
"build": "webpack",
"dev": "webpack serve"
}
}
需要本地服务时:
npm install webpack-dev-server --save-dev
最小配置的四个必填项:entry、output、mode、devtool。理解这四项,就理解了 Webpack 配置的骨架。
三、核心概念:五个关键词
| 概念 | 作用 | 配置项 |
|---|---|---|
| Entry | 依赖图起点 | entry |
| Output | 产物输出位置与命名 | output |
| Loader | 把非 JS 模块转成 Webpack 能处理的模块 | module.rules |
| Plugin | 在构建生命周期挂钩子,做更广的事 | plugins |
| Mode | 预设优化策略 | mode |
一条完整的链路:从 entry 出发 → 递归解析 import → 命中 rules 用 loader 转换 → 构建依赖图 → plugin 在各阶段介入 → 输出到 output。
3.1 构建生命周期(理解 Plugin 的关键)
Webpack 内部通过 Tapable 实现钩子,常见阶段:
| 钩子 | 时机 | 典型用途 |
|---|---|---|
environment |
环境初始化 | 读取配置 |
afterPlugins |
插件加载完 | 二次配置 |
compile |
开始编译 | 记录时间 |
make |
从入口开始构建 | 自定义构建 |
emit |
产物写入磁盘前 | 修改/生成文件 |
afterEmit |
写入后 | 上传、通知 |
done |
构建完成 | 统计、日志 |
HtmlWebpackPlugin、MiniCssExtractPlugin 都是在这些钩子上工作的。
四、Loader:让 Webpack 认识一切
4.1 Loader 的执行顺序
规则:
- 从右到左、从下到上执行(
use数组逆序)。 enforce: 'pre'最先,normal居中,enforce: 'post'最后。include/exclude控制作用范围,避免误伤node_modules。
4.2 常用 Loader 清单
| Loader | 处理 | 关键配置 |
|---|---|---|
babel-loader |
JS/TS 转译 | presets、cacheDirectory |
ts-loader / swc-loader |
TypeScript | transpileOnly、configFile |
css-loader |
解析 CSS 中的 @import、url() |
modules(CSS Modules) |
style-loader |
把 CSS 注入 DOM | 仅开发环境使用 |
postcss-loader |
自动前缀、现代语法降级 | postcss.config.js |
sass-loader / less-loader |
预处理器 | 配合对应实现包 |
asset-modules(内置) |
图片、字体 | type: 'asset' |
html-loader |
HTML 中的资源引用 | 按需 |
4.3 典型规则配置
module: {
rules: [
{
test: /\.(t|j)sx?$/,
exclude: /node_modules/,
use: {
loader: "babel-loader",
options: { cacheDirectory: true }
}
},
{
test: /\.css$/,
use: ["style-loader", "css-loader", "postcss-loader"]
},
{
test: /\.(png|jpe?g|gif|svg)$/i,
type: "asset",
parser: { dataUrlCondition: { maxSize: 8 * 1024 } },
generator: { filename: "images/[name].[hash:8][ext]" }
}
]
}
4.4 自定义 Loader
Loader 本质是一个函数:接收源码,返回处理后的源码。
// loaders/replace-loader.js
module.exports = function (source) {
const options = this.getOptions();
return source.replace(new RegExp(options.search, "g"), options.replace);
};
{
test: /\.js$/,
use: [{ loader: path.resolve(__dirname, "loaders/replace-loader.js"), options: { search: "foo", replace: "bar" } }]
}
要点:this 上有 getOptions、async、emitFile 等能力;异步处理用 this.async() 返回 callback。
五、Plugin:介入构建全流程
5.1 Loader 与 Plugin 的区别
| 维度 | Loader | Plugin |
|---|---|---|
| 作用对象 | 单个模块的源码 | 整个构建流程 |
| 运行时机 | 模块解析阶段 | 生命周期钩子 |
| 能力 | 转换文件内容 | 生成文件、优化产物、注入变量 |
| 形态 | 函数 | 带 apply(compiler) 的类/对象 |
5.2 常用 Plugin
| Plugin | 作用 | 备注 |
|---|---|---|
HtmlWebpackPlugin |
生成 HTML 并注入产物 | 多页面用 chunks 配置 |
MiniCssExtractPlugin |
抽取 CSS 为独立文件 | 生产环境使用 |
CssMinimizerPlugin |
压缩 CSS | 配合 optimization.minimizer |
DefinePlugin |
注入编译期常量 | 环境变量、process.env |
CopyWebpackPlugin |
复制静态资源 | public/ 目录 |
BundleAnalyzerPlugin |
产物体积分析 | 只在分析时启用 |
CompressionWebpackPlugin |
生成 gzip/brotli | 配合 Nginx |
5.3 自定义 Plugin
// plugins/report-plugin.js
class ReportPlugin {
apply(compiler) {
compiler.hooks.done.tap("ReportPlugin", stats => {
const info = stats.toJson({ all: false, assets: true });
console.log(`构建完成,产物数:${info.assets.length}`);
});
}
}
module.exports = ReportPlugin;
要点:compiler 是全局编译对象,compilation 是单次编译对象;监听 done 可拿到统计信息,监听 emit 可修改产物。
六、模块解析与依赖图
6.1 解析规则
resolve: {
extensions: [".ts", ".tsx", ".js", ".jsx"],
alias: { "@": path.resolve(__dirname, "src") },
modules: ["node_modules", path.resolve(__dirname, "src")],
mainFields: ["browser", "module", "main"]
}
| 配置 | 作用 | 注意 |
|---|---|---|
extensions |
省略扩展名时的查找顺序 | 顺序影响解析速度 |
alias |
路径别名 | 与 TypeScript paths 保持一致 |
modules |
查找第三方包的目录 | 自定义目录放后面 |
mainFields |
包的入口字段优先级 | 影响 Tree Shaking |
6.2 依赖图与循环依赖
Webpack 会递归解析 import,形成依赖图。循环依赖会导致模块导出为 undefined,常见于两个模块互相 import。处理方式:提取公共模块到第三个文件,或延迟到函数内部 require。
6.3 externals
把不希望打包的依赖排除,运行时从外部获取:
externals: {
react: "React",
"react-dom": "ReactDOM"
}
配合 CDN 使用,可以显著减小产物,但要确保外部资源可用与版本一致。
七、构建性能优化
7.1 缓存
Webpack 5 内置持久化缓存,开启即可大幅提升二次构建速度:
cache: {
type: "filesystem",
buildDependencies: { config: [__filename] }
}
babel-loader 加 cacheDirectory: true,ts-loader 用 transpileOnly + fork-ts-checker-webpack-plugin 把类型检查放到独立进程。
7.2 多进程/多实例
| 工具 | 作用 |
|---|---|
thread-loader |
把耗时 loader 放到 worker 池 |
esbuild-loader / swc-loader |
用 Go/Rust 实现替代 Babel |
TerserPlugin 并行 |
默认并行压缩 |
{
test: /\.(t|j)sx?$/,
use: ["thread-loader", "babel-loader"]
}
thread-loader 只适合大文件,小文件反而因进程开销变慢。
7.3 缩小处理范围
include限定源码目录,exclude排除node_modules。resolve.extensions精简,减少无效查找。module.noParse跳过已知无依赖的大文件(如lodash的 UMD 包)。
7.4 开发环境
devtool用eval-cheap-module-source-map,平衡速度与调试。webpack-dev-server用hot: true,配合HMR只更新变更模块。- 关闭生产环境的压缩、分析、哈希计算。
八、产物优化
8.1 Tree Shaking
条件:
- 使用 ES Module(
import/export),CommonJS 不参与。 - 生产模式(
mode: 'production')。 package.json的sideEffects字段声明副作用,或sideEffects: false。- 避免
import * as x后整体使用。
{ "sideEffects": ["*.css"] }
8.2 代码分割
optimization: {
splitChunks: {
chunks: "all",
cacheGroups: {
vendor: { test: /[\\/]node_modules[\\/]/, name: "vendors", priority: -10 },
common: { minChunks: 2, priority: -20, reuseExistingChunk: true }
}
},
runtimeChunk: "single"
}
runtimeChunk 把运行时代码单独抽出,避免业务变更导致 vendor 缓存失效。
8.3 压缩
| 对象 | 工具 | 说明 |
|---|---|---|
| JS | TerserPlugin(默认) |
生产模式自动开启 |
| CSS | CssMinimizerPlugin |
需显式配置 |
| HTML | HtmlWebpackPlugin 自带 minify |
按需 |
| 传输 | CompressionWebpackPlugin |
生成 .gz / .br |
8.4 哈希策略
| 字段 | 作用 | 建议 |
|---|---|---|
[hash] |
整个构建 | 不推荐 |
[chunkhash] |
单个 chunk | JS 常用 |
[contenthash] |
内容变化 | 推荐,缓存最精准 |
output: { filename: "[name].[contenthash:8].js" }
九、多环境与配置拆分
9.1 拆分方式
config/
├─ webpack.common.js 公共配置
├─ webpack.dev.js 开发
├─ webpack.prod.js 生产
└─ webpack.analyze.js 分析
用 webpack-merge 合并:
const { merge } = require("webpack-merge");
const common = require("./webpack.common.js");
module.exports = merge(common, {
mode: "production",
devtool: "source-map",
plugins: [new MiniCssExtractPlugin({ filename: "[name].[contenthash:8].css" })]
});
9.2 环境变量
| 方式 | 用途 | 注意 |
|---|---|---|
DefinePlugin |
注入编译期常量 | 值需 JSON.stringify |
dotenv + EnvironmentPlugin |
从 .env 读取 |
不要提交敏感变量 |
--env 参数 |
传入构建参数 | 配合函数式配置 |
new webpack.DefinePlugin({
"process.env.NODE_ENV": JSON.stringify(process.env.NODE_ENV),
__APP_VERSION__: JSON.stringify(require("./package.json").version)
})
注意:DefinePlugin 是文本替换,不要在注入值里放密钥。
十、代码分割与懒加载
10.1 动态 import
button.addEventListener("click", async () => {
const { default: Modal } = await import("./components/Modal");
new Modal().open();
});
Webpack 会自动为动态 import 生成独立 chunk。
10.2 魔法注释
import(/* webpackChunkName: "chart", webpackPrefetch: true */ "./chart");
| 注释 | 作用 |
|---|---|
webpackChunkName |
指定 chunk 名 |
webpackPrefetch |
空闲时预取 |
webpackPreload |
与父 chunk 并行加载 |
10.3 分割策略
- 路由级分割:每个页面一个 chunk。
- 组件级分割:体积大、非首屏的组件单独拆。
- 第三方库:按需引入,避免整包打入。
十一、模块联邦(微前端)
11.1 基本结构
提供方(remote):
new ModuleFederationPlugin({
name: "remoteApp",
filename: "remoteEntry.js",
exposes: { "./Button": "./src/components/Button" },
shared: { react: { singleton: true }, "react-dom": { singleton: true } }
})
消费方(host):
new ModuleFederationPlugin({
name: "hostApp",
remotes: { remoteApp: "remoteApp@https://cdn.example.com/remoteEntry.js" },
shared: { react: { singleton: true }, "react-dom": { singleton: true } }
})
11.2 要点
| 项 | 建议 |
|---|---|
shared |
React 等必须 singleton: true,避免多实例 |
| 版本管理 | 提供方与消费方约定版本范围,破坏性变更走新 entry |
| 部署 | remoteEntry.js 需可独立发布,做好缓存与版本号 |
| 调试 | 消费方加载失败要有降级,避免整页白屏 |
模块联邦适合多团队共享组件/页面的场景;如果只是代码复用,发布 npm 包更简单。
十二、调试与产物分析
12.1 分析产物
npm install webpack-bundle-analyzer --save-dev
const { BundleAnalyzerPlugin } = require("webpack-bundle-analyzer");
plugins: [new BundleAnalyzerPlugin({ analyzerMode: "static" })];
输出 report.html,定位体积大头与重复依赖。
12.2 构建统计
webpack --profile --json > stats.json
可上传到 Webpack Analyse 或自定义分析,查看模块耗时与 chunk 构成。
12.3 常见调试手段
| 问题 | 手段 |
|---|---|
| 产物过大 | BundleAnalyzer + stats.json |
| 构建慢 | --profile、speed-measure-webpack-plugin |
| 缓存失效 | 检查 contenthash 与 runtimeChunk |
| 模块解析错 | resolve.alias 与 extensions 对齐 tsconfig |
十三、从 4 到 5 的迁移要点
| 变化 | 处理 |
|---|---|
| 内置持久化缓存 | 删除 hard-source-webpack-plugin,开 cache.filesystem |
| Asset Modules | url-loader / file-loader 替换为 type: 'asset' |
| Node.js polyfill 移除 | 需要时手动安装 crypto-browserify 等并配置 resolve.fallback |
| 模块联邦 | 微前端可直接用内置能力 |
optimization.splitChunks 默认值变化 |
检查缓存组配置 |
移除 NamedModulesPlugin 等 |
由 optimization.moduleIds 统一控制 |
迁移建议:先在分支上升级,跑通构建与 E2E,再对比产物体积与构建时间,最后合并。
十四、方案对比与选型
| 方案 | 优点 | 缺点 | 适用 |
|---|---|---|---|
| Webpack 5 | 生态最成熟、配置能力强、插件丰富 | 配置复杂、冷启动偏慢 | 中大型项目、需要精细控制 |
| Vite | 开发极快、配置简洁 | 生产构建底层仍可能用 Rollup,生态插件略少 | 新项目、追求开发体验 |
| Rspack | 兼容 Webpack 配置、Rust 实现、快 | 生态仍在完善 | 想提速又不想改配置 |
| Turbopack | 增量构建快 | 生态与稳定性待成熟 | Next.js 生态 |
决策建议:
- 已有 Webpack 配置且稳定 → 不轻易换,优先优化缓存与并行。
- 新项目、团队接受新工具 → Vite 优先。
- 想保留 Webpack 配置又想要速度 → 评估 Rspack。
十五、常见问题
| 现象 | 原因 | 处理 |
|---|---|---|
| 构建产物白屏 | publicPath 配置错 |
按部署路径设置 output.publicPath |
| CSS 不生效 | style-loader 与 MiniCssExtractPlugin 混用 |
开发用 style,生产用 extract |
| 图片 404 | asset 输出路径与引用不一致 | 配置 generator.filename |
| Tree Shaking 无效 | 用了 CommonJS 或 import * |
改 ES Module、声明 sideEffects |
| 缓存频繁失效 | 用了 [hash] 或未拆 runtime |
改 [contenthash] + runtimeChunk |
| 构建越来越慢 | 无缓存、loader 范围过大 | 开 filesystem cache、加 include |
| 循环依赖报 undefined | 模块互相 import | 提取公共模块或延迟 require |
| 多页面 chunk 混乱 | HtmlWebpackPlugin 未指定 chunks |
每个页面显式配置 chunks |
| 环境变量注入失败 | DefinePlugin 未 JSON.stringify |
字符串值必须序列化 |
| 模块联邦多实例 | shared 未设 singleton |
React 等设 singleton: true |
十六、FAQ
Q1:Webpack 和 Vite 怎么选?
新项目、追求开发体验选 Vite;已有成熟 Webpack 配置、需要精细控制产物、依赖大量 Webpack 插件,继续用 Webpack。两者不是替代关系,是场景差异。
Q2:Loader 和 Plugin 到底怎么区分?
Loader 处理单个文件的转换(输入源码,输出源码);Plugin 处理构建流程(生成文件、优化、注入)。转换用 Loader,流程用 Plugin。
Q3:为什么生产环境不用 style-loader?
style-loader 把 CSS 注入 <style> 标签,无法抽取独立文件、无法并行加载、不利于缓存。生产环境应用 MiniCssExtractPlugin。
Q4:Tree Shaking 为什么没生效?
常见原因:依赖是 CommonJS、用了 import * as、package.json 未声明 sideEffects、未开生产模式。逐项排查。
Q5:如何减小产物体积?
按优先级:代码分割 + 懒加载 → Tree Shaking → 按需引入第三方库 → externals + CDN → 压缩与 gzip。用 BundleAnalyzer 定位大头再动手。
Q6:构建太慢怎么办?
开持久化缓存、用 esbuild/swc 替代 Babel、thread-loader 处理大文件、缩小 loader 范围、开发环境降低 devtool 精度。先测量再优化。
Q7:contenthash 和 chunkhash 区别?
chunkhash 基于 chunk 内容,contenthash 基于文件内容。业务代码变更时,contenthash 能让未变化的文件保持缓存,是当前推荐。
Q8:模块联邦和 npm 包怎么选?
多团队、需要独立发布与运行时组合,用模块联邦;只是代码复用、版本稳定,发 npm 包更简单可控。
Q9:Webpack 5 的持久化缓存有什么坑?
缓存依赖配置与 loader 版本,升级后可能失效;CI 中要正确缓存 node_modules/.cache;调试问题时先清缓存排除干扰。
Q10:如何做多页面应用?
多个 entry + 多个 HtmlWebpackPlugin 实例,每个实例用 chunks 指定对应入口,splitChunks 抽公共依赖。
附录:标准配置模板与检查清单
webpack.common.js 模板
const path = require("node:path");
const HtmlWebpackPlugin = require("html-webpack-plugin");
module.exports = {
entry: { main: "./src/index.js" },
output: {
path: path.resolve(__dirname, "dist"),
filename: "js/[name].[contenthash:8].js",
clean: true,
publicPath: "/"
},
resolve: {
extensions: [".ts", ".tsx", ".js", ".jsx"],
alias: { "@": path.resolve(__dirname, "src") }
},
module: {
rules: [
{
test: /\.(t|j)sx?$/,
exclude: /node_modules/,
use: { loader: "babel-loader", options: { cacheDirectory: true } }
},
{
test: /\.css$/,
use: ["style-loader", "css-loader", "postcss-loader"]
},
{
test: /\.(png|jpe?g|svg)$/i,
type: "asset",
parser: { dataUrlCondition: { maxSize: 8 * 1024 } },
generator: { filename: "images/[name].[hash:8][ext]" }
}
]
},
plugins: [new HtmlWebpackPlugin({ template: "./public/index.html" })],
optimization: {
splitChunks: { chunks: "all" },
runtimeChunk: "single"
}
};