编写一个 Plugin

插件向第三方开发者暴露了 webpack 引擎的全部潜力。通过使用分阶段构建回调,开发者可以将自己的行为引入到 webpack 构建过程中。构建插件比构建加载器更高级一些,因为你需要了解一些 webpack 的底层内部机制才能进行钩入。准备好阅读一些源代码吧!

创建插件

webpack 插件由以下几部分组成:

  • 一个命名的 JavaScript 函数或 JavaScript 类。
  • 在其原型中定义 apply 方法。
  • 指定一个 事件钩子 进行挂载。
  • 操作 webpack 内部实例的特定数据。
  • 在功能完成后调用 webpack 提供的回调。
// A JavaScript class.
class MyExampleWebpackPlugin {
  // Define `apply` as its prototype method which is supplied with compiler as its argument
  apply(compiler) {
    // Specify the event hook to attach to
    compiler.hooks.emit.tapAsync(
      'MyExampleWebpackPlugin',
      (compilation, callback) => {
        console.log('This is an example plugin!');
        console.log(
          'Here’s the `compilation` object which represents a single build of assets:',
          compilation
        );

        // Manipulate the build using the plugin API provided by webpack
        compilation.addModule(/* ... */);

        callback();
      }
    );
  }
}

基本插件架构

插件是其原型上具有 apply 方法的实例化对象。当安装插件时,webpack 编译器会调用一次此 apply 方法。apply 方法会获得底层 webpack 编译器的引用,从而可以访问编译器回调。插件的结构如下:

class HelloWorldPlugin {
  apply(compiler) {
    compiler.hooks.done.tap(
      'Hello World Plugin',
      (
        stats /* stats is passed as an argument when done hook is tapped.  */
      ) => {
        console.log('Hello World!');
      }
    );
  }
}

module.exports = HelloWorldPlugin;

然后要使用该插件,请在你的 webpack 配置的 plugins 数组中包含一个实例:

// webpack.config.js
var HelloWorldPlugin = require('hello-world');

module.exports = {
  // ... configuration settings here ...
  plugins: [new HelloWorldPlugin({ options: true })],
};

使用 schema-utils 来验证通过插件选项传递的选项。这是一个示例:

import { validate } from 'schema-utils';

// schema for options object
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) {}
}

编译器和编译

在开发插件时,两个最重要的资源是 compilercompilation 对象。理解它们的作用是扩展 webpack 引擎的重要第一步。

class HelloCompilationPlugin {
  apply(compiler) {
    // Tap into compilation hook which gives compilation as argument to the callback function
    compiler.hooks.compilation.tap('HelloCompilationPlugin', (compilation) => {
      // Now we can tap into various hooks available through compilation
      compilation.hooks.optimize.tap('HelloCompilationPlugin', () => {
        console.log('Assets are being optimized.');
      });
    });
  }
}

module.exports = HelloCompilationPlugin;

有关 compilercompilation 和其他重要对象上可用的钩子列表,请参阅 插件 API 文档。

异步事件钩子

一些插件钩子是异步的。要挂载它们,我们可以使用 tap 方法(其行为是同步的),或者使用 tapAsync 方法或 tapPromise 方法(它们是异步方法)。

tapAsync

当我们使用 tapAsync 方法挂载插件时,我们需要调用作为函数最后一个参数提供的回调函数。

class HelloAsyncPlugin {
  apply(compiler) {
    compiler.hooks.emit.tapAsync(
      'HelloAsyncPlugin',
      (compilation, callback) => {
        // Do something async...
        setTimeout(function () {
          console.log('Done with async work...');
          callback();
        }, 1000);
      }
    );
  }
}

module.exports = HelloAsyncPlugin;

tapPromise

当我们使用 tapPromise 方法挂载插件时,我们需要返回一个 Promise,该 Promise 在异步任务完成后解析。

class HelloAsyncPlugin {
  apply(compiler) {
    compiler.hooks.emit.tapPromise('HelloAsyncPlugin', (compilation) => {
      // return a Promise that resolves when we are done...
      return new Promise((resolve, reject) => {
        setTimeout(function () {
          console.log('Done with async work...');
          resolve();
        }, 1000);
      });
    });
  }
}

module.exports = HelloAsyncPlugin;

示例

一旦我们能够与 webpack 编译器和每个独立的编译挂钩,我们能对引擎本身做的事情就变得无限可能。我们可以重新格式化现有文件、创建派生文件,或者生成全新的资产。

让我们编写一个示例插件,它会生成一个名为 assets.md 的新构建文件,其内容将列出我们构建中的所有资产文件。这个插件可能看起来像这样:

class FileListPlugin {
  static defaultOptions = {
    outputFile: 'assets.md',
  };

  // Any options should be passed in the constructor of your plugin,
  // (this is a public API of your plugin).
  constructor(options = {}) {
    // Applying user-specified options over the default options
    // and making merged options further available to the plugin methods.
    // You should probably validate all the options here as well.
    this.options = { ...FileListPlugin.defaultOptions, ...options };
  }

  apply(compiler) {
    const pluginName = FileListPlugin.name;

    // webpack module instance can be accessed from the compiler object,
    // this ensures that correct version of the module is used
    // (do not require/import the webpack or any symbols from it directly).
    const { webpack } = compiler;

    // Compilation object gives us reference to some useful constants.
    const { Compilation } = webpack;

    // RawSource is one of the "sources" classes that should be used
    // to represent asset sources in compilation.
    const { RawSource } = webpack.sources;

    // Tapping to the "thisCompilation" hook in order to further tap
    // to the compilation process on an earlier stage.
    compiler.hooks.thisCompilation.tap(pluginName, (compilation) => {
      // Tapping to the assets processing pipeline on a specific stage.
      compilation.hooks.processAssets.tap(
        {
          name: pluginName,

          // Using one of the later asset processing stages to ensure
          // that all assets were already added to the compilation by other plugins.
          stage: Compilation.PROCESS_ASSETS_STAGE_SUMMARIZE,
        },
        (assets) => {
          // "assets" is an object that contains all assets
          // in the compilation, the keys of the object are pathnames of the assets
          // and the values are file sources.

          // Iterating over all the assets and
          // generating content for our Markdown file.
          const content =
            '# In this build:\n\n' +
            Object.keys(assets)
              .map((filename) => `- ${filename}`)
              .join('\n');

          // Adding new asset to the compilation, so it would be automatically
          // generated by the webpack in the output directory.
          compilation.emitAsset(
            this.options.outputFile,
            new RawSource(content)
          );
        }
      );
    });
  }
}

module.exports = { FileListPlugin };

webpack.config.js

const { FileListPlugin } = require('./file-list-plugin.js');

// Use the plugin in your webpack configuration:
module.exports = {
  // …

  plugins: [
    // Adding the plugin with the default options
    new FileListPlugin(),

    // OR:

    // You can choose to pass any supported options to it:
    new FileListPlugin({
      outputFile: 'my-assets.md',
    }),
  ],
};

这将生成一个具有所选名称的 Markdown 文件,其内容如下所示:

# In this build:

- main.css
- main.js
- index.html

不同类型的插件

插件可以根据其挂载的事件钩子进行分类。每个事件钩子都被预定义为同步、异步、瀑布式或并行钩子,并通过内部的 call/callAsync 方法调用。支持或可挂载的钩子列表通常在 this.hooks 属性中指定。

例如

this.hooks = {
  shouldEmit: new SyncBailHook(['compilation']),
};

这表示唯一支持的钩子是 shouldEmit,它是一个 SyncBailHook 类型的钩子,并且传递给任何挂载 shouldEmit 钩子的插件的唯一参数是 compilation

支持的各种钩子类型有:

同步钩子

  • SyncHook

    • 定义为 new SyncHook([params])
    • 使用 tap 方法挂载。
    • 使用 call(...params) 方法调用。
  • Bail 钩子

    • 使用 SyncBailHook[params] 定义
    • 使用 tap 方法挂载。
    • 使用 call(...params) 方法调用。

    在这些类型的钩子中,每个插件回调将按照特定的 args 依次调用。如果任何插件返回了除 undefined 之外的任何值,则该值将由钩子返回,并且不再调用后续的插件回调。许多有用的事件,如 optimizeChunksoptimizeChunkModules 都是 SyncBailHooks。

  • Waterfall 钩子

    • 使用 SyncWaterfallHook[params] 定义
    • 使用 tap 方法挂载。
    • 使用 call(...params) 方法调用

    在这里,每个插件都将按照前一个插件的返回值作为参数依次调用。插件必须考虑其执行顺序。它必须接受来自已执行的前一个插件的参数。第一个插件的值为 init。因此,瀑布式钩子至少需要提供 1 个参数。这种模式用于与 webpack 模板相关的 Tapable 实例,例如 ModuleTemplateChunkTemplate 等。

异步钩子

  • Async Series 钩子

    • 使用 AsyncSeriesHook[params] 定义
    • 使用 tap/tapAsync/tapPromise 方法挂载。
    • 使用 callAsync(...params) 方法调用

    插件处理函数在调用时会带上所有参数和一个签名为 (err?: Error) -> void 的回调函数。处理函数按照注册顺序调用。在所有处理函数调用完毕后,会调用 callback。这也是 emitrun 等事件常用的模式。

  • 异步瀑布 插件将以瀑布式异步应用。

    • 使用 AsyncWaterfallHook[params] 定义
    • 使用 tap/tapAsync/tapPromise 方法挂载。
    • 使用 callAsync(...params) 方法调用

    插件处理函数在调用时会带上当前值和一个签名为 (err: Error, nextValue: any) -> void. 调用时,nextValue 是下一个处理程序的当前值。第一个处理程序的当前值是 init。所有处理程序应用后,使用最后一个值调用回调。如果任何处理程序为 err 传递了值,则回调将使用此错误被调用,并且不再调用更多处理程序。此插件模式适用于 before-resolveafter-resolve 等事件。

  • Async Series Bail

    • 使用 AsyncSeriesBailHook[params] 定义
    • 使用 tap/tapAsync/tapPromise 方法挂载。
    • 使用 callAsync(...params) 方法调用
  • Async Parallel

    • 使用 AsyncParallelHook[params] 定义
    • 使用 tap/tapAsync/tapPromise 方法挂载。
    • 使用 callAsync(...params) 方法调用

配置默认值

Webpack 在应用插件默认值之后应用配置默认值。这使得插件可以拥有自己的默认值,并提供了一种创建配置预设插件的方法。

10 贡献者

slavafomintbroadleynveenjainiamakulovbyzykfranjohn21EugeneHlushkosnitin315rahul3vjamesgeorge007