FORMA

Plugin

Webpack 插件在构建生命周期钩子上扩展能力(修改资源、生成 HTML、上传 CDN 等)。背景见 工作原理;与 Loader 分工:Loader 转换单文件,插件面向整体构建。

一、插件基本架构

每个插件都是一个具有 apply(compiler) 方法的类或对象。在 apply 中通过 compiler.hooks 注册钩子回调。

js
class MyPlugin {
  apply(compiler) {
    // 同步钩子使用 tap
    compiler.hooks.compile.tap("MyPlugin", (params) => {
      console.log("开始编译");
    });

    // 异步串行钩子使用 tapAsync(调用 callback)
    compiler.hooks.emit.tapAsync("MyPlugin", (compilation, callback) => {
      setTimeout(() => {
        console.log("发射文件前");
        callback();
      }, 1000);
    });

    // 异步并行钩子使用 tapPromise(返回 Promise)
    compiler.hooks.run.tapPromise("MyPlugin", (compiler) => {
      return new Promise((resolve) => {
        setTimeout(() => {
          console.log("运行开始");
          resolve();
        }, 500);
      });
    });
  }
}

钩子类型决定注册方式

  • SyncHook:只能用 tap
  • SyncBailHook:只能用 tap,返回非 undefined 则中断
  • AsyncParallelHook:可用 tap/tapAsync/tapPromise
  • AsyncSeriesHook:同上,但串行执行

二、Compiler 与 Compilation 核心属性

1. compiler 对象

代表整个 Webpack 实例,包含了配置、文件系统、插件集合等。

属性说明常用场景
compiler.options最终的合并配置读取用户配置项
compiler.inputFileSystem读取文件的文件系统(默认 fs手动读取文件
compiler.outputFileSystem输出文件的文件系统写入自定义文件
compiler.hooks所有生命周期钩子注册插件逻辑
compiler.context项目根目录绝对路径解析相对路径

2. compilation 对象

代表一次完整的资源构建过程,包含当前打包的所有模块、chunk、生成资源等。

属性说明常用场景
compilation.modules所有模块的集合(Set)遍历所有模块,筛选特定模块
compilation.chunks所有 chunk 的集合修改 chunk 名称或内容
compilation.assets生成的文件资源对象(key: 文件名, value: Source)修改、添加、删除输出资源
compilation.errors / warnings错误/警告数组收集并上报错误
compilation.inputFileSystemcompiler.inputFileSystem读取文件
compilation.getStats()生成 stats 对象输出构建统计信息

修改 assets 示例

js
compiler.hooks.emit.tapAsync("MyPlugin", (compilation, callback) => {
  // 添加一个新文件
  compilation.assets["my-version.json"] = {
    source: () => JSON.stringify({ version: "1.0.0" }),
    size: () => 20,
  };
  // 或使用 webpack-sources
  const { RawSource } = require("webpack-sources");
  compilation.assets["info.txt"] = new RawSource("Build completed");
  callback();
});

三、常见自定义插件实现

1. 生成额外文件(如 manifest.json、版本信息)

js
class GenerateManifestPlugin {
  apply(compiler) {
    compiler.hooks.emit.tapAsync("GenerateManifestPlugin", (compilation, callback) => {
      const manifest = {
        timestamp: Date.now(),
        files: Object.keys(compilation.assets),
      };
      const json = JSON.stringify(manifest, null, 2);
      compilation.assets["manifest.json"] = {
        source: () => json,
        size: () => json.length,
      };
      callback();
    });
  }
}

2. 修改最终输出的 HTML(配合 HtmlWebpackPlugin)

js
class HtmlModifyPlugin {
  apply(compiler) {
    compiler.hooks.compilation.tap("HtmlModifyPlugin", (compilation) => {
      const HtmlWebpackPlugin = require("html-webpack-plugin");
      const hooks = HtmlWebpackPlugin.getHooks(compilation);
      hooks.beforeEmit.tapAsync("HtmlModifyPlugin", (data, cb) => {
        data.html = data.html.replace(/<body>/, '<body><div id="custom">Inject</div>');
        cb(null, data);
      });
    });
  }
}

3. 打包后自动上传 CDN

js
class UploadToCDNPlugin {
  apply(compiler) {
    compiler.hooks.afterEmit.tapPromise("UploadToCDNPlugin", async (compilation) => {
      const assets = compilation.assets;
      for (const [filename, source] of Object.entries(assets)) {
        const content = source.source();
        // 调用云存储 SDK 上传
        await uploadToCDN(filename, content);
      }
    });
  }
}

4. 移除指定注释内容

js
class RemoveCommentsPlugin {
  apply(compiler) {
    compiler.hooks.compilation.tap("RemoveCommentsPlugin", (compilation) => {
      // 使用 TerserPlugin 的 hooks 或直接修改 assets
      compilation.hooks.processAssets.tap(
        {
          name: "RemoveCommentsPlugin",
          stage: compilation.PROCESS_ASSETS_STAGE_OPTIMIZE,
        },
        (assets) => {
          for (const [filename, source] of Object.entries(assets)) {
            const content = source.source().toString();
            const cleaned = content.replace(/\/\*.*?\*\//gs, "").replace(/\/\/.*$/gm, "");
            // 更新 assets
            compilation.updateAsset(filename, new RawSource(cleaned));
          }
        },
      );
    });
  }
}

四、使用 webpack-sources 处理代码字符串

Webpack 内部使用 webpack-sources 库来表示源代码,支持多种 Source 类型,可以进行拼接、修改、缓存等操作。

Source 类型用途
RawSource原始字符串内容
ConcatSource拼接多个 Source
CachedSource缓存内部 Source 结果,避免重复计算
ReplaceSource在原始内容中进行替换操作(保留 source map)

示例:在生成的 JS 文件头部添加注释

js
const { RawSource, ConcatSource } = require("webpack-sources");

class AddHeaderPlugin {
  apply(compiler) {
    compiler.hooks.emit.tapAsync("AddHeaderPlugin", (compilation, callback) => {
      const header = new RawSource("/* This file generated by Webpack */\n");
      for (const filename in compilation.assets) {
        if (filename.endsWith(".js")) {
          const original = compilation.assets[filename];
          const combined = new ConcatSource(header, original);
          compilation.assets[filename] = combined;
        }
      }
      callback();
    });
  }
}

替换内容(保留 source map)

js
const { ReplaceSource } = require("webpack-sources");
// 在某个 loader 内,也可在插件中使用
const replaceSource = new ReplaceSource(originalSource);
replaceSource.replace(0, 10, "new content");
replaceSource.insert(15, "inserted");
const result = replaceSource.source(); // String
result.sourceAndMap; // { source, map }

五、Node 调试 Webpack 插件

使用 Chrome DevTools 可以断点调试插件代码。

1. 方法一:命令行启动

bash
node --inspect-brk ./node_modules/webpack/bin/webpack.js --config webpack.config.js
  • --inspect-brk:在第一行代码处断点,等待调试器连接。
  • 打开 Chrome 浏览器,访问 chrome://inspect,点击「Open dedicated DevTools for Node」。

2. 方法二:VS Code 调试配置

.vscode/launch.json 中添加:

json
{
  "version": "0.2.0",
  "configurations": [
    {
      "type": "node",
      "request": "launch",
      "name": "Debug Webpack",
      "cwd": "${workspaceFolder}",
      "runtimeExecutable": "node",
      "args": [
        "--inspect-brk",
        "./node_modules/webpack/bin/webpack.js",
        "--config",
        "webpack.config.js"
      ]
    }
  ]
}

然后在插件代码中直接打断点即可。

3. 调试技巧

  • 在插件的 apply 方法内设置 debugger 语句。
  • 使用 console.log(compiler.hooks) 查看可用的钩子和参数。
  • 通过 compiler.optionscompilation.assets 检查当前状态。

总结

方面要点
基本架构apply(compiler) + 钩子注册(tap/tapAsync/tapPromise
Compiler全局单例,包含配置、文件系统、生命周期钩子
Compilation每次构建的上下文,包含 moduleschunksassets(关键修改点)
常见插件生成额外文件、修改 HTML、上传 CDN、移除注释等
webpack-sourcesRawSourceConcatSourceReplaceSource 处理代码字符串
调试--inspect-brk + Chrome DevTools 或 VS Code 启动配置

开发高级插件时,注意钩子的执行时机(emit 适合修改最终资源,processAssets 是 Webpack 5 推荐的资源处理阶段),并合理使用 webpack-sources 保证 source map 支持。

参考文献

以下链接在编写时均可正常访问:

资料说明
Plugin API官方
编写 Plugin指南

Series

webpack

5 / 6