插件向第三方开发者公开了 webpack 引擎的全部潜力。使用分阶段构建回调,开发者可以在 webpack 构建过程中引入自己的行为。构建插件比构建加载器更高级,因为您需要了解一些 webpack 底层内部机制才能接入它们。准备好阅读一些源代码吧!
一个 webpack 插件包含
apply
方法。// 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) {}
}
在开发插件时,两个最重要的资源是 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
方法来利用插件时,我们需要调用作为最后一个参数传递给我们的函数的回调函数。
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
方法来利用插件时,我们需要返回一个 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 之外的任何值,则该值将由钩子返回,并且不会再调用任何插件回调。许多有用的事件,如 optimizeChunks
、optimizeChunkModules
,都是 SyncBailHooks。
瀑布钩子
SyncWaterfallHook[params]
定义tap
方法接入。call(...params)
方法调用这里,每个插件都使用前一个插件的返回值中的参数依次调用。插件必须考虑其执行顺序。它必须接受先前执行的插件的参数。第一个插件的值是 init
。因此,瀑布钩子至少必须提供 1 个参数。这种模式用于与 webpack 模板(如 ModuleTemplate
、ChunkTemplate
等)相关的 Tapable 实例中。
Async Series 钩子
AsyncSeriesHook[params]
定义tap
/tapAsync
/tapPromise
方法接入。插件处理函数使用所有参数和一个回调函数调用,回调函数的签名为 `(err?: Error) -> void`。处理函数按注册顺序调用。`callback` 在所有处理函数调用后调用。这也是 `emit`、`run` 等事件常用的模式。
异步瀑布 插件将以瀑布方式异步应用。
tap
/tapAsync
/tapPromise
方法接入。插件处理函数使用当前值和一个回调函数调用,回调函数的签名为 `(err: Error, nextValue: any) -> void`。当调用时,`nextValue` 是下一个处理函数的当前值。第一个处理函数的当前值是 `init`。在所有处理函数应用后,回调函数使用最后一个值调用。如果任何处理函数传递了 `err` 的值,则回调函数使用此错误调用,不再调用更多处理函数。此插件模式适用于 `before-resolve` 和 `after-resolve` 等事件。
异步串行跳过
tap
/tapAsync
/tapPromise
方法接入。异步并行
tap
/tapAsync
/tapPromise
方法接入。Webpack 在应用插件默认值后应用配置默认值。这允许插件具有自己的默认值,并提供了一种创建配置预设插件的方法。