プラグインの作成

プラグインは、サードパーティの開発者に対して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) {}
}

コンパイラとコンパイル

プラグイン開発中に最も重要な2つのリソースは、`compiler`オブジェクトと`compilation`オブジェクトです。それらの役割を理解することは、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;

`compiler`、`compilation`、その他の重要なオブジェクトで使用可能なフックのリストについては、プラグイン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を返す必要があります。

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',
    }),
  ],
};

これにより、次のような名前のマークダウンファイルが生成されます。

# In this build:

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

異なるプラグインの形状

プラグインは、タップするイベントフックに基づいてタイプに分類できます。すべてのイベントフックは、同期、非同期、ウォーターフォール、またはパラレルフックとして事前に定義されており、フックは内部的にcall/callAsyncメソッドを使用して呼び出されます。サポートされているか、タップできるフックのリストは、通常`this.hooks`プロパティに指定されています。

例えば

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

これは、サポートされている唯一のフックが`SyncBailHook`タイプの`shouldEmit`であり、`shouldEmit`フックにタップするプラグインに渡される唯一のパラメータが`compilation`であることを示しています。

サポートされているさまざまなタイプのフックは次のとおりです。

同期フック

  • SyncHook

    • `new SyncHook([params])`として定義されます。
    • `tap`メソッドを使用してタップします。
    • `call(...params)`メソッドを使用して呼び出されます。
  • Bail Hooks

    • `SyncBailHook[params]`を使用して定義されます。
    • `tap`メソッドを使用してタップします。
    • `call(...params)`メソッドを使用して呼び出されます。

    これらのタイプのフックでは、各プラグインコールバックは、特定の`args`を使用して順番に呼び出されます。任意のプラグインによって`undefined`以外の値が返されると、その値がフックによって返され、それ以上のプラグインコールバックは呼び出されません。`optimizeChunks`、`optimizeChunkModules`などの多くの有用なイベントはSyncBailHooksです。

  • ウォーターフォールフック

    • `SyncWaterfallHook[params]`を使用して定義されます。
    • `tap`メソッドを使用してタップします。
    • `call(...params)`メソッドを使用して呼び出されます。

    ここでは、各プラグインは、前のプラグインの戻り値からの引数を使用して順番に呼び出されます。プラグインは、その実行順序を考慮する必要があります。前のプラグインの実行結果からの引数を受け入れる必要があります。最初のプラグインの値は`init`です。したがって、ウォーターフォールフックには少なくとも1つのパラメータを供給する必要があります。このパターンは、`ModuleTemplate`、`ChunkTemplate`など、webpackテンプレートに関連するTapableインスタンスで使用されます。

非同期フック

  • Async Series Hook

    • `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-resolveafter-resolveのようなイベントで想定されています。

  • 非同期直列中断

    • AsyncSeriesBailHook[params]を使用して定義
    • `tap` / `tapAsync` / `tapPromise`メソッドを使用してタップします。
    • `callAsync(...params)`メソッドを使用して呼び出されます。
  • 非同期並列

    • AsyncParallelHook[params]を使用して定義
    • `tap` / `tapAsync` / `tapPromise`メソッドを使用してタップします。
    • `callAsync(...params)`メソッドを使用して呼び出されます。

設定のデフォルト値

Webpackは、プラグインのデフォルト値が適用された後に、設定のデフォルト値を適用します。これにより、プラグインは独自のデフォルト値を備えることができ、設定プリセットプラグインを作成する方法が提供されます。

10 貢献者

slavafomintbroadleynveenjainiamakulovbyzykfranjohn21EugeneHlushkosnitin315rahul3vjamesgeorge007