官方解释:【Compiler and Compilation】以及 【Writing a Plugin】,翻译并展开如下:
开发插件时最重要的两个概念是 compiler 编译器和 compilation 编译对象。理解它们的角色是扩展 webpack 引擎重要的第一步。
一个 webpack 插件需要具备:
- 是一个JavaScript命名函数或者JavaScript类
- 定义一个名为 apply 的原型方法(插件最核心的部分)
- 指定要钩入/监听的事件 hook
- 处理webpack内部实例特定的数据
- 功能完成后,调用webpack提供的回调
传入的 compiler 是 webpack 初始化时创建的 Compiler 实例,它上面的 hooks 属性,用于将插件注册到 compiler 生命周期中的各种钩子事件上。
// 一个插件类基本结构示例:
class HelloCompilationPlugin {
constructor(options = {}) {
// 省略构造器部分
}
// 调用原型方法 apply 并传入 compiler 对象
apply(compiler) {
// 给 compilation 钩子注册 'HelloCompilationPlugin',回调会在 compilation 对象创建之后触发
compiler.hooks.compilation.tap('HelloCompilationPlugin', (compilation) => {
// 回调参数是 compilation 对象,因此这里可以使用各种可用的 compilation hooks(钩子)
compilation.hooks.optimize.tap('HelloCompilationPlugin', () => {
// 优化阶段开始时触发
console.log('资源正在优化');
});
});
// 在 emit 阶段,即输出 asset 到 output 目录之前触发回调函数
compiler.hooks.emit.tapAsync(
'HelloCompilationPlugin',
(compilation, callback) => {
console.log('This is an example plugin!');
console.log(
'`compilation`对象,即资源的一个单一版本的构建',
compilation
);
// 使用webpack提供的 plugin API 操作构建
compilation.addModule(/* ... */);
callback();
}
);
}
}
module.exports = HelloCompilationPlugin;
在 webpack 中以new XxxPlugin
的方式配置插件,用于在 Compiler 实例对象上注册
// webpack.config.js
const HelloCompilationPlugin = require('./HelloCompilationPlugin.js');
module.exports = {
plugins: [
//... 其他插件
// 创建插件实例
new HelloCompilationPlugin({...options})
]
}
Compiler
模块是 webpack 的主要引擎,它扩展自 Tapable
类。在执行 webpack 构建的准备阶段,会创建一个Compiler
的实例,然后配置项传递的实例化插件以及webpack内置插件都会在该 compiler
对象上注册。具体就是依次调用插件的 apply 方法,并将 compiler 对象 (包含webpack的各种配置信息) 传进去供 plugin 使用,compiler 包含整个构建流程的全部钩子,通过它可以把控整个 webpack 构建周期。在运行期间 compiler 会根据 webpack 不同阶段触发的各种事件钩子,执行插件附加/绑定在 hook 上的函数。 compiler 只是负责维持生命周期运行的功能,所有的加载、打包和写入工作,都被委托到注册过的插件上了。webpack
使用 WebpackOptionsDefaulter 和 WebpackOptionsApply 来配置 Compiler
实例以及所有内置插件。
Compiler 类实例化并注册 plugins 后,若 webpack 函数接收了回调callback,会执行compiler.run()方法,webpack即刻开启编译之旅。如果未指定callback回调,则需要用户自己调用run方法来启动编译。
上面说的 Compiler 实例化、插件注册及启动编译 (run or watch方法),详见下面/lib/webpack.js
源码[简化版]
// /lib/webpack.js
const webpack = (options, callback) => {
// 创建 Compiler 类的实例
const compiler = new Compiler(options.context);
compiler.options = options;
// 注册所有自定义插件
if (Array.isArray(options.plugins)) {
// 遍历传入的 webpack 配置中的实例化插件数组
for (const plugin of options.plugins) {
if (typeof plugin === "function") {
// 在compiler对象的作用域下调用plugin构造函数,即this指向compiler;同时把compiler对象当作参数传过去。并且compiler对象会继承plugin的所有属性、方法
plugin.call(compiler, compiler);
} else {
// 如果 plugin 是其他类型,就执行plugin对象的apply方法。
// plugins 数组的内容一般都是一个个插件实例化对象,也就是 object。
//
plugin.apply(compiler);
}
}
}
applyWebpackOptionsDefaults(options);
// 触发 compiler 的 两个 hook: environment,afterEnvironment
compiler.hooks.environment.call();
compiler.hooks.afterEnvironment.call();
// 根据 options 的配置不同,注册激活一些默认自带的插件和 resolverFactory.hooks
// 大部分插件的作用是往 compiler.hooks:compilation,thisCompilation 里注册一些事件
new WebpackOptionsApply().process(options, compiler);
compiler.hooks.initialize.call();
// 获取是否以watch监听模式启动的 webpack 以及 监听相关配置
let watch = options.watch || false;
let watchOptions = options.watchOptions || {};
if (callback) { // 如果传递了回调
if (watch) { // 配置传了 watch 则调用监听模式启动 webpack
compiler.watch(watchOptions, callback);
} else { // 启动 compiler.run,即开启编译工作, webpack 的核心构建流程
compiler.run((err, stats) => {
// stats 对象是编译过程中的有用信息, 包括:
//* 错误和警告(如果有的话)
//* 计时信息
//* module 和 chunk 信息
// webpack CLI 正是基于这些信息在控制台 展示友好的格式输出。
compiler.close(err2 => {
callback(err || err2, stats);
});
});
}
return compiler;
} else {
if (watch) {
util.deprecate(() => {}, "watch模式必须提供callback回调函数!", "DEP_WEBPACK_WATCH_WITHOUT_CALLBACK")();
}
return compiler;
}
}
module.exports = webpack;
那 Tapable 是什么?GitHub文档
它是一个管理钩子事件监听与触发的小型库,export 了许多Hook类(hook 构造函数),如 SyncHook、SyncBailHook 等,可以用来为插件创建 hooks。除此之外,还暴露了 tap,tapAsync
和 tapPromise
这些绑定 hook 的方法(除此之外还有 intercept -钩子拦截器),用来向 webpack 注入自定义构建的步骤。整体类似发布订阅模式。
tap: (name: string | Tap, fn: (context?, ...args) => Result) => void
tapAsync: (name: string | Tap, fn: (context?, ...args, callback: (err, result: Result) => void) => void) => void
tapPromise: (name: string | Tap, fn: (context?, ...args) => Promise<Result>) => void
第一个参数 name 或 Tap 对象:name
是绑定事件名,通常用来识别插件,因此一般就传插件名称;Tab
对象可以添加一些具体的信息。
第二个参数是回调函数,即该事件被触发时,需要做的事情。回调的 context
参数是可选值,表示上下文对象,且需要第一个参数传Tab对象:{ name: 'myPluginxxx', context: true, ... }
整体和事件侦听器的机制类似。
其中Tab参数的接口:
interface Tap {
name: string,
type: string
fn: Function,
stage: number,
context: boolean,
before?: string | Array
}
同步 hook 只能使用 tap 方法;而异步 hook 除了 tapAsync 和 tapPromise 这些异步方法,也支持用 tap 方法让 hook 以同步方式运行。
当使用 tapAsync method来访问插件时,需要调用作为函数的最后一个参数提供的回调函数。
class HelloAsyncPlugin {
compiler.hooks.emit.tapAsync(
'HelloAsyncPlugin',
(compilation, callback) => {
// Do something async...
console.log('以异步方式触及运行钩子');
setTimeout(function () {
console.log('Done with async work...');
callback();
}, 1000);
}
);
}
}
module.exports = HelloAsyncPlugin;
当我们使用 tapPromise method来访问插件时,则需要返回一个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('以异步的方式触发具有延迟操作的钩子。');
resolve();
}, 1000);
});
});
// 在回调用上 async await
compiler.hooks.run.tapPromise('MyPlugin', async (source, target, routesList) => {
await new Promise((resolve) => setTimeout(resolve, 1000));
console.log('以异步的方式触发具有延迟操作的钩子。');
}
);
}
}
module.exports = HelloAsyncPlugin;
我们可以有多种方式 hook 到 compiler 中,hook将编译一个方法,可以让各种插件都以最合适有效的方式去运行。
创建钩子的方法:const hook = new SyncHook(["arg1", "arg2", "arg3"])
在类的 constructor 的 this.hooks 内用生成 Hook 类实例的方式定义 hooks,表明它们的类型并传入参数名称数组;
在 webpack 运行特定阶段(比如 compiler.run 和 compile)通过call
、callAsync
、promise
调用这些 hook,声明注册在这个 hook 上的插件 (内的方法) 触发的时机。类实似于发布-订阅模式中的发布事件。
插件通过tap
、tapAsync
等方法订阅 Compiler 或 Compilation 实例的 hook,方法内可以调用 api 进行一些处理。webpack 在运行过程中,当这些特定的 hook 事件被广播时,订阅了该 hook 的插件在监听到后就会执行绑定的逻辑。
掌握 webpack 流程中 compiler、compilation 对象事件钩子触发的时机,就是掌握如何写一个插件的关键。
具体让我们看Compiler类源码:
// /lib/Compiler.js
const {
SyncHook,
SyncBailHook,
AsyncParallelHook,
AsyncSeriesHook
} = require("tapable");
class Compiler {
constructor(context) {
// 定义一堆hook,done,beforeRun,run,emit等等
this.hooks = Object.freeze({
/** @type {SyncBailHook<[Compilation], boolean>} */
run: new AsyncSeriesHook(["compiler"]), // 在开始读取records之前调用
/** @type {SyncHook<[Compilation, CompilationParams]>} */
thisCompilation: new SyncHook(["compilation", "params"]), // 初始化 compilation 时调用,在触发 compilation 事件之前调用
/** @type {AsyncSeriesHook<[Compilation]>} */
emit: new AsyncSeriesHook(["compilation"]), // 输出 asset 到 output 目录之前执行
/** @type {AsyncSeriesHook<[Compilation]>} */
afterEmit: new AsyncSeriesHook(["compilation"]), // 输出 asset 到 output 目录之后执行
/** @type {AsyncSeriesHook<[Stats]>} */
done: new AsyncSeriesHook(["stats"]), // 在 compilation 完成时执行
})
}
watch(watchOptions, handler) {} // 以监听模式执行 webpack 打包的方法
run(callback) { // run 即为执行 webpack 打包的主流程函数
const onCompiled = (err, compilation) => {})
const run = () => {
this.hooks.beforeRun.callAsync(this, err => {
this.hooks.run.callAsync(this, err => {
if (err) return finalCallback(err);
this.readRecords(err => { // 读取之前的 records
if (err) return finalCallback(err);
this.compile(onCompiled); // 在 compile 过程后调用 onCompiled,主要用于输出构建资源
});
});
});
};
}
compile(callback) { // compile 是真正进行编译的方法,最终会把所有原始资源编译为目标资源。
const params = this.newCompilationParams();
this.hooks.beforeCompile.callAsync(params, err => {
if (err) return callback(err);
this.hooks.compile.call(params);
// createCompilation方法主要就是清除之前的compilation,重新实例化一个Compilation
const compilation = this.createCompilation();
compilation.name = this.name;
compilation.records = this.records;
// 触发compiler.hooks:thisCompilation 和 compilation
// 注册plugins阶段在这两个钩子注册的事件在拿到compilation对象后开始执行
this.hooks.thisCompilation.call(compilation, params);
this.hooks.compilation.call(compilation, params);
return compilation;
}
}
}
Compiler 有几个重要的方法,watch
和run
是启动/执行 webpack 构建的函数,而compile
是负责编译的。 它触发 compile 钩子并实例化了一个 compilation,再触发自己的 make 钩子把 compilation 对象作为参数传过去。在后续编译过程中会触发 Compilation 的一系列海量 hooks:如buildModule
、succeedModule
、succeedEntry
、seal
、chunk
,实现模块的加载、封闭、优化、分块,哈希和重建等等,并将当前模块资源、编译生成资源、变化的文件、以及被跟踪依赖的状态等相关信息保存到 compilation 对象。Compilation.js 类的源码很长,具体编译流程也会涉及到 webpack 的许许多多其他模块,就不在这篇说了。
compiler 钩子触发时机和 webpack 执行流程对照
总结:
Compiler类(./lib/Compiler.js):webpack的主要引擎,扩展自Tapable。webpack 从执行到结束,Compiler只会实例化一次。生成的 compiler 对象记录了 webpack 当前运行环境的完整的信息,该对象是全局唯一的,插件可以通过它获取到 webpack config 信息,如entry、output、loaders等配置。
Compilation类(./lib/Compilation.js):扩展自Tapable,也提供了很多关键点回调供插件做自定义处理时选择使用拓展。一个 compilation 对象代表了一次单一的版本构建和生成资源,它储存了当前的模块资源、编译生成的资源、变化的文件、以及被跟踪依赖的状态信息。简单来说,Compilation的职责就是对所有 require 图(graph)中对象的字面上的编译,构建 module 和 chunk,并利用插件优化构建过程,同时把本次打包编译的内容全存到内存里。compilation 编译可以多次执行,如在watch模式下启动 webpack,每次监测到源文件发生变化,都会重新实例化一个compilation对象,从而生成一组新的编译资源。这个对象可以访问所有的模块和它们的依赖(大部分是循环依赖)。
Compiler 和 Compilation 的区别
compiler 对象代表的是构建过程中不变的 webpack 环境,整个 webpack 从启动到关闭的生命周期。针对的是webpack。
compilation 对象只代表一次新的编译,只要项目文件有改动,compilation 就会被重新创建。针对的是随时可变的项目文件。
关于 compiler hooks、compilation hooks 以及其他重要对象上可用的 hooks 列表,请参阅 plugins API 文档。
参考:webpack插件编写及原理解析