撰寫外掛程式

外掛向第三方開發人員公開 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 方法使用外掛時,我們需要傳回一個承諾,當我們的非同步任務完成時會解析。

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

它表示唯一支援的掛鉤是 shouldEmit,這是 SyncBailHook 類型的掛鉤,傳遞給任何存取 shouldEmit 掛鉤的插件的唯一參數是 compilation

支援的各種掛鉤類型為

同步掛鉤

  • SyncHook

    • 定義為 new SyncHook([params])
    • 使用 tap 方法存取。
    • 使用 call(...params) 方法呼叫。
  • Bail 掛鉤

    • 使用 SyncBailHook[params] 定義
    • 使用 tap 方法存取。
    • 使用 call(...params) 方法呼叫。

    在這些類型的掛鉤中,每個插件回呼都會一個接一個地使用特定 args 呼叫。如果任何插件傳回的值不是未定義,則掛鉤會傳回該值,且不會呼叫進一步的插件回呼。許多有用的事件,例如 optimizeChunksoptimizeChunkModules 是 SyncBailHooks。

  • Waterfall 掛鉤

    • 使用 SyncWaterfallHook[params] 定義
    • 使用 tap 方法存取。
    • 使用 call(...params) 方法呼叫

    在這裡,每個插件都會一個接一個地使用前一個插件的傳回值中的參數呼叫。插件必須考慮其執行順序。它必須接受已執行的前一個插件的參數。第一個插件的值為 init。因此,至少必須為瀑布掛鉤提供 1 個參數。此模式用於與 webpack 範本(例如 ModuleTemplateChunkTemplate 等)相關的 Tapable 實例中。

非同步掛鉤

  • 非同步序列掛鉤

    • 使用 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 等事件。

  • 非同步串列暫停

    • 使用 AsyncSeriesBailHook[params] 定義
    • 使用 tap/tapAsync/tapPromise 方法存取。
    • 使用 callAsync(...params) 方法呼叫
  • 非同步平行

    • 使用 AsyncParallelHook[params] 定義
    • 使用 tap/tapAsync/tapPromise 方法存取。
    • 使用 callAsync(...params) 方法呼叫

組態預設值

Webpack 會在套用外掛預設值後套用組態預設值。這讓外掛可以提供自己的預設值,並提供建立組態預設值外掛的方法。

10 貢獻者

slavafomintbroadleynveenjainiamakulovbyzykfranjohn21EugeneHlushkosnitin315rahul3vjamesgeorge007