Webpack 深入
Webpack 是一个静态模块打包器:它从入口文件出发,递归构建依赖图,把 JS、CSS、图片等一切资源当作"模块",最终产出一份或多份浏览器可执行的 bundle。理解它的五大核心概念,就掌握了 Webpack 的主干。
一、核心概念
| 概念 | 作用 | 类比 |
|---|---|---|
| entry(入口) | 打包起点,告诉 Webpack 从哪里开始 | 书的目录 |
| output(出口) | 打包结果输出到哪、叫什么 | 装书的箱子 |
| loader(加载器) | 把"非 JS 资源"或"新语法"转换成 Webpack 能处理的模块 | 翻译官 |
| plugin(插件) | 在构建生命周期中做 loader 做不了的事 | 施工队 |
| mode(模式) | development / production / none 三种内置优化 | 开关档位 |
二、配置基本结构
// webpack.config.js(CommonJS 导出)
const path = require("path");
module.exports = {
entry: "./src/index.js", // 入口
output: {
path: path.resolve(__dirname, "dist"), // 输出目录
filename: "bundle.[contenthash].js", // 文件名带内容哈希
clean: true, // 每次构建清空 dist
},
module: {
rules: [ /* loader 配置 */ ],
},
plugins: [ /* 插件实例 */ ],
mode: "development", // development | production
devServer: { port: 3000, hot: true },
};npm install -D webpack webpack-cli webpack-dev-server
npx webpack # 按配置构建
npx webpack serve # 启动开发服务器三、常用 loader
loader 在 module.rules 中配置,执行顺序是数组逆序(从右到左):
module: {
rules: [
{ test: /\.js$/, exclude: /node_modules/, use: "babel-loader" },
{ test: /\.css$/, use: ["style-loader", "css-loader"] }, // 先 css-loader 再 style-loader
{ test: /\.scss$/, use: ["style-loader", "css-loader", "sass-loader"] },
{ test: /\.(png|jpg|svg)$/, type: "asset" }, // 资源模块
],
}| loader | 作用 |
|---|---|
babel-loader | 调用 Babel 转译 ES6+ 语法 |
css-loader | 解析 CSS 中的 @import/url(),生成模块 |
style-loader | 把 CSS 以 <style> 标签注入页面(开发环境用) |
sass-loader | 把 SCSS/Sass 编译为 CSS |
vue-loader | 解析 .vue 单文件组件(配合 Vue 插件) |
file-loader | 把资源输出为独立文件并返回 URL(已可被 asset 模块替代) |
asset 模块 | 内置资源处理:asset/resource、asset/inline、asset(自动选择) |
CSS 提取的配置差异:生产环境通常用 MiniCssExtractPlugin 抽出独立 CSS 文件,而非 style-loader 的运行时注入:
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
module.exports = {
module: {
rules: [
{
test: /\.css$/,
use: [MiniCssExtractPlugin.loader, "css-loader"], // 生产:提取文件
},
],
},
plugins: [new MiniCssExtractPlugin({ filename: "css/[name].[contenthash].css" })],
};四、常用 plugin
const HtmlWebpackPlugin = require("html-webpack-plugin");
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const CopyWebpackPlugin = require("copy-webpack-plugin");
const { DefinePlugin } = require("webpack");
module.exports = {
plugins: [
new HtmlWebpackPlugin({ template: "./index.html" }), // 生成 HTML 并自动注入 bundle
new CopyWebpackPlugin({ patterns: [{ from: "public", to: "public" }] }), // 复制静态资源
new DefinePlugin({ // 注入全局常量
__DEV__: JSON.stringify(true),
}),
],
};| plugin | 作用 |
|---|---|
HtmlWebpackPlugin | 生成 HTML,自动注入带哈希的 JS/CSS 标签 |
MiniCssExtractPlugin | 把 CSS 提取为独立文件(生产用) |
CopyWebpackPlugin | 复制无需处理的静态文件(favicon 等) |
DefinePlugin | 编译期替换全局常量,配合环境变量使用 |
CleanWebpackPlugin | 构建前清空输出目录(Webpack 5 已内置 output.clean) |
五、代码分割
5.1 为什么需要
单 bundle 体积大 → 首屏下载慢。代码分割把代码拆成多个 chunk,按需加载。
5.2 三种方式
1. 动态 import() 触发懒加载(最常用):
// 路由级懒加载
const About = () => import("./pages/About.vue");
button.onclick = async () => {
const { showChart } = await import("./chart.js"); // 独立 chunk,用时才下载
showChart();
};2. SplitChunks 抽公共代码:
module.exports = {
optimization: {
splitChunks: {
chunks: "all", // 对所有 chunk 生效
cacheGroups: {
vendors: {
test: /[\\/]node_modules[\\/]/,
name: "vendors", // 第三方库打成独立 chunk,利用浏览器缓存
priority: 10,
},
},
},
},
};3. 多入口:
entry: { main: "./src/main.js", admin: "./src/admin.js" },5.3 缓存与哈希
文件名使用内容哈希,内容不变则文件名不变,浏览器可长期缓存:
output: { filename: "[name].[contenthash:8].js" }六、Tree Shaking 原理与条件
Tree Shaking(摇树优化) 删除未被引用的导出,减小打包体积。
6.1 原理
基于 ESM 的静态分析:Webpack 解析依赖图时能确定每个导出的使用情况,未被使用的导出标记为"死代码",交给压缩工具(Terser)删除。
// math.js
export function add(a, b) { return a + b; }
export function sub(a, b) { return a - b; } // 未被引用 → 被摇掉
// main.js
import { add } from "./math.js";
console.log(add(1, 2));6.2 生效条件
| 条件 | 说明 |
|---|---|
| 必须用 ESM 语法 | import/export,CommonJS 无法静态分析 |
| 生产模式开启 | mode: "production" 内置 Terser 压缩 |
| 模块无副作用 | 无副作用的包需在 package.json 声明 "sideEffects": false |
| 未被引用 | 被引用的代码即使"没用上"也不会删 |
{
"sideEffects": ["*.css"] // 除 CSS 外均可摇树;CSS 有副作用必须保留
}如果依赖包没有 sideEffects 声明,Webpack 会保守地保留它的所有代码。
七、HMR 热更新原理
HMR(Hot Module Replacement) 在开发时只替换发生变化的模块,不刷新整个页面,保留组件状态。
修改代码
→ 文件监听触发编译
→ 生成增量更新(含新模块代码的 JSON)
→ WebSocket 通知浏览器(hot)
→ 模块对应 accept 回调执行,替换旧模块// 代码侧:模块需要"接受"自身的 HMR
if (module.hot) {
module.hot.accept("./component.js", () => {
// 替换后的重新渲染逻辑
});
}Vue/React 的 loader 已经自动处理了 accept 逻辑,业务代码无需手写。关键点:
- 开发服务器与浏览器之间通过 WebSocket 通信;
- 编译发生在内存中(
devServer默认不落盘); - HMR 失败时自动降级为整页刷新。
八、环境区分与性能优化
8.1 环境区分
用 webpack-merge 合并公共配置:
// webpack.common.js
const common = { /* 公共配置 */ };
// webpack.dev.js
const { merge } = require("webpack-merge");
module.exports = merge(common, { mode: "development", devtool: "eval-source-map" });
// webpack.prod.js
module.exports = merge(common, { mode: "production", devtool: "source-map" });{
"scripts": {
"dev": "webpack serve --config webpack.dev.js",
"build": "webpack --config webpack.prod.js"
}
}8.2 性能优化手段
| 手段 | 原理 | 收益 |
|---|---|---|
| 持久化缓存 | cache: { type: "filesystem" } 缓存构建中间结果 | 二次构建大幅提速 |
thread-loader | 多进程并行跑 loader | 大项目构建提速 |
| DllPlugin | 把稳定依赖预先打包成 DLL | 已逐步被持久化缓存替代 |
| 压缩 | TerserPlugin 压缩 JS、css-minimizer 压缩 CSS | 减小产物体积 |
| 缩小解析范围 | resolve.alias、exclude: /node_modules/ | 减少无用解析 |
module.exports = {
cache: { type: "filesystem" }, // Webpack 5 持久化缓存
module: {
rules: [
{ test: /\.js$/, exclude: /node_modules/, use: ["thread-loader", "babel-loader"] },
],
},
optimization: {
minimize: true,
minimizer: [new TerserPlugin()],
},
};九、打包产物分析
| 工具 | 用途 |
|---|---|
webpack-bundle-analyzer | 可视化 bundle 组成,找出体积大头 |
webpack-bundle-size-analyzer | 命令行报告各模块体积 |
--profile --json 输出 | 导出 stats.json 分析构建耗时 |
npx webpack --profile --json > stats.json// 分析插件(可只在 build 时引入)
const BundleAnalyzerPlugin = require("webpack-bundle-analyzer").BundleAnalyzerPlugin;
plugins: [process.env.ANALYZE && new BundleAnalyzerPlugin()],常见优化结论:体积大头往往是 moment、lodash 等整包引入,改用 lodash-es + Tree Shaking、按需引入 UI 库,能立刻见效。
十、loader 与 plugin 对比
| 维度 | loader | plugin |
|---|---|---|
| 处理对象 | 单个文件(资源转换) | 整个构建流程 |
| 时机 | 模块加载阶段 | 构建生命周期各钩子 |
| 形式 | 函数,输入输出模块内容 | 类,含 apply 方法 |
| 示例 | babel-loader、css-loader | HtmlWebpackPlugin、DefinePlugin |
Webpack 的工作方式可以概括为:loader 把所有东西变成模块,plugin 把模块变成产物。掌握了这两层,再深入生命周期钩子、模块联邦等高级主题就有了坚实的基础。