2077 字
10 分钟
Webpack Plugin 开发完全指南

1. 什么是 Webpack Plugin?#

Webpack Plugin 本质上是一个包含 apply 方法的类或对象。Plugin 可以在 Webpack 构建流程的各个阶段注入自定义逻辑。

1.1 最简单的 Plugin#

MyPlugin.js
class MyPlugin {
// apply方法会被webpack调用
apply(compiler) {
console.log('MyPlugin is working!')
// compiler是webpack的核心对象
// 可以通过hooks注册回调
}
}
module.exports = MyPlugin

1.2 使用 Plugin#

webpack.config.js
const MyPlugin = require('./MyPlugin')
module.exports = {
plugins: [
new MyPlugin()
]
}

2. 实际案例:自己写的 Plugin#

案例1:生成文件列表插件#

自动生成项目中所有打包文件的列表。

FileListPlugin.js
class FileListPlugin {
constructor(options) {
// 接收配置参数
this.options = options || {}
this.filename = this.options.filename || 'filelist.md'
}
apply(compiler) {
// 在emit阶段(生成资源到output目录之前)
compiler.hooks.emit.tapAsync(
'FileListPlugin',
(compilation, callback) => {
// 获取所有生成的文件
let filelist = '# File List\n\n'
for (let filename in compilation.assets) {
filelist += `- ${filename}\n`
}
// 将文件列表作为新资源添加到输出
compilation.assets[this.filename] = {
source: () => filelist,
size: () => filelist.length
}
callback()
}
)
}
}
module.exports = FileListPlugin

案例2:打包进度插件#

在控制台输出构建进度和时间统计。

BuildProgressPlugin.js
class BuildProgressPlugin {
apply(compiler) {
let startTime
// 编译开始
compiler.hooks.compile.tap('BuildProgressPlugin', () => {
startTime = Date.now()
console.log('🚀 开始编译...')
})
// 编译完成
compiler.hooks.done.tap('BuildProgressPlugin', (stats) => {
const duration = Date.now() - startTime
if (stats.hasErrors()) {
console.log('❌ 编译失败')
} else {
console.log(`✅ 编译成功!耗时: ${duration}ms`)
}
})
}
}
module.exports = BuildProgressPlugin

案例3:清理注释插件#

自动清理生成的 JS 文件中的注释。

RemoveCommentsPlugin.js
class RemoveCommentsPlugin {
apply(compiler) {
compiler.hooks.emit.tap('RemoveCommentsPlugin', (compilation) => {
// 遍历所有生成的资源
for (let filename in compilation.assets) {
// 只处理JS文件
if (filename.endsWith('.js')) {
let content = compilation.assets[filename].source()
// 移除单行注释
content = content.replace(/\/\/.*/g, '')
// 移除多行注释
content = content.replace(/\/\*[\s\S]*?\*\//g, '')
// 更新资源
compilation.assets[filename] = {
source: () => content,
size: () => content.length
}
}
}
})
}
}
module.exports = RemoveCommentsPlugin

案例4:上传到 CDN 插件#

构建完成后自动将资源上传到 CDN。

UploadCDNPlugin.js
class UploadCDNPlugin {
constructor(options) {
this.cdnUrl = options.cdnUrl
this.uploadFn = options.uploadFn
}
apply(compiler) {
// 在资源生成后上传
compiler.hooks.afterEmit.tapAsync(
'UploadCDNPlugin',
async (compilation, callback) => {
const assets = compilation.assets
console.log('📤 开始上传到CDN...')
try {
for (let filename in assets) {
const filePath = assets[filename].existsAt
await this.uploadFn(filePath, this.cdnUrl)
console.log(`✅ ${filename} 上传成功`)
}
callback()
} catch (error) {
console.error('❌ 上传失败:', error)
callback(error)
}
}
)
}
}
module.exports = UploadCDNPlugin

3. Webpack 生命周期钩子(Hooks)#

Webpack 使用 Tapable 库来管理钩子系统,主要有两个核心对象:

3.1 Compiler 钩子(全局级别)#

Compiler 对象代表了完整的 webpack 环境配置,在 webpack 启动时创建,全局唯一。

// Compiler 主要钩子
compiler.hooks = {
// 1. 环境准备阶段
environment: SyncHook, // 环境准备好
afterEnvironment: SyncHook, // 环境准备完成
// 2. 入口配置阶段
entryOption: SyncBailHook, // entry配置处理
afterPlugins: SyncHook, // 插件加载完成
afterResolvers: SyncHook, // 解析器设置完成
// 3. 编译阶段
beforeRun: AsyncSeriesHook, // 编译器开始运行前
run: AsyncSeriesHook, // 编译器开始运行
watchRun: AsyncSeriesHook, // watch模式下编译开始
beforeCompile: AsyncSeriesHook, // 编译参数创建后
compile: SyncHook, // 开始编译
thisCompilation: SyncHook, // 触发compilation事件前
compilation: SyncHook, // compilation对象创建
make: AsyncParallelHook, // 完成一次编译前
// 4. 输出阶段
shouldEmit: SyncBailHook, // 是否应该生成资源
emit: AsyncSeriesHook, // 生成资源到output目录前
afterEmit: AsyncSeriesHook, // 生成资源到output目录后
assetEmitted: AsyncSeriesHook, // 资源已生成
// 5. 完成阶段
done: AsyncSeriesHook, // 编译完成
failed: SyncHook, // 编译失败
// 6. 无效阶段
invalid: SyncHook, // watch模式文件变化
watchClose: SyncHook // watch模式停止
}

3.2 Compilation 钩子(编译级别)#

Compilation 对象代表一次资源版本构建,每次文件变化都会创建新的 Compilation。

// Compilation 主要钩子
compilation.hooks = {
// 1. 构建阶段
buildModule: SyncHook, // 构建模块前
rebuildModule: SyncHook, // 重新构建模块
failedModule: SyncHook, // 模块构建失败
succeedModule: SyncHook, // 模块构建成功
finishModules: AsyncSeriesHook, // 所有模块构建完成
// 2. 模块处理阶段
seal: SyncHook, // 开始封装
beforeChunks: SyncHook, // 生成chunks前
afterChunks: SyncHook, // 生成chunks后
// 3. 优化阶段
optimize: SyncHook, // 优化开始
optimizeModules: SyncBailHook, // 优化模块
optimizeChunks: SyncBailHook, // 优化chunks
optimizeTree: AsyncSeriesHook, // 优化依赖树
optimizeChunkModules: SyncBailHook, // 优化chunk中的模块
// 模块优化
optimizeModuleIds: SyncHook, // 优化模块ID
afterOptimizeModuleIds: SyncHook, // 模块ID优化完成
// Chunk优化
optimizeChunkIds: SyncHook, // 优化chunkID
afterOptimizeChunkIds: SyncHook, // chunkID优化完成
// 4. 资源生成阶段
beforeModuleAssets: SyncHook, // 创建模块资源前
moduleAsset: SyncHook, // 模块资源生成
chunkAsset: SyncHook, // chunk资源生成
// 5. 资源优化阶段
processAssets: AsyncSeriesHook, // 处理资源(重要!)
afterProcessAssets: SyncHook, // 资源处理完成
optimizeAssets: AsyncSeriesHook, // 优化资源
afterOptimizeAssets: SyncHook, // 资源优化完成
// 6. 生成hash
beforeHash: SyncHook, // 生成hash前
afterHash: SyncHook, // 生成hash后
// 7. 记录
record: SyncHook, // 存储记录
// 8. 完成
beforeModuleIds: SyncHook, // 模块ID分配前
moduleIds: SyncHook, // 模块ID分配
needAdditionalPass: SyncBailHook, // 是否需要额外的pass
succeedModule: SyncHook, // 模块成功
stillValidModule: SyncHook, // 模块仍然有效
// stats相关
statsPreset: HookMap, // stats预设
statsNormalize: SyncHook, // stats标准化
statsFactory: SyncHook, // stats工厂
statsPrinter: SyncHook // stats打印
}

4. 钩子类型说明#

4.1 SyncHook - 同步钩子#

compiler.hooks.compile.tap('MyPlugin', () => {
console.log('同步执行')
})

4.2 SyncBailHook - 同步熔断钩子#

返回非 undefined 值会停止后续钩子执行。

compiler.hooks.shouldEmit.tap('MyPlugin', (compilation) => {
return false // 阻止生成资源
})

4.3 AsyncSeriesHook - 异步串行钩子#

方式1: callback

compiler.hooks.emit.tapAsync('MyPlugin', (compilation, callback) => {
setTimeout(() => {
console.log('异步操作')
callback()
}, 1000)
})

方式2: promise

compiler.hooks.emit.tapPromise('MyPlugin', (compilation) => {
return new Promise((resolve) => {
setTimeout(() => {
console.log('异步操作')
resolve()
}, 1000)
})
})

方式3: async/await

compiler.hooks.emit.tapPromise('MyPlugin', async (compilation) => {
await someAsyncOperation()
console.log('异步操作完成')
})

4.4 AsyncParallelHook - 异步并行钩子#

compiler.hooks.make.tapAsync('MyPlugin', (compilation, callback) => {
// 所有注册的回调会并行执行
callback()
})

5. 完整实战案例:自动生成版本信息插件#

VersionPlugin.js
const fs = require('fs')
const path = require('path')
class VersionPlugin {
constructor(options = {}) {
this.options = {
filename: 'version.json',
includeHash: true,
includeTime: true,
...options
}
}
apply(compiler) {
const pluginName = 'VersionPlugin'
// 1. 编译开始时记录时间
let startTime
compiler.hooks.compile.tap(pluginName, () => {
startTime = Date.now()
console.log('📦 开始构建...')
})
// 2. 在生成资源前创建版本文件
compiler.hooks.emit.tapAsync(pluginName, (compilation, callback) => {
const stats = compilation.getStats().toJson()
const versionInfo = {
version: this.getVersion(),
buildTime: this.options.includeTime ? new Date().toISOString() : undefined,
hash: this.options.includeHash ? stats.hash : undefined,
duration: Date.now() - startTime,
assets: Object.keys(compilation.assets).map(name => ({
name,
size: compilation.assets[name].size()
}))
}
const content = JSON.stringify(versionInfo, null, 2)
// 添加到输出资源
compilation.assets[this.options.filename] = {
source: () => content,
size: () => content.length
}
console.log(`✅ 版本信息已生成: ${this.options.filename}`)
callback()
})
// 3. 编译完成后的处理
compiler.hooks.done.tap(pluginName, (stats) => {
if (stats.hasErrors()) {
console.log('❌ 构建失败')
return
}
const duration = Date.now() - startTime
console.log(`✅ 构建成功!耗时: ${duration}ms`)
// 输出统计信息
const info = stats.toJson()
console.log(`📊 资源数量: ${Object.keys(info.assets).length}`)
console.log(`📦 总大小: ${this.formatSize(info.assets.reduce((sum, asset) => sum + asset.size, 0))}`)
})
// 4. 监听文件变化
compiler.hooks.watchRun.tap(pluginName, (compiler) => {
const changedFiles = compiler.modifiedFiles
if (changedFiles && changedFiles.size > 0) {
console.log('🔄 检测到文件变化:', Array.from(changedFiles).join(', '))
}
})
}
// 读取package.json的版本号
getVersion() {
try {
const pkgPath = path.resolve(process.cwd(), 'package.json')
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'))
return pkg.version || '0.0.0'
} catch (e) {
return '0.0.0'
}
}
// 格式化文件大小
formatSize(bytes) {
if (bytes < 1024) return bytes + ' B'
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(2) + ' KB'
return (bytes / (1024 * 1024)).toFixed(2) + ' MB'
}
}
module.exports = VersionPlugin

使用这个插件#

webpack.config.js
const VersionPlugin = require('./plugins/VersionPlugin')
module.exports = {
plugins: [
new VersionPlugin({
filename: 'build-info.json',
includeHash: true,
includeTime: true
})
]
}

生成的 build-info.json#

{
"version": "1.0.0",
"buildTime": "2025-01-04T10:30:00.000Z",
"hash": "a1b2c3d4e5f6",
"duration": 2345,
"assets": [
{
"name": "main.a1b2c3.js",
"size": 245678
},
{
"name": "main.a1b2c3.css",
"size": 12345
}
]
}

6. 常用生命周期钩子速查表#

📌 最常用的钩子#

// 1. 编译开始
compiler.hooks.compile.tap() // 同步
// 2. 生成资源前(修改输出内容)
compiler.hooks.emit.tapAsync() // 异步
// 3. 生成资源后(上传CDN等)
compiler.hooks.afterEmit.tapAsync() // 异步
// 4. 编译完成
compiler.hooks.done.tap() // 同步
// 5. 优化资源(压缩、处理等)
compilation.hooks.processAssets.tap() // 同步
// 6. 优化chunks
compilation.hooks.optimizeChunks.tap() // 同步
// 7. 模块构建完成
compilation.hooks.finishModules.tap() // 同步

7. 调试技巧#

class DebugPlugin {
apply(compiler) {
// 打印所有可用的钩子
console.log('Compiler hooks:', Object.keys(compiler.hooks))
// 监听所有compilation钩子
compiler.hooks.compilation.tap('DebugPlugin', (compilation) => {
console.log('Compilation hooks:', Object.keys(compilation.hooks))
// 打印每个阶段
compilation.hooks.seal.tap('DebugPlugin', () => {
console.log('📍 Seal stage')
})
compilation.hooks.optimize.tap('DebugPlugin', () => {
console.log('📍 Optimize stage')
})
})
}
}

总结#

Plugin 开发核心要点#

  1. ✅ Plugin必须是一个类或对象,包含 apply 方法
  2. ✅ 通过 compiler/compilation 的 hooks 注册回调
  3. ✅ 了解钩子的类型(同步/异步)和执行时机
  4. ✅ 使用 tap/tapAsync/tapPromise 注册回调
  5. ✅ 异步钩子必须调用 callback 或返回 Promise

最常用的生命周期#

  • compile - 编译开始
  • emit - 生成资源前(最常用)
  • done - 编译完成
  • compilation.processAssets - 处理资源(Webpack 5推荐)
Webpack Plugin 开发完全指南
https://fuwari.vercel.app/posts/webpack-plugin/
作者
Lorem Ipsum
发布于
2025-11-04
许可协议
CC BY-NC-SA 4.0