Webpack
一、核心概念(Entry、Output、Loader、Plugin)
1. Webpack 是什么?它的核心概念有哪些?
定义
Webpack 是一个现代 JavaScript 应用程序的静态模块打包器(Module Bundler)。它能够分析项目结构,找到 JavaScript 模块以及浏览器不能直接运行的扩展语言(如 TypeScript、SCSS 等),并将其转换和打包为浏览器可用的格式。
核心概念
Webpack 有四个核心概念:
| 概念 | 说明 |
|---|---|
| Entry(入口) | 指示 Webpack 从哪个文件开始打包,是构建依赖图的起点 |
| Output(输出) | 指示 Webpack 将打包后的文件输出到哪里,以及如何命名 |
| Loader | 转换非 JavaScript 模块,让 Webpack 能够处理各种类型的文件 |
| Plugin(插件) | 执行范围更广的任务,包括打包优化、资源管理、环境变量注入等 |
原理
Webpack 的工作原理可以概括为以下几个步骤:
- 初始化参数:读取配置文件(webpack.config.js)和命令行参数
- 开始编译:根据参数初始化 Compiler 对象,加载所有插件
- 确定入口:根据 Entry 配置找到所有入口文件
- 编译模块:从入口文件出发,递归构建依赖关系图
- 完成编译:所有模块编译完成后,确定输出的 Chunk
- 输出资源:根据 Output 配置将 Chunk 输出到文件系统
配置示例
// webpack.config.js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
// 入口
entry: './src/index.js',
// 输出
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'bundle.[contenthash].js',
},
// Loader
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
use: 'babel-loader'
},
{
test: /\.css$/,
use: ['style-loader', 'css-loader']
}
]
},
// Plugin
plugins: [
new HtmlWebpackPlugin({
template: './public/index.html'
})
]
};
常见误区
- 误区 1:Webpack 只能处理 JavaScript 文件。实际上通过 Loader,Webpack 可以处理任何类型的文件
- 误区 2:Loader 和 Plugin 功能相同。Loader 用于模块转换,Plugin 用于扩展打包流程
- 误区 3:Webpack 只适用于大型项目。即使是小型项目,Webpack 也能提供模块化和优化的能力
2. Entry(入口)的作用和配置
定义
Entry 是 Webpack 构建依赖关系图的起点。Webpack 从 Entry 开始,递归分析所有依赖的模块,最终将所有模块打包成 Bundle。
配置方式
单入口(字符串形式):
module.exports = {
entry: './src/index.js'
};
单入口(数组形式):
module.exports = {
entry: ['./src/polyfill.js', './src/index.js']
};
多入口(对象形式):
module.exports = {
entry: {
app: './src/app.js',
admin: './src/admin.js',
vendor: ['react', 'react-dom']
}
};
不同入口类型的区别
| 类型 | 语法 | 适用场景 |
|---|---|---|
| 单入口(字符串) | './src/index.js' | 单页面应用(SPA) |
| 单入口(数组) | ['./src/polyfill.js', './src/index.js'] | 需要在入口前注入 polyfill |
| 多入口(对象) | { app: './src/app.js', admin: './src/admin.js' } | 多页面应用(MPA) |
原理
当 Webpack 启动时,它会:
- 读取 Entry 配置
- 解析入口文件的绝对路径
- 读取入口文件内容
- 分析 AST(抽象语法树),找出所有的依赖模块(import/require)
- 递归处理每个依赖模块,构建完整的依赖关系图
常见误区
- 误区 1:入口文件只能有一个。实际上可以配置多个入口
- 误区 2:Entry 必须是 JS 文件。Entry 可以是任何 Webpack 能解析的文件类型
3. Output(输出)的配置
定义
Output 配置指示 Webpack 如何输出打包后的文件,包括输出路径、文件名、公共路径等。
常用配置项
const path = require('path');
module.exports = {
output: {
// 输出目录(必须是绝对路径)
path: path.resolve(__dirname, 'dist'),
// 输出文件名
filename: 'bundle.js',
// 按需加载的 Chunk 文件名
chunkFilename: '[name].[contenthash].chunk.js',
// 公共资源路径
publicPath: '/',
// 清理输出目录
clean: true,
// 跨域加载资源
crossOriginLoading: 'anonymous'
}
};
文件名占位符
| 占位符 | 说明 | 示例 |
|---|---|---|
[name] | Chunk 名称 | app.js |
[hash] | 编译哈希值 | bundle.a1b2c3d4.js |
[contenthash] | 基于文件内容的哈希值 | bundle.e5f6g7h8.js |
[id] | Chunk ID | bundle.0.js |
[fullhash] | 完整编译哈希 | bundle.x1y2z3w4.js |
多入口配置示例
module.exports = {
entry: {
app: './src/app.js',
admin: './src/admin.js'
},
output: {
filename: '[name].[contenthash:8].js',
path: path.resolve(__dirname, 'dist')
}
};
// 输出: dist/app.a1b2c3d4.js, dist/admin.e5f6g7h8.js
常见误区
- 误区 1:使用
[hash]进行缓存控制。应该使用[contenthash],因为[hash]是全局的,任何一个文件变化都会导致所有文件名变化 - 误区 2:output.path 使用相对路径。必须使用绝对路径
4. Loader 的作用和使用
定义
Loader 是 Webpack 的模块转换器,它能够将各种类型的文件转换为 Webpack 能够处理的有效模块。本质上,Loader 就是一个函数,接收源文件内容作为参数,返回转换后的结果。
原理
Loader 的工作流程:
- Webpack 在解析模块时,根据配置的 rules 匹配文件
- 匹配成功后,调用对应的 Loader 函数
- Loader 接收文件内容作为输入,进行处理后返回新的内容
- 如果有多个 Loader,按照配置的顺序依次执行
Loader 配置方式
方式一:use(单个):
module.exports = {
module: {
rules: [
{
test: /\.js$/,
use: 'babel-loader'
}
]
}
};
方式二:use(多个):
module.exports = {
module: {
rules: [
{
test: /\.css$/,
use: ['style-loader', 'css-loader']
}
]
}
};
方式三:inline(内联):
import styles from 'css-loader!./style.css';
方式四:CLI:
webpack --module-bind 'css=style-loader!css-loader'
Loader 的执行顺序
Loader 的执行顺序是从右到左,从下到上
示例:
use: ['style-loader', 'css-loader', 'postcss-loader']
执行顺序:
1. postcss-loader(最先执行)
2. css-loader
3. style-loader(最后执行)
常见 Loader
| Loader | 作用 |
|---|---|
| babel-loader | 将 ES6+ 转换为 ES5 |
| css-loader | 解析 CSS 文件中的 @import 和 url() |
| style-loader | 将 CSS 注入到 DOM 中 |
| sass-loader | 将 Sass/SCSS 编译为 CSS |
| less-loader | 将 Less 编译为 CSS |
| postcss-loader | 使用 PostCSS 处理 CSS |
| ts-loader | 将 TypeScript 转换为 JavaScript |
| file-loader | 将文件发送到输出目录 |
| url-loader | 将文件转换为 base64 Data URL |
| eslint-loader | 使用 ESLint 检查 JavaScript 代码 |
自定义 Loader 示例
// my-loader.js
module.exports = function(source) {
// source 是源文件内容
// 进行转换处理
const result = source.replace(/foo/g, 'bar');
// 返回转换后的结果
return result;
};
// 使用
module.exports = {
module: {
rules: [
{
test: /\.js$/,
use: path.resolve('my-loader.js')
}
]
}
};
5. Plugin 的作用和使用
定义
Plugin(插件)用于扩展 Webpack 的功能。与 Loader 不同,Plugin 能够执行更广泛的任务,包括打包优化、资源管理、环境变量注入等。Plugin 基于 Tapable 事件流机制,在 Webpack 生命周期的不同阶段执行特定操作。
原理
Webpack Plugin 的工作原理:
- Plugin 是一个具有
apply方法的 JavaScript 对象 apply方法会被 Webpack compiler 调用- 在
apply方法中,通过 compiler 对象挂载各种钩子(Hook) - 当 Webpack 执行到对应生命周期时,会触发这些钩子
Plugin 配置
const HtmlWebpackPlugin = require('html-webpack-plugin');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
module.exports = {
plugins: [
new HtmlWebpackPlugin({
template: './public/index.html',
filename: 'index.html',
minify: {
collapseWhitespace: true,
removeComments: true
}
}),
new CleanWebpackPlugin()
]
};
自定义 Plugin 示例
class MyWebpackPlugin {
apply(compiler) {
// compilation 钩子在每次构建时触发
compiler.hooks.emit.tapAsync('MyWebpackPlugin', (compilation, callback) => {
// 在输出目录添加一个文件
compilation.assets['info.txt'] = {
source: () => '构建信息',
size: () => '构建信息'.length
};
callback();
});
// done 钩子在构建完成后触发
compiler.hooks.done.tap('MyWebpackPlugin', (stats) => {
console.log('构建完成!');
});
}
}
module.exports = MyWebpackPlugin;
Webpack 生命周期钩子
| 钩子 | 说明 |
|---|---|
entryOption | 入口配置处理完成后 |
afterPlugins | 插件加载完成后 |
run | 开始读取 records |
compile | 开始编译 |
compilation | 创建 compilation 对象时 |
emit | 输出资源到 output 目录之前 |
afterEmit | 输出资源到 output 目录之后 |
done | 构建完成 |
6. Loader 与 Plugin 的区别
对比分析
| 维度 | Loader | Plugin |
|---|---|---|
| 定义 | 模块转换器,用于转换文件内容 | 扩展程序,用于扩展 Webpack 功能 |
| 功能 | 将非 JS 文件转换为 Webpack 可处理的模块 | 执行打包优化、资源管理、环境变量注入等 |
| 本质 | 一个函数,接收源文件,返回转换后的内容 | 一个类,具有 apply 方法 |
| 执行时机 | 在模块加载时执行 | 在 Webpack 生命周期的不同阶段执行 |
| 配置方式 | module.rules 中配置 | plugins 数组中配置 |
| 执行顺序 | 从右到左,从下到上 | 按注册顺序执行 |
| 工作范围 | 针对单个文件 | 针对整个构建流程 |
| 典型场景 | 转译 JS、编译 CSS、处理图片等 | 生成 HTML、清理目录、压缩代码等 |
选择策略
- 使用 Loader:当你需要将一种文件格式转换为另一种格式时
- 使用 Plugin:当你需要影响整个构建流程或添加额外功能时
示例对比
module.exports = {
module: {
rules: [
// Loader:处理单个文件
{
test: /\.css$/,
use: ['style-loader', 'css-loader'] // 转换 CSS 文件
}
]
},
plugins: [
// Plugin:影响整个构建流程
new HtmlWebpackPlugin() // 生成 HTML 文件并注入打包后的资源
]
};
二、模块打包原理
7. Webpack 打包原理
定义
Webpack 打包原理是指 Webpack 如何从入口文件开始,分析模块依赖关系,并将所有模块合并为一个或多个 Bundle 的过程。
原理分析
Webpack 打包的核心流程:
-
初始化阶段:
- 读取配置文件和命令行参数
- 初始化 Compiler 对象
- 加载所有插件
- 创建 compilation 对象
-
构建阶段:
- 从 Entry 开始,解析入口文件
- 使用 Loader 处理不同类型的文件
- 分析 AST(抽象语法树),找出所有依赖(import/require)
- 递归处理依赖,构建完整的依赖关系图(Module Graph)
-
优化阶段:
- Tree Shaking 消除无用代码
- 代码分割(Code Splitting)
- 模块合并和压缩
-
输出阶段:
- 将优化后的模块组装成 Chunk
- 将 Chunk 转换为最终输出的 Bundle
- 写入文件系统
源码解读
// Webpack 打包的核心逻辑(简化版)
const webpack = require('webpack');
function runWebpack(config) {
// 1. 初始化 Compiler
const compiler = webpack(config);
// 2. 开始编译
compiler.run((err, stats) => {
if (err) {
console.error(err);
return;
}
console.log('构建完成');
});
}
打包后的代码结构
// Webpack 打包后的简化结构
(function(modules) {
// 模块缓存
const installedModules = {};
// require 函数
function __webpack_require__(moduleId) {
if (installedModules[moduleId]) {
return installedModules[moduleId].exports;
}
const module = installedModules[moduleId] = {
i: moduleId,
l: false,
exports: {}
};
modules[moduleId].call(
module.exports,
module,
module.exports,
__webpack_require__
);
module.l = true;
return module.exports;
}
// 加载入口模块
return __webpack_require__(__webpack_require__.s = "./src/index.js");
})({
"./src/index.js": (function(module, exports, __webpack_require__) {
const utils = __webpack_require__("./src/utils.js");
console.log(utils.add(1, 2));
}),
"./src/utils.js": (function(module, exports) {
exports.add = (a, b) => a + b;
})
});
8. Module、Chunk、Bundle 的区别
定义
| 概念 | 定义 |
|---|---|
| Module(模块) | 源代码中的单个文件,如一个 JS 文件、CSS 文件、图片文件等 |
| Chunk(代码块) | Webpack 在构建过程中生成的代码块,由多个 Module 组成 |
| Bundle(打包文件) | 最终输出的文件,由一个或多个 Chunk 生成 |
关系说明
Module → Chunk → Bundle
1. Module 是源代码中的单个文件
2. Webpack 将多个 Module 组合成一个或多个 Chunk
3. 每个 Chunk 最终生成一个或多个 Bundle
详细解释
Module:
- 就是项目中的各个文件
- Webpack 支持多种 Module 格式:ES Modules、CommonJS、AMD
- 通过 Loader,任何文件都可以成为 Module
Chunk:
- 是 Webpack 打包过程中的中间产物
- 一个 Entry 生成一个 Chunk
- 动态导入(import())也会生成额外的 Chunk
- SplitChunksPlugin 可以将公共模块提取为单独的 Chunk
Bundle:
- 是最终输出的文件
- 一个 Chunk 可能生成多个 Bundle(如 JS + Source Map)
- 最终部署到服务器的文件
示例说明
// webpack.config.js
module.exports = {
entry: {
app: './src/app.js',
vendor: ['react', 'react-dom']
},
output: {
filename: '[name].bundle.js'
}
};
输入:
- src/app.js (Module)
- src/utils.js (Module)
- node_modules/react/index.js (Module)
- node_modules/react-dom/index.js (Module)
Webpack 处理:
- app Chunk: app.js + utils.js
- vendor Chunk: react + react-dom
输出:
- app.bundle.js (Bundle)
- vendor.bundle.js (Bundle)
9. 模块依赖图(Dependency Graph)
定义
模块依赖图是 Webpack 用来记录模块之间依赖关系的数据结构。Webpack 从入口文件开始,递归分析所有依赖,最终构建出一个完整的依赖图。
原理
依赖图构建流程:
1. 从 Entry 开始
2. 解析文件内容,生成 AST
3. 分析 AST 中的导入语句(import/require)
4. 找到被导入的模块
5. 递归处理每个模块
6. 构建完整的依赖关系图
示例:
index.js
├── app.js
│ ├── utils.js
│ └── api.js
└── styles.css
└── variables.css
依赖类型
Webpack 支持的模块导入方式:
| 类型 | 语法 | 说明 |
|---|---|---|
| ES Modules | import foo from './foo' | 静态导入,编译时确定 |
| CommonJS | require('./foo') | 动态导入,运行时确定 |
| AMD | require(['./foo'], callback) | 异步模块定义 |
| 动态导入 | import('./foo') | 返回 Promise,用于代码分割 |
三、代码分割与懒加载
10. 代码分割(Code Splitting)
定义
代码分割是 Webpack 最重要的特性之一,它允许将代码分割成多个 Bundle,按需加载或并行加载,从而减小首屏加载体积,提高页面加载速度。
为什么需要代码分割?
- 首屏加载优化:只加载当前页面需要的代码
- 缓存优化:将不常变化的代码(如第三方库)单独打包
- 并行加载:多个小文件可以并行下载
代码分割策略
策略一:多入口分割
module.exports = {
entry: {
app: './src/app.js',
vendor: ['react', 'react-dom']
},
output: {
filename: '[name].[contenthash].js'
}
};
策略二:动态导入(推荐)
// 使用 import() 动态导入
button.addEventListener('click', async () => {
const { default: Modal } = await import('./Modal');
modal.show();
});
// React 路由懒加载
const Home = lazy(() => import('./Home'));
const About = lazy(() => import('./About'));
策略三:SplitChunksPlugin
module.exports = {
optimization: {
splitChunks: {
chunks: 'all', // all | async | initial
minSize: 20000, // 最小体积
maxSize: 0, // 最大体积
minChunks: 1, // 最小引用次数
maxAsyncRequests: 30,
maxInitialRequests: 30,
cacheGroups: {
vendor: {
test: /[\\/]node_modules[\\/]/,
name: 'vendors',
priority: 10
},
common: {
minChunks: 2,
priority: 5,
reuseExistingChunk: true
}
}
}
}
};
SplitChunksPlugin 配置详解
| 配置项 | 说明 | 默认值 |
|---|---|---|
chunks | 选择哪些 Chunk 进行分割 | async |
minSize | 生成 Chunk 的最小体积 | 20000 |
maxSize | 生成 Chunk 的最大体积 | 0 |
minChunks | 被引用次数 | 1 |
maxAsyncRequests | 按需加载时的最大并行请求数 | 30 |
maxInitialRequests | 入口点的最大并行请求数 | 30 |
cacheGroups | 缓存组,定义分割规则 | - |
11. 懒加载(Lazy Loading)
定义
懒加载是一种按需加载资源的策略。只有当用户实际需要某个模块时,才去加载它,而不是一开始就加载所有代码。
实现方式
方式一:动态 import()
// 基础用法
const loadModule = async () => {
const module = await import('./module.js');
module.doSomething();
};
// 条件加载
if (needFeature) {
import('./feature.js').then(module => {
module.init();
});
}
方式二:React.lazy
import { lazy, Suspense } from 'react';
const LazyComponent = lazy(() => import('./LazyComponent'));
function App() {
return (
<Suspense fallback={<div>Loading...</div>}>
<LazyComponent />
</Suspense>
);
}
方式三:Vue 路由懒加载
const routes = [
{
path: '/home',
component: () => import('./Home.vue')
},
{
path: '/about',
component: () => import('./About.vue')
}
];
原理
动态 import() 的实现原理:
- Webpack 在编译时遇到
import(),会将其视为一个代码分割点 - 将
import()引用的模块及其依赖单独打包成一个 Chunk - 运行时,
import()返回一个 Promise - 当 Promise 解析时,通过 JSONP 或 Fetch 加载对应的 Chunk
- 加载完成后执行模块代码
最佳实践
- 路由级别懒加载:按页面分割代码
- 组件级别懒加载:对不首屏显示的组件进行懒加载
- 事件触发的懒加载:对需要用户交互才展示的模块进行懒加载
- 避免过度分割:过多的小文件会增加 HTTP 请求数量
12. 预加载(Preload)和 Prefetch
定义
| 类型 | 说明 | 适用场景 |
|---|---|---|
| Preload | 预加载当前页面可能需要的资源,优先级高 | 当前页面马上会用到的资源 |
| Prefetch | 预加载未来页面可能需要的资源,优先级低 | 未来页面可能用到的资源 |
Webpack 实现
Preload:
// webpackPrefetch: false(默认)
// webpackPreload: true
import(/* webpackPreload: true */ './Chart.js');
Prefetch:
import(/* webpackPrefetch: true */ './LoginModal.js');
对比
| 维度 | Preload | Prefetch |
|---|---|---|
| 优先级 | 高(与当前页面资源相同) | 低(浏览器空闲时加载) |
| 加载时机 | 立即加载 | 空闲时加载 |
| 适用场景 | 当前页面需要的资源 | 下一个页面可能需要的资源 |
| 缓存 | 立即缓存 | 空闲时缓存 |
示例
// 预加载:当前页面马上会展示的模态框
import(/* webpackPreload: true */ './CriticalModal.js');
// Prefetch:用户可能会点击的下一个页面
button.addEventListener('mouseenter', () => {
import(/* webpackPrefetch: true */ './NextPage.js');
});
四、Tree Shaking
13. Tree Shaking 的原理
定义
Tree Shaking 是一个术语,通常用于描述移除 JavaScript 上下文中的未引用代码(Dead Code)。它依赖于 ES Modules 的静态结构分析。
原理分析
Tree Shaking 的工作流程:
- ES Modules 静态分析:Webpack 利用 ES Modules 的静态导入导出特性,在编译时确定哪些模块被使用
- 标记未使用代码:通过静态分析,标记未被引用的导出
- Uglify/Terser 压缩:在压缩阶段,移除标记为未使用的代码
必要条件
Tree Shaking 生效需要满足以下条件:
| 条件 | 说明 |
|---|---|
| ES Modules | 必须使用 ES Modules 语法(import/export) |
| production 模式 | 需要在 production 模式下或手动配置优化 |
| sideEffects 配置 | 正确配置 sideEffects 标记无副作用的模块 |
| pure 标记 | 使用 /*#__PURE__*/ 标记纯函数 |
配置示例
// webpack.config.js
module.exports = {
mode: 'production',
optimization: {
usedExports: true, // 标记使用的导出
minimize: true, // 启用压缩
minimizer: [
new TerserPlugin() // 移除未使用代码
]
}
};
// package.json
{
"name": "my-app",
"sideEffects": [
"*.css",
"*.scss"
]
}
代码示例
// math.js
export function add(a, b) {
return a + b;
}
export function subtract(a, b) {
return a - b;
}
// index.js
import { add } from './math.js';
console.log(add(1, 2));
// Tree Shaking 后,subtract 函数会被移除
常见误区
- 误区 1:CommonJS 也支持 Tree Shaking。实际上 CommonJS 是动态的,无法静态分析
- 误区 2:只要用了 ES Modules 就自动生效。还需要配置 optimization 和 sideEffects
- 误区 3:所有未使用的代码都会被移除。有副作用的代码不会被移除
14. sideEffects 的作用
定义
sideEffects 用于告诉 Webpack 哪些模块有副作用(修改全局变量、修改原型、产生输出等),以便 Tree Shaking 时保留这些模块。
副作用的定义
副作用是指函数或模块在执行时,除了返回值之外还对外部环境产生了影响,如:
- 修改全局变量
- 修改内置对象原型
- 发送网络请求
- 操作 DOM
- 引入 CSS 文件
配置方式
方式一:package.json
{
"name": "my-library",
"sideEffects": false
}
{
"name": "my-library",
"sideEffects": [
"./src/polyfill.js",
"*.css",
"*.scss"
]
}
方式二:webpack.config.js
module.exports = {
module: {
rules: [
{
test: /\.js$/,
sideEffects: false
}
]
}
};
示例说明
// polyfill.js(有副作用)
Array.prototype.customMethod = function() {
// 修改 Array 原型
};
// styles.css(有副作用)
// CSS 文件天然有副作用,会修改页面样式
// utils.js(无副作用)
export function add(a, b) {
return a + b;
}
五、热模块替换(HMR)
15. HMR(Hot Module Replacement)原理
定义
HMR(Hot Module Replacement,热模块替换)是 Webpack 提供的最有用的功能之一。它允许在运行时更新各种类型的模块,而无需完全刷新页面。
原理分析
HMR 的工作流程:
1. 启动 Webpack Dev Server
2. 在浏览器和服务器之间建立 WebSocket 连接
3. 文件发生变化
4. Webpack 重新编译修改的模块
5. 通过 WebSocket 通知浏览器
6. 浏览器请求更新的模块(JSONP)
7. 运行时替换旧模块
8. 模块接受更新,页面不刷新
详细流程
服务器端:
- Webpack 监听文件变化
- 文件变化时,重新编译受影响的模块
- 将新模块的 hash 和更新内容发送到内存中
- 通过 WebSocket 通知浏览器有更新
客户端:
- 浏览器接收到更新通知(包含新 hash)
- 通过 AJAX 请求获取更新的 manifest 和 chunk
- 运行时检查模块是否接受更新
- 如果模块配置了
module.hot.accept,执行替换逻辑 - 如果没有配置,向上冒泡到父模块
配置方式
// webpack.config.js
const webpack = require('webpack');
module.exports = {
devServer: {
hot: true, // 启用 HMR
open: true, // 自动打开浏览器
port: 3000,
host: 'localhost'
},
plugins: [
new webpack.HotModuleReplacementPlugin()
]
};
模块接受更新
// index.js
import { add } from './math.js';
console.log(add(1, 2));
if (module.hot) {
module.hot.accept('./math.js', () => {
console.log('math.js 已更新');
// 重新执行更新后的逻辑
});
}
框架集成
React HMR:
// 使用 react-refresh 或 @pmmmwh/react-refresh-webpack-plugin
const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin');
module.exports = {
mode: 'development',
devServer: {
hot: true
},
plugins: [
new ReactRefreshWebpackPlugin()
]
};
Vue HMR:
// Vue 的 vue-loader 自动支持 HMR
// 无需额外配置,修改 .vue 文件会自动更新
16. webpack-dev-server 的使用
定义
webpack-dev-server 是一个小型的 Node.js Express 服务器,用于开发环境提供实时重载和 HMR 功能。
常用配置
module.exports = {
devServer: {
// 服务器配置
port: 3000,
host: 'localhost',
open: true,
// 热更新
hot: true,
liveReload: true,
// 静态文件
static: {
directory: path.join(__dirname, 'public'),
},
// 代理
proxy: {
'/api': {
target: 'http://localhost:3001',
pathRewrite: { '^/api': '' },
changeOrigin: true
}
},
// 路由回退
historyApiFallback: true,
// 压缩
compress: true
}
};
六、性能优化
17. Webpack 性能优化策略
优化方向
Webpack 性能优化主要分为两个方向:
| 方向 | 目标 | 方法 |
|---|---|---|
| 构建速度优化 | 减少编译时间 | 缓存、多线程、缩小范围等 |
| 构建体积优化 | 减小输出文件大小 | Tree Shaking、压缩、代码分割等 |
18. 构建速度优化
方法一:缩小文件搜索范围
module.exports = {
// 指定模块解析路径
resolve: {
modules: [path.resolve('node_modules')],
extensions: ['.js', '.json', '.vue'],
alias: {
'@': path.resolve('src')
}
},
module: {
rules: [
{
test: /\.js$/,
// 排除不需要处理的目录
exclude: /node_modules/,
// 只处理指定目录
include: path.resolve('src'),
use: 'babel-loader'
}
]
}
};
方法二:使用缓存
Webpack 5 内置缓存:
module.exports = {
cache: {
type: 'filesystem', // 使用文件系统缓存
cacheDirectory: path.resolve('.webpack-cache')
}
};
cache-loader(Webpack 4):
module.exports = {
module: {
rules: [
{
test: /\.js$/,
use: ['cache-loader', 'babel-loader']
}
]
}
};
方法三:多线程处理
thread-loader:
module.exports = {
module: {
rules: [
{
test: /\.js$/,
use: [
'thread-loader', // 放在其他 loader 前面
'babel-loader'
]
}
]
}
};
HappyPack(Webpack 4):
const HappyPack = require('happypack');
module.exports = {
module: {
rules: [
{
test: /\.js$/,
use: 'happypack/loader'
}
]
},
plugins: [
new HappyPack({
loaders: ['babel-loader']
})
]
};
方法四:DLL 预编译
DllPlugin 配置:
// webpack.dll.js
const webpack = require('webpack');
module.exports = {
entry: {
vendor: ['react', 'react-dom']
},
output: {
filename: '[name].dll.js',
path: path.resolve('dll'),
library: '[name]_library'
},
plugins: [
new webpack.DllPlugin({
name: '[name]_library',
path: path.resolve('dll/[name]-manifest.json')
})
]
};
DllReferencePlugin 使用:
// webpack.config.js
module.exports = {
plugins: [
new webpack.DllReferencePlugin({
manifest: require('./dll/vendor-manifest.json')
})
]
};
方法五:externals 排除依赖
module.exports = {
externals: {
react: 'React',
'react-dom': 'ReactDOM',
vue: 'Vue'
}
};
通过 CDN 引入这些库,避免打包它们。
19. 构建体积优化
方法一:Tree Shaking
module.exports = {
mode: 'production',
optimization: {
usedExports: true,
minimize: true
}
};
方法二:代码分割
module.exports = {
optimization: {
splitChunks: {
chunks: 'all',
cacheGroups: {
vendor: {
test: /[\\/]node_modules[\\/]/,
name: 'vendors',
chunks: 'all'
}
}
}
}
};
方法三:代码压缩
JavaScript 压缩:
const TerserPlugin = require('terser-webpack-plugin');
module.exports = {
optimization: {
minimizer: [
new TerserPlugin({
parallel: true, // 并行压缩
terserOptions: {
compress: {
drop_console: true, // 移除 console
drop_debugger: true
}
}
})
]
}
};
CSS 压缩:
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin');
module.exports = {
optimization: {
minimizer: [
new CssMinimizerPlugin()
]
}
};
方法四:图片压缩
module.exports = {
module: {
rules: [
{
test: /\.(png|jpg|gif|svg)$/,
type: 'asset',
parser: {
dataUrlCondition: {
maxSize: 8 * 1024 // 8kb 以下转 base64
}
}
}
]
}
};
方法五:Gzip 压缩
const CompressionPlugin = require('compression-webpack-plugin');
module.exports = {
plugins: [
new CompressionPlugin({
algorithm: 'gzip',
test: /\.(js|css|html|svg)$/,
threshold: 10240, // 10kb 以上才压缩
minRatio: 0.8
})
]
};
优化效果对比
| 优化方法 | 优化前 | 优化后 | 效果 |
|---|---|---|---|
| Tree Shaking | 500kb | 350kb | -30% |
| 代码分割 | 500kb | 首屏 200kb | -60% |
| 代码压缩 | 200kb | 100kb | -50% |
| Gzip 压缩 | 100kb | 30kb | -70% |
七、多环境配置
20. 多环境配置方案
定义
多环境配置是指针对不同运行环境(开发、测试、生产)使用不同的 Webpack 配置。
目录结构
webpack/
├── webpack.common.js # 公共配置
├── webpack.dev.js # 开发环境配置
├── webpack.prod.js # 生产环境配置
└── webpack.test.js # 测试环境配置
使用 webpack-merge
// webpack.common.js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
entry: './src/index.js',
output: {
path: path.resolve(__dirname, 'dist'),
filename: '[name].[contenthash].js'
},
plugins: [
new HtmlWebpackPlugin({
template: './public/index.html'
})
],
module: {
rules: [
{
test: /\.js$/,
use: 'babel-loader'
}
]
}
};
// webpack.dev.js
const { merge } = require('webpack-merge');
const common = require('./webpack.common.js');
const webpack = require('webpack');
module.exports = merge(common, {
mode: 'development',
devtool: 'eval-source-map',
devServer: {
hot: true,
open: true,
port: 3000
},
plugins: [
new webpack.HotModuleReplacementPlugin()
]
});
// webpack.prod.js
const { merge } = require('webpack-merge');
const common = require('./webpack.common.js');
const TerserPlugin = require('terser-webpack-plugin');
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin');
module.exports = merge(common, {
mode: 'production',
devtool: 'source-map',
optimization: {
minimize: true,
minimizer: [
new TerserPlugin(),
new CssMinimizerPlugin()
]
}
});
package.json 配置
{
"scripts": {
"dev": "webpack serve --config webpack/webpack.dev.js",
"build": "webpack --config webpack/webpack.prod.js",
"build:test": "webpack --config webpack/webpack.test.js"
}
}
21. 环境变量配置
使用 DefinePlugin
const webpack = require('webpack');
module.exports = {
plugins: [
new webpack.DefinePlugin({
'process.env.NODE_ENV': JSON.stringify('production'),
'process.env.API_URL': JSON.stringify('https://api.example.com'),
__DEV__: JSON.stringify(false)
})
]
};
使用 EnvironmentPlugin
const webpack = require('webpack');
module.exports = {
plugins: [
new webpack.EnvironmentPlugin(['NODE_ENV', 'API_URL'])
]
};
使用 --env 参数
// webpack.config.js
module.exports = (env) => {
console.log('NODE_ENV: ', env.NODE_ENV);
return {
mode: env.NODE_ENV === 'production' ? 'production' : 'development',
devtool: env.NODE_ENV === 'production' ? 'source-map' : 'eval-source-map'
};
};
webpack --env NODE_ENV=production
八、常用 Loader 与 Plugin
22. 常用 Loader 详解
babel-loader
{
test: /\.js$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
presets: [
['@babel/preset-env', {
targets: '> 1%, last 2 versions',
useBuiltIns: 'usage',
corejs: 3
}]
],
plugins: [
['@babel/plugin-transform-runtime', {
corejs: 3
}]
]
}
}
}
css-loader + style-loader
{
test: /\.css$/,
use: [
'style-loader', // 将 CSS 注入 DOM
{
loader: 'css-loader',
options: {
modules: true, // 启用 CSS Modules
importLoaders: 1 // 在 css-loader 前应用的 loader 数量
}
}
]
}
sass-loader / less-loader
{
test: /\.scss$/,
use: [
'style-loader',
'css-loader',
'sass-loader'
]
}
postcss-loader
{
test: /\.css$/,
use: [
'style-loader',
'css-loader',
{
loader: 'postcss-loader',
options: {
postcssOptions: {
plugins: [
require('autoprefixer'),
require('postcss-preset-env')
]
}
}
}
]
}
url-loader / file-loader
// Webpack 5 推荐使用 Asset Modules
{
test: /\.(png|jpg|gif|svg)$/,
type: 'asset',
parser: {
dataUrlCondition: {
maxSize: 8 * 1024 // 8kb 以下转 base64
}
},
generator: {
filename: 'images/[name].[hash:8][ext]'
}
}
23. 常用 Plugin 详解
HtmlWebpackPlugin
new HtmlWebpackPlugin({
template: './public/index.html',
filename: 'index.html',
title: 'My App',
minify: {
collapseWhitespace: true,
removeComments: true,
removeRedundantAttributes: true
},
chunks: ['app', 'vendor']
})
CleanWebpackPlugin
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
module.exports = {
plugins: [
new CleanWebpackPlugin({
cleanOnceBeforeBuildPatterns: ['dist/**/*']
})
]
};
MiniCssExtractPlugin
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
module.exports = {
module: {
rules: [
{
test: /\.css$/,
use: [
MiniCssExtractPlugin.loader,
'css-loader'
]
}
]
},
plugins: [
new MiniCssExtractPlugin({
filename: '[name].[contenthash].css',
chunkFilename: '[id].[contenthash].css'
})
]
};
ProvidePlugin
new webpack.ProvidePlugin({
$: 'jquery',
jQuery: 'jquery',
React: 'react'
});
BannerPlugin
new webpack.BannerPlugin({
banner: `Build Date: ${new Date().toLocaleDateString()}`,
raw: false,
entryOnly: true
});
BundleAnalyzerPlugin
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
module.exports = {
plugins: [
new BundleAnalyzerPlugin({
analyzerMode: 'server',
analyzerPort: 8888,
openAnalyzer: true
})
]
};
九、Webpack 工作流程
24. Webpack 构建流程详解
完整流程
Webpack 构建流程
1. 初始化阶段
- 读取配置文件
- 初始化 Compiler 对象
- 加载所有插件
- 调用插件的 apply 方法
2. 编译阶段
- 创建 compilation 对象
- 从 Entry 开始,解析入口文件
- 使用 Loader 处理文件
- 分析 AST,找出依赖模块
- 递归处理所有依赖
3. 优化阶段
- Tree Shaking
- 代码分割
- 模块合并
4. 输出阶段
- 组装 Chunk
- 生成 Bundle
- 写入文件系统
核心对象
Compiler:
// Compiler 代表完整的 Webpack 环境配置
// 在启动时创建一次
const compiler = webpack(config);
compiler.hooks.run.tap('MyPlugin', () => {
console.log('开始编译');
});
Compilation:
// Compilation 代表一次新的构建
// 每次文件变化都会创建新的 Compilation
compiler.hooks.compilation.tap('MyPlugin', (compilation) => {
compilation.hooks.optimize.tap('MyPlugin', () => {
console.log('优化模块');
});
});
Tapable 机制
const { SyncHook, AsyncSeriesHook } = require('tapable');
class MyCompiler {
constructor() {
this.hooks = {
run: new SyncHook(['params']),
compile: new AsyncSeriesHook(['params'])
};
}
run() {
this.hooks.run.call('start');
}
async compile() {
await this.hooks.compile.promise('compile');
}
}
Webpack 生命周期钩子
| 钩子 | 类型 | 说明 |
|---|---|---|
initialize | SyncBailHook | 初始化 |
afterPlugins | SyncHook | 插件加载完成后 |
afterResolvers | SyncHook | resolver 配置完成后 |
environment | SyncHook | 环境准备完成后 |
beforeRun | AsyncSeriesHook | 运行之前 |
run | AsyncSeriesHook | 开始读取 records |
emit | AsyncSeriesHook | 输出资源之前 |
afterEmit | AsyncSeriesHook | 输出资源之后 |
done | SyncHook | 构建完成 |
25. 自定义 Loader
定义
自定义 Loader 是一个导出函数的模块,该函数接收文件内容作为参数,返回转换后的结果。
示例
// replace-loader.js
module.exports = function(source) {
// source 是源文件内容
const options = this.getOptions();
const result = source.replace(
new RegExp(options.find, 'g'),
options.replace
);
// 返回转换后的内容
return result;
};
// 使用
{
test: /\.js$/,
use: {
loader: path.resolve('replace-loader.js'),
options: {
find: 'foo',
replace: 'bar'
}
}
}
Loader API
| API | 说明 |
|---|---|
this.getOptions() | 获取 Loader 配置选项 |
this.callback() | 返回多个值 |
this.async() | 声明异步 Loader |
this.cacheable() | 设置缓存 |
this.addDependency() | 添加文件依赖 |
异步 Loader
module.exports = function(source) {
const callback = this.async();
setTimeout(() => {
const result = source.toUpperCase();
callback(null, result);
}, 1000);
};
26. 自定义 Plugin
定义
自定义 Plugin 是一个具有 apply 方法的类,在 apply 方法中通过 compiler 对象挂载钩子函数。
示例
class FileListPlugin {
constructor(options) {
this.options = options;
}
apply(compiler) {
compiler.hooks.emit.tapAsync(
'FileListPlugin',
(compilation, callback) => {
// 生成文件列表
let fileList = 'Files in this build:\n\n';
for (let filename in compilation.assets) {
fileList += `- ${filename}\n`;
}
// 添加到输出
compilation.assets['filelist.md'] = {
source: () => fileList,
size: () => fileList.length
};
callback();
}
);
}
}
module.exports = FileListPlugin;
Compiler vs Compilation
| 维度 | Compiler | Compilation |
|---|---|---|
| 生命周期 | 整个 Webpack 生命周期 | 单次构建生命周期 |
| 创建时机 | Webpack 启动时 | 每次编译时 |
| 包含信息 | 配置、插件、Loader 等 | 模块、依赖、资源等 |
| 常用场景 | 全局性操作 | 模块级操作 |
十、Webpack 5 新特性
27. Webpack 5 新特性
持久化缓存
module.exports = {
cache: {
type: 'filesystem', // 使用文件系统缓存
buildDependencies: {
config: [__filename] // 配置变化时重建缓存
}
}
};
资源模块(Asset Modules)
Webpack 5 内置了资源模块,替代了 file-loader、url-loader、raw-loader。
module.exports = {
module: {
rules: [
// 自动选择
{
test: /\.(png|jpg|gif)$/,
type: 'asset'
},
// 导出为单独文件
{
test: /\.(png|jpg|gif)$/,
type: 'asset/resource'
},
// 导出为 Data URI
{
test: /\.svg$/,
type: 'asset/inline'
},
// 导出为源代码
{
test: /\.txt$/,
type: 'asset/source'
}
]
}
};
模块联邦(Module Federation)
// 主机应用配置
const ModuleFederationPlugin = require('webpack/lib/container/ModuleFederationPlugin');
module.exports = {
plugins: [
new ModuleFederationPlugin({
name: 'host',
remotes: {
app1: 'app1@http://localhost:3001/remoteEntry.js'
},
shared: ['react', 'react-dom']
})
]
};
// 远程应用配置
module.exports = {
plugins: [
new ModuleFederationPlugin({
name: 'app1',
filename: 'remoteEntry.js',
exposes: {
'./Button': './src/Button'
},
shared: ['react', 'react-dom']
})
]
};
其他新特性
| 特性 | 说明 |
|---|---|
| 更好的 Tree Shaking | 嵌套的 Tree Shaking 支持 |
| 更好的模块反馈 | 改进的模块大小反馈 |
| Node.js polyfills 移除 | 不再自动填充 Node.js 模块 |
| 优化持久缓存 | 缓存算法改进 |
| 自动公共路径 | publicPath: 'auto' |
十一、构建工具对比
28. Webpack 与 Vite 对比
定义对比
| 维度 | Webpack | Vite |
|---|---|---|
| 打包方式 | Bundler(打包所有代码) | ESM + unbundled(按需编译) |
| 开发服务器 | 启动慢,需要打包全部代码 | 启动快,按需编译 |
| HMR | 随着项目增大而变慢 | 始终保持快速 |
| 构建输出 | 生产环境打包 | 使用 Rollup 构建 |
| 生态成熟度 | 非常成熟 | 快速发展中 |
| 配置复杂度 | 配置较多 | 约定优于配置 |
原理对比
Webpack:
1. 启动时打包整个应用
2. 将所有模块打包成 Bundle
3. 启动开发服务器
4. 文件变化时重新编译相关模块
Vite:
1. 利用浏览器原生 ES Modules
2. 按需编译和加载模块
3. 启动开发服务器(秒级)
4. 文件变化时仅重新编译修改的文件
选择策略
| 场景 | 推荐 |
|---|---|
| 大型项目,需要精细控制 | Webpack |
| 新项目,追求开发体验 | Vite |
| 需要复杂代码分割 | Webpack |
| Vue/React 新项目 | Vite |
| 需要稳定的生态 | Webpack |
| 快速原型开发 | Vite |
29. Webpack 与 Rollup 对比
对比分析
| 维度 | Webpack | Rollup |
|---|---|---|
| 定位 | 应用程序打包工具 | 类库打包工具 |
| 代码分割 | 强大的代码分割 | 基础代码分割 |
| HMR | 原生支持 | 不支持 |
| Tree Shaking | 支持 | 非常优秀 |
| 生态 | 丰富 | 相对简单 |
| 配置 | 复杂 | 简单 |
选择策略
| 场景 | 推荐 |
|---|---|
| Web 应用 | Webpack |
| JavaScript 类库 | Rollup |
| 需要复杂插件生态 | Webpack |
| 追求最小打包体积 | Rollup |
| 需要 HMR | Webpack |
| 纯 ES Modules 项目 | Rollup |
30. Webpack 与 Parcel 对比
对比分析
| 维度 | Webpack | Parcel |
|---|---|---|
| 配置 | 需要配置文件 | 零配置 |
| 学习曲线 | 较陡 | 平缓 |
| 性能 | 可优化到极致 | 开箱即用 |
| 灵活性 | 高 | 低 |
| 生态 | 非常成熟 | 相对较小 |
| HMR | 支持 | 支持 |
| 代码分割 | 灵活配置 | 自动处理 |
选择策略
| 场景 | 推荐 |
|---|---|
| 需要精细控制 | Webpack |
| 快速上手 | Parcel |
| 企业级项目 | Webpack |
| 小型项目/原型 | Parcel |
十二、其他特性
31. Webpack Source Map
定义
Source Map 是一个映射文件,它将压缩/编译后的代码映射回源代码,方便调试。
devtool 配置选项
| 选项 | 构建速度 | 重新构建速度 | 生产环境 | 说明 |
|---|---|---|---|---|
eval | +++ | +++ | no | 每个模块用 eval() 执行 |
cheap-eval-source-map | ++ | ++ | no | 廉价映射,不包含列信息 |
cheap-module-eval-source-map | + | ++ | no | 包含 Loader 的 Source Map |
eval-source-map | -- | + | no | 高质量映射 |
source-map | --- | -- | yes | 完整 Source Map |
hidden-source-map | --- | -- | yes | 生成但不引用 |
nosources-source-map | --- | -- | yes | 无源代码内容 |
推荐配置
// 开发环境
module.exports = {
devtool: 'eval-cheap-module-source-map'
};
// 生产环境
module.exports = {
devtool: 'source-map' // 或 false(不生成)
};
32. Proxy 代理配置
定义
在开发环境中,由于同源策略,前端直接请求后端 API 会遇到跨域问题。Webpack Dev Server 提供了 Proxy 功能,将 API 请求代理到后端服务器。
基础配置
module.exports = {
devServer: {
proxy: {
'/api': {
target: 'http://localhost:3001',
pathRewrite: { '^/api': '' },
changeOrigin: true,
secure: false
}
}
}
};
多代理配置
module.exports = {
devServer: {
proxy: [
{
context: ['/auth', '/api'],
target: 'http://localhost:3001'
},
{
context: '/cdn',
target: 'http://cdn.example.com'
}
]
}
};
代理配置选项
| 选项 | 说明 |
|---|---|
target | 代理目标服务器地址 |
pathRewrite | 重写请求路径 |
changeOrigin | 修改请求头中的 host 为目标 URL |
secure | 是否验证 SSL 证书 |
bypass | 绕过代理的函数 |
headers | 添加请求头 |
33. historyApiFallback
定义
在使用 HTML5 History API(pushState/replaceState)的 SPA 应用中,刷新时可能会返回 404 错误。historyApiFallback 会将所有请求重定向到 index.html。
配置
module.exports = {
devServer: {
historyApiFallback: {
rewrites: [
{ from: /^\/$/, to: '/views/landing.html' },
{ from: /^\/subpage/, to: '/views/subpage.html' },
{ from: /./, to: '/views/404.html' }
]
}
}
};
34. CDN 加速
配置方式
module.exports = {
output: {
publicPath: 'https://cdn.example.com/assets/',
filename: '[name].[contenthash].js'
}
};
externals 排除第三方依赖
module.exports = {
externals: {
react: 'React',
'react-dom': 'ReactDOM'
}
};
<!-- index.html -->
<script src="https://cdn.example.com/react.production.min.js"></script>
<script src="https://cdn.example.com/react-dom.production.min.js"></script>
十三、常见问题解答
35. Webpack 常见问题
Q1: Loader 执行顺序为什么是从右到左?
Webpack 使用 compose 函数组合 Loader,遵循函数组合的数学规律,从右到左执行。
Q2: 如何处理 CSS 中的图片路径?
{
test: /\.css$/,
use: [
'style-loader',
{
loader: 'css-loader',
options: {
url: true // 处理 url()
}
}
]
}
Q3: 如何提取公共代码?
module.exports = {
optimization: {
splitChunks: {
cacheGroups: {
common: {
chunks: 'initial',
minChunks: 2,
name: 'common'
}
}
}
}
};
Q4: 如何优化大型项目构建速度?
- 使用 Webpack 5 文件系统缓存
- 使用 thread-loader 多线程
- 缩小 Loader 处理范围(include/exclude)
- 使用 DllPlugin 预编译第三方库
- 升级硬件(SSD、大内存)