编写插件

插件向第三方开发者公开了 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 方法的实例化对象。此 apply 方法在安装插件时由 webpack 编译器调用一次。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。

  • 瀑布钩子

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

    这里,每个插件都使用前一个插件的返回值中的参数依次调用。插件必须考虑其执行顺序。它必须接受先前执行的插件的参数。第一个插件的值是 init。因此,瀑布钩子至少必须提供 1 个参数。这种模式用于与 webpack 模板(如 ModuleTemplateChunkTemplate 等)相关的 Tapable 实例中。

异步钩子

  • Async Series 钩子

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

    插件处理函数使用所有参数和一个回调函数调用,回调函数的签名为 `(err?: Error) -> void`。处理函数按注册顺序调用。`callback` 在所有处理函数调用后调用。这也是 `emit`、`run` 等事件常用的模式。

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

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

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

  • 异步串行跳过

    • 使用 `AsyncSeriesBailHook[params]` 定义
    • 使用 tap/tapAsync/tapPromise 方法接入。
    • 使用 `callAsync(...params)` 方法调用
  • 异步并行

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

配置默认值

Webpack 在应用插件默认值后应用配置默认值。这允许插件具有自己的默认值,并提供了一种创建配置预设插件的方法。

10 位贡献者

slavafomintbroadleynveenjainiamakulovbyzykfranjohn21EugeneHlushkosnitin315rahul3vjamesgeorge007