会用plugin但自己不会写,怎么办?

298 阅读2分钟

Webpack的插件系统

日常打包项目时,Webpack可以帮我们自动完成很多构建工作,我们仅需要配置一些loader或plugin插件,就可以满足我们的构建需求。对大多数初学者来说,plugin更多是直接使用现成的,只有当现成的plugin无法满足我们的诉求时,才会有自己动手写一个plugin的念头出现。这时候就不得不去了解Webpack的插件系统了。

Webpack 的插件(Plugin)生命周期本质上是 通过 Tapable 库管理的钩子(Hooks)触发机制,开发者通过监听这些钩子介入 Webpack 编译流程的不同阶段。以下是核心生命周期阶段和关键钩子的技术解析。

核心生命周期流程图

flowchart TD
    A[Compiler 初始化] 
    
    A --> B1(environment: 环境准备)
    B1 --> B2(afterEnvironment: 环境就绪)
    B2 --> B3(entryOption: 解析入口配置)
    B3 --> B4(afterPlugins: 插件注册完成)
    B4 --> B5(afterResolvers: 解析器配置完成)
    
    B5 --> C1(beforeRun: 编译前)
    C1 --> C2(run: 开始编译)
    C2 --> C3(initialize: 编译器实例初始化)
    
    C3 --> D1(compile: 创建 compilation 对象)
    D1 --> D2(thisCompilation: compilation 初始化)
    D2 --> D3(compilation: compilation 创建完成)
    
    D3 --> E1(make: 依赖图构建阶段)
    E1 --> E2(buildModule: 模块构建)
    E2 --> E3(succeedModule: 模块构建完成)
    
    E3 --> F1(afterCompile: 编译完成)
    F1 --> G1(emit: 生成资源前最后修改)
    G1 --> G2(afterEmit: 资源写入磁盘)
    
    G2 --> H1(done: 编译完成)
    G2 --> H2(failed: 编译失败)

核心钩子深层解析

虽然生命周期涉及到的钩子多,但对于开发plugin需要关注核心钩子的意义。

1. Compiler 层面(全局生命周期)

钩子名称触发时机典型应用场景
entryOption解析 entry 配置后动态修改入口配置
compile创建 compilation 对象前初始化自定义编译参数
emit生成资源到输出目录前(内存中)修改最终输出内容(如添加 LICENSE)
afterEmit资源已写入磁盘清理临时文件/通知构建完成
done编译完全结束(stats 可用)输出构建耗时分析报告
failed编译失败时错误日志上报/告警

2. Compilation 层面(单次构建过程)

钩子名称触发时机典型应用场景
buildModule开始构建模块前过滤特定模块
succeedModule模块构建成功收集模块元数据
finishModules所有模块构建完成执行模块级统计分析
optimizeChunkschunk 优化阶段自定义 chunk 拆分策略
processAssets资源处理阶段(可多阶段操作)压缩/CSS 提取/资源指纹注入

插件开发技术要点

在了解了核心钩子后,如何使用钩子进行插件开发才是我们本次学习的目标。

1. 钩子类型与监听方式

// 同步钩子(SyncHook)
compiler.hooks.entryOption.tap('MyPlugin', (context, entry) => { /*...*/ });

// 异步串行钩子(AsyncSeriesHook)
compiler.hooks.emit.tapAsync('MyPlugin', (compilation, callback) => {
  // 异步操作完成后调用 callback()
});

// 异步并行钩子(AsyncParallelHook)
compiler.hooks.make.tapPromise('MyPlugin', async compilation => {
  await someAsyncOperation();
});

2. 典型插件结构示例

class MyPlugin {
  apply(compiler) {
    // 监听 emit 阶段(资源输出前)
    compiler.hooks.emit.tap('MyPlugin', compilation => {
      // 操作 compilation.assets
      compilation.assets['new-file.txt'] = {
        source: () => 'This is injected content',
        size: () => 21
      };
    });

    // 监听 done 阶段(编译完成)
    compiler.hooks.done.tap('MyPlugin', stats => {
      console.log(`Build time: ${stats.toJson().time}ms`);
    });
  }
}

plugin开发高级技巧

  1. 资源操作
    通过 compilation.assets 对象可动态增删改输出资源:

    // 删除指定资源
    delete compilation.assets['unused.js'];
    
    // 修改已有资源
    const source = compilation.assets['main.js'].source();
    compilation.assets['main.js'] = new ReplaceString(source, 'foo', 'bar');
    
  2. 跨阶段数据传递
    使用 compilation.cache 或自定义属性存储中间数据:

    compilation.__myPluginCache = { optimized: false };
    
  3. 性能优化
    在 optimizeChunks 阶段合并小 chunk:

    compilation.hooks.optimizeChunks.tap('MyPlugin', chunks => {
      // 自定义 chunk 合并逻辑
    });