插件向第三方开发者暴露了 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
方法的实例化对象。当安装插件时,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) {}
}
在开发插件时,两个最重要的资源是 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。
Waterfall 钩子
SyncWaterfallHook[params]
定义tap
方法挂载。call(...params)
方法调用在这里,每个插件都将按照前一个插件的返回值作为参数依次调用。插件必须考虑其执行顺序。它必须接受来自已执行的前一个插件的参数。第一个插件的值为 init
。因此,瀑布式钩子至少需要提供 1 个参数。这种模式用于与 webpack 模板相关的 Tapable 实例,例如 ModuleTemplate
、ChunkTemplate
等。
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
等事件。
Async Series Bail
AsyncSeriesBailHook[params]
定义tap
/tapAsync
/tapPromise
方法挂载。callAsync(...params)
方法调用Async Parallel
AsyncParallelHook[params]
定义tap
/tapAsync
/tapPromise
方法挂载。callAsync(...params)
方法调用Webpack 在应用插件默认值之后应用配置默认值。这使得插件可以拥有自己的默认值,并提供了一种创建配置预设插件的方法。