关于 Webpack Plugin 的原理与开发

You,7 min read

关于 Webpack Plugin 的原理与开发

创建插件

webpack 插件由以下组成:

// 一个 JavaScript 类
class MyExampleWebpackPlugin {
  // 在插件函数的 prototype 上定义一个 `apply` 方法,以 compiler 为参数。
  apply(compiler) {
    // 指定一个挂载到 webpack 自身的事件钩子。
    compiler.hooks.emit.tapAsync(
      'MyExampleWebpackPlugin',
      (compilation, callback) => {
        console.log('这是一个示例插件!');
        console.log(
          '这里表示了资源的单次构建的 `compilation` 对象:',
          compilation
        );
 
        // 用 webpack 提供的插件 API 处理构建过程
        compilation.addModule(/* ... */);
 
        callback();
      }
    );
  }
}

基本插件架构

插件是由「具有 apply 方法的 prototype 对象」所实例化出来的。

这个 apply 方法在安装插件时,会被 webpack compiler 调用一次。apply 方法可以接收一个 webpack compiler 对象的引用,从而可以在回调函数中访问到 compiler 对象。

一个插件结构如下:

class HelloWorldPlugin {
  apply(compiler) {
    compiler.hooks.done.tap(
      'Hello World Plugin',
      (
        stats /* 绑定 done 钩子后,stats 会作为参数传入。 */
      ) => {
        console.log('Hello World!');
      }
    );
  }
}
 
module.exports = HelloWorldPlugin;

然后,要安装这个插件,只需要在你的 webpack 配置的 plugin 数组中添加一个实例:

// webpack.config.js
var HelloWorldPlugin = require('hello-world');
 
module.exports = {
  // ... 这里是其他配置 ...
  plugins: [new HelloWorldPlugin({ options: true })],
};

使用 schema-utils (opens in a new tab) 来校验传入插件的选项。

这里是个例子

import { validate } from 'schema-utils';
 
// 选项对象的 schema
const schema = {
  type: 'object',
  properties: {
    test: {
      type: 'string',
    },
  },
};
 
export default class HelloWorldPlugin {
  constructor(options = {}) {
    validate(schema, options, {
      name: 'Hello World Plugin',
      baseDataPath: 'options',
    });
  }
 
  apply(compiler) {}
}

Compiler 和 Compilation

在插件开发中最重要的两个资源就是 compiler (opens in a new tab) 和 compilation (opens in a new tab) 对象。

理解它们的角色是扩展 webpack 引擎重要的第一步。

class HelloCompilationPlugin {
  apply(compiler) {
    // 指定一个挂载到 compilation 的钩子,
    // 回调函数的参数为 compilation 。
    compiler.hooks.compilation.tap('HelloCompilationPlugin', (compilation) => {
      // 现在可以通过 compilation 对象绑定各种钩子
      compilation.hooks.optimize.tap('HelloCompilationPlugin', () => {
        console.log('资源已经优化完毕。');
      });
    });
  }
}
 
module.exports = HelloCompilationPlugin;

compiler 和 compilation 以及其他重要对象提供的钩子清单,请查看 plugins API (opens in a new tab) 文档

这个还是要看webpack 官方文档 里面的hooks 和 API 太多了 按需去了解 不必全部看完 常用的 一些 hook 和 api 即可

异步编译插件

有些插件钩子是异步的。我们可以像同步方式一样用 tap 方法来绑定,也可以用 tapAsync 或 tapPromise 这两个异步方法来绑定

tapAsync

当我们用 tapAsync 方法来绑定插件时,_必须_调用函数的最后一个参数 callback 指定的回调函数

class HelloAsyncPlugin {
  apply(compiler) {
    compiler.hooks.emit.tapAsync(
      'HelloAsyncPlugin',
      (compilation, callback) => {
        // 执行某些异步操作...
        setTimeout(function () {
          console.log('异步任务完成...');
          callback();
        }, 1000);
      }
    );
  }
}
 
module.exports = HelloAsyncPlugin;

tapPromise

当我们用 tapPromise 方法来绑定插件时,_必须_返回一个 pormise ,异步任务完成后 resolve

class HelloAsyncPlugin {
  apply(compiler) {
    compiler.hooks.emit.tapPromise('HelloAsyncPlugin', (compilation) => {
      // 返回一个 pormise ,异步任务完成后 resolve
      return new Promise((resolve, reject) => {
        setTimeout(function () {
          console.log('异步任务完成...');
          resolve();
        }, 1000);
      });
    });
  }
}
 
module.exports = HelloAsyncPlugin;

示例

一旦能我们深入理解 webpack compiler 和每个独立的 compilation,我们依赖 webpack 引擎将有无限多的事可以做。

我们可以重新格式化已有的文件,创建衍生的文件,或者制作全新的生成文件。

让我们来写一个简单的示例插件,生成一个叫做 assets.md 的新文件;

文件内容是所有构建生成的文件的列表。这个插件大概像下面这样:

class FileListPlugin {
  static defaultOptions = {
    outputFile: 'assets.md',
  };
 
  // 需要传入自定义插件构造函数的任意选项
  //(这是自定义插件的公开API)
  constructor(options = {}) {
    // 在应用默认选项前,先应用用户指定选项
    // 合并后的选项暴露给插件方法
    // 记得在这里校验所有选项
    this.options = { ...FileListPlugin.defaultOptions, ...options };
  }
 
  apply(compiler) {
    const pluginName = FileListPlugin.name;
 
    // webpack 模块实例,可以通过 compiler 对象访问,
    // 这样确保使用的是模块的正确版本
    // (不要直接 require/import webpack)
    const { webpack } = compiler;
 
    // Compilation 对象提供了对一些有用常量的访问。
    const { Compilation } = webpack;
 
    // RawSource 是其中一种 “源码”("sources") 类型,
    // 用来在 compilation 中表示资源的源码
    const { RawSource } = webpack.sources;
 
    // 绑定到 “thisCompilation” 钩子,
    // 以便进一步绑定到 compilation 过程更早期的阶段
    compiler.hooks.thisCompilation.tap(pluginName, (compilation) => {
      // 绑定到资源处理流水线(assets processing pipeline)
      compilation.hooks.processAssets.tap(
        {
          name: pluginName,
 
          // 用某个靠后的资源处理阶段,
          // 确保所有资源已被插件添加到 compilation
          stage: Compilation.PROCESS_ASSETS_STAGE_SUMMARIZE,
        },
        (assets) => {
          // "assets" 是一个包含 compilation 中所有资源(assets)的对象。
          // 该对象的键是资源的路径,
          // 值是文件的源码
 
          // 遍历所有资源,
          // 生成 Markdown 文件的内容
          const content =
            '# In this build:\n\n' +
            Object.keys(assets)
              .map((filename) => `- ${filename}`)
              .join('\n');
 
          // 向 compilation 添加新的资源,
          // 这样 webpack 就会自动生成并输出到 output 目录
          compilation.emitAsset(
            this.options.outputFile,
            new RawSource(content)
          );
        }
      );
    });
  }
}
 
module.exports = { FileListPlugin };

webpack.config.js

const { FileListPlugin } = require('./file-list-plugin.js');
 
// 在 webpack 配置中使用自定义的插件:
module.exports = {
  // …
 
  plugins: [
    // 添加插件,使用默认选项
    new FileListPlugin(),
 
    // 或者:
 
    // 使用任意支持的选项
    new FileListPlugin({
      outputFile: 'my-assets.md',
    }),
  ],
};

插件的不同类型

Synchronous(同步)Hooks

Asyncchronous(异步Hooks)

默认配置

2026 © Lizhenyui.