Webpack 核心原理深入
构建流程、模块解析、依赖图、Tapable 钩子系统
一、Webpack 架构设计
1.1 核心对象
Compiler:
typescript
class Compiler {
options: WebpackOptions
hooks: {
beforeRun: SyncHook
run: AsyncSeriesHook
emit: AsyncSeriesHook
done: SyncHook
// ... 更多钩子
}
run(callback: Callback) {
// 开始编译
}
}Compilation:
typescript
class Compilation {
modules: Set<Module>
chunks: Set<Chunk>
assets: Record<string, Source>
hooks: {
buildModule: SyncHook
succeedModule: SyncHook
finishModules: AsyncSeriesHook
// ... 更多钩子
}
}关系:
Compiler (全局唯一)
↓ 创建
Compilation (每次编译创建)
↓ 包含
Modules, Chunks, Assets1.2 构建流程详解
完整流程:
typescript
// 1. 初始化
const compiler = webpack(config)
// 2. 开始编译
compiler.run((err, stats) => {
// 编译完成
})内部流程:
1. 初始化阶段
├─ 读取配置
├─ 创建 Compiler
├─ 注册插件
└─ 触发 environment 钩子
2. 编译阶段
├─ 触发 beforeCompile 钩子
├─ 创建 Compilation
├─ 触发 make 钩子
├─ 从 Entry 开始构建
├─ 调用 Loader 转换模块
├─ 解析依赖
├─ 递归构建依赖模块
└─ 完成模块编译
3. 生成阶段
├─ 触发 seal 钩子
├─ 生成 Chunk
├─ 优化 Chunk
├─ 生成 Hash
├─ 生成 Assets
└─ 触发 emit 钩子
4. 输出阶段
├─ 写入文件系统
└─ 触发 done 钩子二、模块解析机制
2.1 模块类型
NormalModule:
typescript
class NormalModule extends Module {
request: string // 模块路径
userRequest: string // 用户请求路径
rawRequest: string // 原始请求
loaders: LoaderItem[] // 应用的 Loader
parser: Parser // 解析器
generator: Generator // 生成器
build(options, compilation, resolver, fs, callback) {
// 构建模块
}
}ContextModule:
typescript
// 动态导入
import(`./locale/${language}.js`)
// 生成 ContextModule
class ContextModule extends Module {
options: {
regExp: RegExp
recursive: boolean
}
}ExternalModule:
typescript
// 外部依赖
externals: {
jquery: 'jQuery'
}
class ExternalModule extends Module {
request: string
externalType: string
}2.2 依赖解析
Resolver 配置:
typescript
resolve: {
// 查找模块的目录
modules: ['node_modules', path.resolve(__dirname, 'src')],
// 尝试的扩展名
extensions: ['.js', '.json', '.jsx', '.ts', '.tsx'],
// 别名
alias: {
'@': path.resolve(__dirname, 'src'),
'components': path.resolve(__dirname, 'src/components')
},
// 主文件
mainFiles: ['index'],
// package.json 的字段
mainFields: ['browser', 'module', 'main']
}解析流程:
typescript
// 1. 解析模块路径
import Button from '@/components/Button'
// 2. 应用 alias
'@/components/Button' → '/project/src/components/Button'
// 3. 尝试扩展名
'/project/src/components/Button.js' ✗
'/project/src/components/Button.json' ✗
'/project/src/components/Button.jsx' ✓
// 4. 返回绝对路径
'/project/src/components/Button.jsx'2.3 依赖图构建
示例代码:
javascript
// index.js
import { add } from './math.js'
import './style.css'
console.log(add(1, 2))
// math.js
export function add(a, b) {
return a + b
}
// style.css
body { color: red; }依赖图:
index.js (Entry)
├─ math.js (NormalModule)
└─ style.css (NormalModule)构建过程:
typescript
// 1. 从 Entry 开始
const entryModule = compilation.addEntry(context, entry)
// 2. 构建模块
entryModule.build()
// 3. 解析依赖
const dependencies = parser.parse(entryModule.source)
// 4. 递归构建依赖
dependencies.forEach(dep => {
const module = compilation.addModule(dep)
module.build()
})
// 5. 生成依赖图
const dependencyGraph = {
'index.js': {
dependencies: ['math.js', 'style.css'],
code: '...'
},
'math.js': {
dependencies: [],
code: '...'
},
'style.css': {
dependencies: [],
code: '...'
}
}三、Loader 原理
3.1 Loader 本质
Loader 是一个函数:
typescript
// 同步 Loader
function loader(source: string): string {
// 转换源代码
return transformedSource
}
// 异步 Loader
function loader(source: string): void {
const callback = this.async()
doAsyncWork(source, (err, result) => {
callback(err, result)
})
}Loader Context:
typescript
interface LoaderContext {
// 当前处理的文件路径
resourcePath: string
// 异步回调
async(): (err: Error | null, content: string) => void
// 添加依赖
addDependency(file: string): void
// 获取查询参数
query: any
// 缓存
cacheable(flag: boolean): void
// Source Map
sourceMap: boolean
}3.2 Loader 执行顺序
配置:
javascript
{
test: /\.css$/,
use: ['style-loader', 'css-loader', 'postcss-loader']
}执行顺序:
源文件
↓
postcss-loader (Pitch)
↓
css-loader (Pitch)
↓
style-loader (Pitch)
↓
读取文件内容
↓
style-loader (Normal)
↓
css-loader (Normal)
↓
postcss-loader (Normal)
↓
转换后的代码Pitch 和 Normal:
typescript
// Loader 有两个阶段
loader.pitch = function(remainingRequest, precedingRequest, data) {
// Pitch 阶段(从左到右)
// 可以提前返回结果,跳过后续 Loader
}
function loader(source) {
// Normal 阶段(从右到左)
return transformedSource
}3.3 手写 Loader
示例 1:替换文本 Loader:
javascript
// replace-loader.js
module.exports = function(source) {
const options = this.getOptions()
return source.replace(
new RegExp(options.search, 'g'),
options.replace
)
}使用:
javascript
{
test: /\.js$/,
use: {
loader: path.resolve(__dirname, 'loaders/replace-loader.js'),
options: {
search: 'console.log',
replace: '// console.log'
}
}
}示例 2:Markdown Loader:
javascript
// markdown-loader.js
const marked = require('marked')
module.exports = function(source) {
// 标记为可缓存
this.cacheable && this.cacheable()
// 转换 Markdown 为 HTML
const html = marked(source)
// 返回 ES Module
return `export default ${JSON.stringify(html)}`
}使用:
javascript
{
test: /\.md$/,
use: path.resolve(__dirname, 'loaders/markdown-loader.js')
}javascript
// 代码中使用
import content from './README.md'
document.body.innerHTML = content示例 3:异步 Loader:
javascript
// async-loader.js
module.exports = function(source) {
const callback = this.async()
// 异步处理
setTimeout(() => {
const result = source.toUpperCase()
callback(null, result)
}, 1000)
}四、Plugin 原理
4.1 Tapable 钩子系统
钩子类型:
typescript
// 同步钩子
const { SyncHook, SyncBailHook, SyncWaterfallHook } = require('tapable')
// 异步钩子
const { AsyncSeriesHook, AsyncParallelHook } = require('tapable')使用示例:
typescript
class Car {
constructor() {
this.hooks = {
// 同步钩子
accelerate: new SyncHook(['speed']),
// 异步串行钩子
brake: new AsyncSeriesHook(['speed']),
// 异步并行钩子
calculateRoutes: new AsyncParallelHook(['source', 'target'])
}
}
setSpeed(speed) {
// 触发钩子
this.hooks.accelerate.call(speed)
}
}
// 注册监听
const car = new Car()
car.hooks.accelerate.tap('LoggerPlugin', (speed) => {
console.log(`Accelerating to ${speed}`)
})
car.setSpeed(100)4.2 Plugin 结构
基本结构:
typescript
class MyPlugin {
apply(compiler) {
// 注册钩子
compiler.hooks.emit.tapAsync('MyPlugin', (compilation, callback) => {
// 插件逻辑
console.log('生成文件前')
callback()
})
}
}
module.exports = MyPlugin使用:
javascript
plugins: [
new MyPlugin()
]4.3 常用钩子
Compiler 钩子:
typescript
compiler.hooks.beforeRun.tap('MyPlugin', (compiler) => {
// 编译前
})
compiler.hooks.run.tap('MyPlugin', (compiler) => {
// 开始编译
})
compiler.hooks.emit.tapAsync('MyPlugin', (compilation, callback) => {
// 生成文件前
callback()
})
compiler.hooks.done.tap('MyPlugin', (stats) => {
// 编译完成
})Compilation 钩子:
typescript
compiler.hooks.compilation.tap('MyPlugin', (compilation) => {
compilation.hooks.buildModule.tap('MyPlugin', (module) => {
// 构建模块前
})
compilation.hooks.succeedModule.tap('MyPlugin', (module) => {
// 模块构建成功
})
compilation.hooks.optimize.tap('MyPlugin', () => {
// 优化阶段
})
})4.4 手写 Plugin
示例 1:文件列表插件:
javascript
// FileListPlugin.js
class FileListPlugin {
constructor(options) {
this.filename = options.filename || 'filelist.md'
}
apply(compiler) {
compiler.hooks.emit.tapAsync('FileListPlugin', (compilation, callback) => {
// 获取所有生成的文件
const filelist = Object.keys(compilation.assets)
.map(filename => `- ${filename}`)
.join('\n')
const content = `# 文件列表\n\n${filelist}`
// 添加新文件到输出
compilation.assets[this.filename] = {
source: () => content,
size: () => content.length
}
callback()
})
}
}
module.exports = FileListPlugin使用:
javascript
plugins: [
new FileListPlugin({ filename: 'assets.md' })
]示例 2:清除注释插件:
javascript
// RemoveCommentsPlugin.js
class RemoveCommentsPlugin {
apply(compiler) {
compiler.hooks.emit.tap('RemoveCommentsPlugin', (compilation) => {
// 遍历所有生成的文件
for (const filename in compilation.assets) {
if (filename.endsWith('.js')) {
const asset = compilation.assets[filename]
const source = asset.source()
// 移除注释
const newSource = source
.replace(/\/\*[\s\S]*?\*\//g, '')
.replace(/\/\/.*/g, '')
// 更新文件内容
compilation.assets[filename] = {
source: () => newSource,
size: () => newSource.length
}
}
}
})
}
}
module.exports = RemoveCommentsPlugin示例 3:打包进度插件:
javascript
// ProgressPlugin.js
class ProgressPlugin {
apply(compiler) {
let lastPercent = 0
compiler.hooks.compilation.tap('ProgressPlugin', (compilation) => {
compilation.hooks.buildModule.tap('ProgressPlugin', (module) => {
const percent = Math.floor(
(compilation.modules.size / compilation._modulesCache.size) * 100
)
if (percent !== lastPercent) {
console.log(`构建进度: ${percent}%`)
lastPercent = percent
}
})
})
}
}
module.exports = ProgressPlugin五、Chunk 生成机制
5.1 Chunk 类型
Entry Chunk:
javascript
entry: {
app: './src/app.js',
admin: './src/admin.js'
}
// 生成两个 Entry Chunk
// app.js, admin.jsAsync Chunk:
javascript
// 动态导入
import('./module.js').then(module => {
// ...
})
// 生成 Async Chunk
// 0.js (module.js)Runtime Chunk:
javascript
optimization: {
runtimeChunk: 'single'
}
// 生成 Runtime Chunk
// runtime.js5.2 SplitChunks 原理
默认配置:
javascript
optimization: {
splitChunks: {
chunks: 'async',
minSize: 20000,
minRemainingSize: 0,
minChunks: 1,
maxAsyncRequests: 30,
maxInitialRequests: 30,
enforceSizeThreshold: 50000,
cacheGroups: {
defaultVendors: {
test: /[\\/]node_modules[\\/]/,
priority: -10,
reuseExistingChunk: true
},
default: {
minChunks: 2,
priority: -20,
reuseExistingChunk: true
}
}
}
}分割流程:
1. 收集所有模块
2. 按 cacheGroups 分组
3. 检查是否满足条件:
- minSize: 最小体积
- minChunks: 最少引用次数
- maxAsyncRequests: 最大异步请求数
- maxInitialRequests: 最大初始请求数
4. 生成新 Chunk
5. 更新依赖关系5.3 Chunk 优化
合并 Chunk:
javascript
optimization: {
mergeDuplicateChunks: true
}移除空 Chunk:
javascript
optimization: {
removeEmptyChunks: true
}模块连接:
javascript
optimization: {
concatenateModules: true // Scope Hoisting
}六、打包产物分析
6.1 Bundle 结构
简化的 Bundle:
javascript
(function(modules) {
// 模块缓存
const installedModules = {}
// require 函数
function __webpack_require__(moduleId) {
// 检查缓存
if (installedModules[moduleId]) {
return installedModules[moduleId].exports
}
// 创建模块
const module = installedModules[moduleId] = {
id: moduleId,
loaded: false,
exports: {}
}
// 执行模块函数
modules[moduleId].call(
module.exports,
module,
module.exports,
__webpack_require__
)
module.loaded = true
return module.exports
}
// 加载入口模块
return __webpack_require__(0)
})([
// 模块 0: index.js
function(module, exports, __webpack_require__) {
const math = __webpack_require__(1)
console.log(math.add(1, 2))
},
// 模块 1: math.js
function(module, exports) {
exports.add = function(a, b) {
return a + b
}
}
])6.2 动态导入
源代码:
javascript
button.addEventListener('click', () => {
import('./module.js').then(module => {
module.default()
})
})打包后:
javascript
button.addEventListener('click', () => {
__webpack_require__.e(/* chunkId */ 0).then(
__webpack_require__.bind(null, /* moduleId */ 1)
).then(module => {
module.default()
})
})
// __webpack_require__.e 实现
__webpack_require__.e = function(chunkId) {
return new Promise((resolve, reject) => {
const script = document.createElement('script')
script.src = __webpack_require__.p + chunkId + '.js'
script.onload = resolve
script.onerror = reject
document.head.appendChild(script)
})
}七、性能优化原理
7.1 Tree Shaking 原理
标记阶段:
javascript
// utils.js
export function add(a, b) { return a + b }
export function subtract(a, b) { return a - b }
// index.js
import { add } from './utils.js'
console.log(add(1, 2))
// 标记结果
// add: used
// subtract: unused删除阶段:
javascript
// Terser 压缩时删除未使用的代码
// 最终输出只包含 add 函数7.2 Scope Hoisting 原理
未优化:
javascript
// module-a.js
export const a = 1
// module-b.js
export const b = 2
// index.js
import { a } from './module-a.js'
import { b } from './module-b.js'
console.log(a + b)
// 打包后(3 个模块)优化后:
javascript
// 合并为一个模块
const a = 1
const b = 2
console.log(a + b)配置:
javascript
optimization: {
concatenateModules: true
}7.3 持久化缓存原理
缓存结构:
.temp_cache/
├─ default/
│ ├─ 0.pack
│ ├─ 1.pack
│ └─ index.json
└─ buildDependencies/
└─ webpack.config.js缓存策略:
typescript
// 1. 计算模块 Hash
const moduleHash = crypto
.createHash('md5')
.update(moduleSource)
.digest('hex')
// 2. 检查缓存
if (cache.has(moduleHash)) {
return cache.get(moduleHash)
}
// 3. 构建模块
const result = buildModule(module)
// 4. 写入缓存
cache.set(moduleHash, result)面试高频题
1: Webpack 的构建流程是什么?
答案:
三个阶段:
- 初始化:读取配置、创建 Compiler、注册插件
- 编译:从 Entry 开始、调用 Loader、解析依赖、递归构建
- 输出:生成 Chunk、生成 Assets、写入文件
核心对象:
- Compiler:全局唯一,控制整个流程
- Compilation:每次编译创建,包含模块和资源
2: Loader 和 Plugin 的区别是什么?
答案:
Loader:
- 文件转换器
- 处理单个文件
- 在模块加载时执行
- 返回转换后的内容
Plugin:
- 功能扩展器
- 介入整个构建流程
- 通过钩子系统工作
- 可以访问 Compiler 和 Compilation
3: Loader 的执行顺序是什么?
答案:
从右到左,从下到上:
javascript
{
test: /\.css$/,
use: ['style-loader', 'css-loader', 'postcss-loader']
}
// 执行顺序:
// postcss-loader → css-loader → style-loader原因:函数组合(compose)
javascript
compose(f, g, h)(x) = f(g(h(x)))4: 如何编写一个 Loader?
答案:
javascript
module.exports = function(source) {
// 1. 获取配置
const options = this.getOptions()
// 2. 转换源代码
const result = transform(source, options)
// 3. 返回结果
return result
}
// 异步 Loader
module.exports = function(source) {
const callback = this.async()
asyncTransform(source, (err, result) => {
callback(err, result)
})
}5: 如何编写一个 Plugin?
答案:
javascript
class MyPlugin {
apply(compiler) {
compiler.hooks.emit.tapAsync('MyPlugin', (compilation, callback) => {
// 在生成文件前执行
console.log('生成文件...')
callback()
})
}
}
module.exports = MyPlugin关键点:
- 实现
apply方法 - 通过
compiler.hooks注册钩子 - 使用
tap、tapAsync或tapPromise
6: Webpack 如何实现 Tree Shaking?
答案:
原理:
- 基于 ES6 Module 静态分析
- 标记未使用的导出(
usedExports) - Terser 删除未使用的代码(
minimize)
配置:
javascript
// webpack.config.js
optimization: {
usedExports: true,
minimize: true
}
// package.json
{
"sideEffects": false
}7: Webpack 如何实现代码分割?
答案:
三种方式:
- 多入口:
javascript
entry: {
app: './src/app.js',
admin: './src/admin.js'
}- SplitChunksPlugin:
javascript
optimization: {
splitChunks: {
chunks: 'all'
}
}- 动态导入:
javascript
import('./module.js').then(module => {
// ...
})8: Webpack 的模块解析流程是什么?
答案:
1. 解析模块路径
import Button from '@/components/Button'
2. 应用 alias
'@/components/Button' → '/project/src/components/Button'
3. 尝试扩展名
Button.js, Button.json, Button.jsx
4. 查找文件
找到 Button.jsx
5. 返回绝对路径
'/project/src/components/Button.jsx'9: Webpack 的 HMR 原理是什么?
答案:
流程:
1. Webpack Compiler 监听文件变化
2. 重新编译生成新模块
3. Webpack Dev Server 通过 WebSocket 推送更新
4. 客户端通过 JSONP 请求新模块
5. HMR Runtime 替换旧模块
6. 触发 module.hot.accept() 回调关键:
- WebSocket 通信
- 模块热替换
- 保留应用状态
10: Webpack 5 的持久化缓存如何工作?
答案:
原理:
javascript
cache: {
type: 'filesystem',
cacheDirectory: path.resolve(__dirname, '.temp_cache')
}工作方式:
- 计算模块内容 Hash
- 检查缓存是否存在
- 命中缓存直接返回
- 未命中则构建并写入缓存
效果:二次构建速度提升 90%+