2077 字
10 分钟
Webpack Plugin 开发完全指南
1. 什么是 Webpack Plugin?
Webpack Plugin 本质上是一个包含 apply 方法的类或对象。Plugin 可以在 Webpack 构建流程的各个阶段注入自定义逻辑。
1.1 最简单的 Plugin
class MyPlugin { // apply方法会被webpack调用 apply(compiler) { console.log('MyPlugin is working!')
// compiler是webpack的核心对象 // 可以通过hooks注册回调 }}
module.exports = MyPlugin1.2 使用 Plugin
const MyPlugin = require('./MyPlugin')
module.exports = { plugins: [ new MyPlugin() ]}2. 实际案例:自己写的 Plugin
案例1:生成文件列表插件
自动生成项目中所有打包文件的列表。
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:打包进度插件
在控制台输出构建进度和时间统计。
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 文件中的注释。
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。
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 = UploadCDNPlugin3. 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. 完整实战案例:自动生成版本信息插件
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使用这个插件
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. 优化chunkscompilation.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 开发核心要点
- ✅ Plugin必须是一个类或对象,包含
apply方法 - ✅ 通过
compiler/compilation的 hooks 注册回调 - ✅ 了解钩子的类型(同步/异步)和执行时机
- ✅ 使用
tap/tapAsync/tapPromise注册回调 - ✅ 异步钩子必须调用
callback或返回Promise
最常用的生命周期
- compile - 编译开始
- emit - 生成资源前(最常用)
- done - 编译完成
- compilation.processAssets - 处理资源(Webpack 5推荐)
Webpack Plugin 开发完全指南
https://fuwari.vercel.app/posts/webpack-plugin/