Webpack构建实战
# 02.Webpack构建实战
掌握 Webpack 的核心心智模型——从"依赖图"到"产物",理解 Loader 和 Plugin 的设计思想与 HMR 原理。
# 1. 案例引入:一个 Vue 项目为什么需要 Webpack?
src/
main.ts # 入口
App.vue # Vue 组件
components/
BaseButton.vue
UserCard.vue
styles/
global.scss # SCSS 预处理器
assets/
logo.svg # SVG 图标
bg.png # 大图
# 浏览器能运行这些吗?
# main.ts → 需要编译 TS
# *.vue → 需要编译 SFC(单文件组件)
# global.scss → 需要编译 SCSS → CSS
# logo.svg / bg.png → 需要转为 URL 或 base64
# import { ref } → 需要解析 vue 模块路径
Webpack 的职责:将 100+ 个异构文件,构建为浏览器能完美运行的 3~5 个静态文件。
# 2. Webpack 核心概念
# 2.1 依赖图——一切从这里开始
Webpack 从 Entry 开始,递归构建模块依赖关系:
main.ts ─┬── App.vue ─┬── BaseButton.vue
│ ├── UserCard.vue
│ └── global.scss → _variables.scss
├── vue (node_modules)
├── vue-router (node_modules)
└── utils/format.ts → dayjs (node_modules)
这张"依赖图"告诉 Webpack:
- 哪些模块需要被打包(所有被引用的模块)
- 模块顺序是什么(入口→第一层→第二层→...)
- 哪些可以拆分成单独文件(代码分割)
# 2.2 四大核心配置
// webpack.config.js
module.exports = {
// 1. Entry:告诉 Webpack 从哪里开始构建
entry: './src/main.ts',
// 2. Output:产物输出到哪里
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'js/[name].[contenthash:8].js', // 文件名 + 哈希
clean: true // 清除上次构建
},
// 3. Loader:处理非 JS 文件(TS/Vue/SCSS/图片)
module: {
rules: [
{ test: /\.ts$/, use: 'ts-loader' }, // TS → JS
{ test: /\.vue$/, use: 'vue-loader' }, // Vue SFC
{ test: /\.scss$/, use: ['style-loader','css-loader','sass-loader'] },
{ test: /\.(png|svg)$/, type: 'asset' }, // 图片资源
]
},
// 4. Plugin:扩展 Webpack 构建能力(压缩/提取CSS/生成HTML/热更新)
plugins: [
new HtmlWebpackPlugin({ template: './index.html' }),
new MiniCssExtractPlugin({ filename: 'css/[name].[hash].css' })
]
};
一张图看懂 Loader vs Plugin:
Loader:转换器(一对一或链式)
输入 A 类型文件 → Loader 转换 → 输出 B 类型文件
例:scss → sass-loader → css-loader → style-loader → DOM <style>
Plugin:增强器(全局)
在构建各阶段注入逻辑——开始、结束、编译中、输出前……
例:压缩 JS、生成 HTML、提取 CSS、复制静态资源
# 3. Loader 深入
# 3.1 链式调用原理
.scss 文件处理流程:
1. sass-loader → 将 SCSS 编译为 CSS 字符串
2. css-loader → 解析 CSS 中的 @import / url(),返回 CSS 模块
3. style-loader → 将 CSS 注入 DOM(<style> 标签)
或 MiniCssExtractPlugin.loader → 提取为独立 .css 文件
链式规则:从后往前执行(右→左,或下→上)
use: ['style-loader', 'css-loader', 'sass-loader']
↑ 最后执行 ↑ ↑ 先执行
# 3.2 常用 Loader 速查
| Loader | 作用 | 配置示例 |
|---|---|---|
ts-loader | TS→JS(使用 tsc 编译) | use: 'ts-loader' |
babel-loader | JS 语法降级(不检查类型) | use: { loader: 'babel-loader', options: { presets: ['@babel/preset-env'] } } |
vue-loader | Vue SFC 编译 | 配合 VueLoaderPlugin |
css-loader | 解析 CSS import/url | options: { modules: true } 开启 CSS Modules |
sass-loader | SCSS/SASS → CSS | 需要 dart-sass 或 node-sass |
postcss-loader | Autoprefixer/Tailwind 等 | postcss.config.js |
asset(内置) | 图片/字体复制或内联 | type: 'asset', parser: { dataUrlCondition: { maxSize: 8*1024 } } |
# 3.3 核心实战配置
// 完整的 module.rules 配置
module: {
rules: [
// TypeScript
{
test: /\.tsx?$/,
exclude: /node_modules/,
use: {
loader: 'ts-loader',
options: {
transpileOnly: true, // 仅转译不检查(快),配合 fork-ts-checker-plugin
appendTsSuffixTo: [/\.vue$/]
}
}
},
// Vue SFC
{
test: /\.vue$/,
use: 'vue-loader'
},
// SCSS
{
test: /\.scss$/,
use: [
isDev ? 'style-loader' : MiniCssExtractPlugin.loader,
'css-loader',
{
loader: 'sass-loader',
options: {
additionalData: `@import "@/styles/variables.scss";`
}
}
]
},
// 图片处理
{
test: /\.(png|jpe?g|gif|svg|webp)$/i,
type: 'asset', // Webpack 5 内置 Asset Module
parser: {
dataUrlCondition: {
maxSize: 8 * 1024 // < 8KB → base64 内联
}
}
}
]
}
# 4. Plugin 深入
# 4.1 常用 Plugin 速查
| Plugin | 作用 |
|---|---|
HtmlWebpackPlugin | 生成 HTML,自动注入打包后的 JS/CSS |
MiniCssExtractPlugin | 提取 CSS 为独立文件 |
DefinePlugin | 注入编译时常量(如 process.env.NODE_ENV) |
CopyWebpackPlugin | 复制静态资源到 dist |
ForkTsCheckerWebpackPlugin | 独立进程做 TS 类型检查(不阻塞构建) |
TerserPlugin | JS 压缩(Webpack 5 内置) |
CssMinimizerPlugin | CSS 压缩 |
# 4.2 核心配置实战
plugins: [
// 生成 HTML
new HtmlWebpackPlugin({
template: 'public/index.html',
favicon: 'public/favicon.ico',
inject: true // 自动注入 script/css
}),
// CSS 提取
new MiniCssExtractPlugin({
filename: 'css/[name].[contenthash:8].css',
chunkFilename: 'css/[id].[contenthash:8].css'
}),
// 编译时环境变量
new webpack.DefinePlugin({
__VUE_OPTIONS_API__: JSON.stringify(true),
__VUE_PROD_DEVTOOLS__: JSON.stringify(false)
})
]
# 5. 代码分割(Code Splitting)
# 5.1 三种分割方式
optimization: {
splitChunks: {
chunks: 'all', // 所有类型的 chunk 都优化
cacheGroups: {
// 方式 1:Vue 全家桶独立 chunk
vueVendor: {
test: /[\\/]node_modules[\\/](vue|vue-router|pinia)[\\/]/,
name: 'vue-vendor',
priority: 10
},
// 方式 2:其他第三方库
vendor: {
test: /[\\/]node_modules[\\/]/,
name: 'vendor',
priority: 5
},
// 方式 3:公共模块(被 3+ chunks 引用的模块提取)
common: {
minChunks: 3,
name: 'common',
priority: 1,
reuseExistingChunk: true
}
}
}
}
代码分割的效果:
原来:app.js (800KB) ← 修改一行代码,用户重新加载 800KB
分割后:
vendor.js (300KB) ← 第三方库变化少,强缓存
vue-vendor.js (150KB) ← Vue 版本稳定,强缓存
common.js (100KB) ← 公共模块
app.js (250KB) ← 业务逻辑经常变
# 修改业务代码,用户只需重新加载 250KB
# 6. HMR 热更新原理
# 6.1 工作原理
HMR 完整流程:
1. Webpack Dev Server 启动 WebSocket 服务
2. 浏览器加载页面时,自动注入 HMR Runtime(小型 WebSocket 客户端)
3. 开发者修改代码 → Webpack 重新编译 → 计算变化文件的最小依赖图
4. WebSocket 推送变化给浏览器:
{ type: "hash", hash: "abc123" } ← 新构建的 hash
{ type: "ok" } ← 构建完成
{ type: "update", modules: {...} } ← 变化的模块
5. HMR Runtime 替换变化模块(不刷新页面)
├── CSS 变化 → 只替换 <style> 内容(状态保留)
├── JS 变化 → 替换模块 + 重新执行(Vue 组件保留状态)
└── 无法 HM 的模块 → 降级为整页刷新
# 6.2 配置
devServer: {
port: 3000,
hot: true, // 开启 HMR
open: true, // 自动打开浏览器
historyApiFallback: true, // SPA History 模式兜底
proxy: { // API 代理
'/api': 'http://localhost:8080'
}
}
# 7. Tree Shaking 与打包优化
# 7.1 Tree Shaking 原理
// math.js
export const add = (a, b) => a + b;
export const sub = (a, b) => a - b; // 未使用
// app.js
import { add } from './math'; // 只引入 add
// 构建结果:
// sub 函数不会出现在最终产物中 ← Tree Shaking 的效果
// 前提条件:
// 1. 使用 ES Module(静态导入)而非 CommonJS
// 2. 生产模式(mode: 'production')
// 3. package.json 的 sideEffects: false
# 7.2 完整优化配置
optimization: {
minimize: true,
minimizer: [
new TerserPlugin({
terserOptions: {
compress: {
drop_console: true, // 删除 console
drop_debugger: true // 删除 debugger
}
}
}),
new CssMinimizerPlugin() // CSS 压缩
],
splitChunks: { /* 如上述 5.1 配置 */ },
runtimeChunk: 'single' // Runtime 代码单独提取
}
# 8. 缓存策略:让二次构建飞起来
module.exports = {
cache: {
type: 'filesystem', // 将缓存写入磁盘(node_modules/.cache/webpack)
// 下次构建直接从缓存读取——大型项目构建时间减少 50%+
},
output: {
filename: 'js/[name].[contenthash:8].js', // 文件内容 hash
}
};
// 缓存分层策略:
// 1. vendor + vue-vendor:很少变化 → 文件名带 contenthash → 用户强缓存
// 2. common chunk:较少变化 → 中等缓存时间
// 3. app chunk:每次可能不同 → 短缓存
# 9. 速查表
| 概念 | 职责 | 示例 |
|---|---|---|
| Entry | 入口起点 | entry: './src/main.ts' |
| Output | 输出配置 | filename: '[name].[hash].js' |
| Loader | 文件转换 | ts-loader/vue-loader/css-loader |
| Plugin | 构建增强 | HtmlWebpackPlugin/MiniCssExtractPlugin |
| splitChunks | 代码分割 | vue-vendor/vendor/common |
| HMR | 热更新 | 局部替换模块,保留状态 |
| TreeShaking | 摇树优化 | 移除未使用代码 |
| filesystem cache | 磁盘缓存 | 二次构建快 50%+ |
Webpack 一句话:从入口出发,递归解析依赖图,通过 Loader 链处理异构模块,通过 Plugin 注入构建流程,最终输出为浏览器可用的静态资源。
下一篇:03.Vite构建实战
上次更新: 2026/06/24, 14:17:21