webpack系列-插件机制杂记

9次阅读

共计 10376 个字符,预计需要花费 26 分钟才能阅读完成。

系列文章
Webpack 系列 - 第一篇基础杂记 webpack 系列 - 插件机制杂记
前言
webpack 本身并不难,他所完成的各种复杂炫酷的功能都依赖于他的插件机制。或许我们在日常的开发需求中并不需要自己动手写一个插件,然而,了解其中的机制也是一种学习的方向,当插件出现问题时,我们也能够自己来定位。
Tapable
Webpack 的插件机制依赖于一个核心的库,Tapable。在深入 webpack 的插件机制之前,需要对该核心库有一定的了解。
Tapable 是什么
tapable 是一个类似于 nodejs 的 EventEmitter 的库, 主要是控制钩子函数的发布与订阅。当然,tapable 提供的 hook 机制比较全面,分为同步和异步两个大类(异步中又区分异步并行和异步串行),而根据事件执行的终止条件的不同,由衍生出 Bail/Waterfall/Loop 类型。
Tapable 的使用(该小段内容引用文章)
基本使用:
const {
SyncHook
} = require(‘tapable’)

// 创建一个同步 Hook,指定参数
const hook = new SyncHook([‘arg1’, ‘arg2’])

// 注册
hook.tap(‘a’, function (arg1, arg2) {
console.log(‘a’)
})

hook.tap(‘b’, function (arg1, arg2) {
console.log(‘b’)
})

hook.call(1, 2)
钩子类型:

BasicHook:执行每一个,不关心函数的返回值,有 SyncHook、AsyncParallelHook、AsyncSeriesHook。
BailHook:顺序执行 Hook,遇到第一个结果 result!==undefined 则返回,不再继续执行。有:SyncBailHook、AsyncSeriseBailHook, AsyncParallelBailHook。
什么样的场景下会使用到 BailHook 呢?设想如下一个例子:假设我们有一个模块 M,如果它满足 A 或者 B 或者 C 三者任何一个条件,就将其打包为一个单独的。这里的 A、B、C 不存在先后顺序,那么就可以使用 AsyncParallelBailHook 来解决:
x.hooks. 拆分模块的 Hook.tap(‘A’, () => {
if (A 判断条件满足) {
return true
}
})
x.hooks. 拆分模块的 Hook.tap(‘B’, () => {
if (B 判断条件满足) {
return true
}
})
x.hooks. 拆分模块的 Hook.tap(‘C’, () => {
if (C 判断条件满足) {
return true
}
})

如果 A 中返回为 true,那么就无须再去判断 B 和 C。但是当 A、B、C 的校验,需要严格遵循先后顺序时,就需要使用有顺序的 SyncBailHook(A、B、C 是同步函数时使用) 或者 AsyncSeriseBailHook(A、B、C 是异步函数时使用)。
WaterfallHook:类似于 reduce,如果前一个 Hook 函数的结果 result !== undefined,则 result 会作为后一个 Hook 函数的第一个参数。既然是顺序执行,那么就只有 Sync 和 AsyncSeries 类中提供这个 Hook:SyncWaterfallHook,AsyncSeriesWaterfallHook 当一个数据,需要经过 A,B,C 三个阶段的处理得到最终结果,并且 A 中如果满足条件 a 就处理,否则不处理,B 和 C 同样,那么可以使用如下
x.hooks.tap(‘A’, (data) => {
if (满足 A 需要处理的条件) {
// 处理数据 data
return data
} else {
return
}
})
x.hooks.tap(‘B’, (data) => {
if (满足 B 需要处理的条件) {
// 处理数据 data
return data
} else {
return
}
})
x.hooks.tap(‘C’, (data) => {
if (满足 C 需要处理的条件) {
// 处理数据 data
return data
} else {
return
}
})
LoopHook:不停的循环执行 Hook,直到所有函数结果 result === undefined。同样的,由于对串行性有依赖,所以只有 SyncLoopHook 和 AsyncSeriseLoopHook(PS:暂时没看到具体使用 Case)
Tapable 的源码分析
Tapable 基本逻辑是,先通过类实例的 tap 方法注册对应 Hook 的处理函数,这里直接分析 sync 同步钩子的主要流程,其他的异步钩子和拦截器等就不赘述了。
const hook = new SyncHook([‘arg1’, ‘arg2’])
从该句代码,作为源码分析的入口,
class SyncHook extends Hook {
// 错误处理,防止调用者调用异步钩子
tapAsync() {
throw new Error(“tapAsync is not supported on a SyncHook”);
}
// 错误处理,防止调用者调用 promise 钩子
tapPromise() {
throw new Error(“tapPromise is not supported on a SyncHook”);
}
// 核心实现
compile(options) {
factory.setup(this, options);
return factory.create(options);
}
}
从类 SyncHook 看到,他是继承于一个基类 Hook,他的核心实现 compile 等会再讲,我们先看看基类 Hook
// 变量的初始化
constructor(args) {
if (!Array.isArray(args)) args = [];
this._args = args;
this.taps = [];
this.interceptors = [];
this.call = this._call;
this.promise = this._promise;
this.callAsync = this._callAsync;
this._x = undefined;
}
初始化完成后,通常会注册一个事件,如:
// 注册
hook.tap(‘a’, function (arg1, arg2) {
console.log(‘a’)
})

hook.tap(‘b’, function (arg1, arg2) {
console.log(‘b’)
})
很明显,这两个语句都会调用基类中的 tap 方法:
tap(options, fn) {
// 参数处理
if (typeof options === “string”) options = {name: options};
if (typeof options !== “object” || options === null)
throw new Error(
“Invalid arguments to tap(options: Object, fn: function)”
);
options = Object.assign({type: “sync”, fn: fn}, options);
if (typeof options.name !== “string” || options.name === “”)
throw new Error(“Missing name for tap”);
// 执行拦截器的 register 函数,比较简单不分析
options = this._runRegisterInterceptors(options);
// 处理注册事件
this._insert(options);
}
从上面的源码分析,可以看到_insert 方法是注册阶段的关键函数,直接进入该方法内部
_insert(item) {
// 重置所有的 调用 方法
this._resetCompilation();
// 将注册事件排序后放进 taps 数组
let before;
if (typeof item.before === “string”) before = new Set([item.before]);
else if (Array.isArray(item.before)) {
before = new Set(item.before);
}
let stage = 0;
if (typeof item.stage === “number”) stage = item.stage;
let i = this.taps.length;
while (i > 0) {
i–;
const x = this.taps[i];
this.taps[i + 1] = x;
const xStage = x.stage || 0;
if (before) {
if (before.has(x.name)) {
before.delete(x.name);
continue;
}
if (before.size > 0) {
continue;
}
}
if (xStage > stage) {
continue;
}
i++;
break;
}
this.taps[i] = item;
}
}
_insert 主要是排序 tap 并放入到 taps 数组里面,排序的算法并不是特别复杂,这里就不赘述了,到了这里,注册阶段就已经结束了,继续看触发阶段。
hook.call(1, 2) // 触发函数
在基类 hook 中,有一个初始化过程,
this.call = this._call;

Object.defineProperties(Hook.prototype, {
_call: {
value: createCompileDelegate(“call”, “sync”),
configurable: true,
writable: true
},
_promise: {
value: createCompileDelegate(“promise”, “promise”),
configurable: true,
writable: true
},
_callAsync: {
value: createCompileDelegate(“callAsync”, “async”),
configurable: true,
writable: true
}
});
我们可以看出_call 是由 createCompileDelegate 生成的,往下看
function createCompileDelegate(name, type) {
return function lazyCompileHook(…args) {
this[name] = this._createCall(type);
return this[name](…args);
};
}
createCompileDelegate 返回一个名为 lazyCompileHook 的函数,顾名思义,即懒编译,直到调用 call 的时候,才会编译出正在的 call 函数。
createCompileDelegate 也是调用的_createCall,而_createCall 调用了 Compier 函数
_createCall(type) {
return this.compile({
taps: this.taps,
interceptors: this.interceptors,
args: this._args,
type: type
});
}
compile(options) {
throw new Error(“Abstract: should be overriden”);
}
可以看到 compiler 必须由子类重写,返回到 syncHook 的 compile 函数,即我们一开始说的核心方法
class SyncHookCodeFactory extends HookCodeFactory {
content({onError, onResult, onDone, rethrowIfPossible}) {
return this.callTapsSeries({
onError: (i, err) => onError(err),
onDone,
rethrowIfPossible
});
}
}

const factory = new SyncHookCodeFactory();

class SyncHook extends Hook {

compile(options) {
factory.setup(this, options);
return factory.create(options);
}
}
关键就在于 SyncHookCodeFactory 和工厂类 HookCodeFactory,先看 setup 函数,
setup(instance, options) {
// 这里的 instance 是 syncHook 实例, 其实就是把 tap 进来的钩子数组给到钩子的_x 属性里.
instance._x = options.taps.map(t => t.fn);
}
然后是最关键的 create 函数,可以看到最后返回的 fn,其实是一个 new Function 动态生成的函数
create(options) {
// 初始化参数, 保存 options 到本对象 this.options, 保存 new Hook([“options”]) 传入的参数到 this._args
this.init(options);
let fn;
// 动态构建钩子, 这里是抽象层, 分同步, 异步, promise
switch (this.options.type) {
// 先看同步
case “sync”:
// 动态返回一个钩子函数
fn = new Function(
// 生成函数的参数,no before no after 返回参数字符串 xxx,xxx 在
// 注意这里 this.args 返回的是一个字符串,
// 在这个例子中是 options
this.args(),
‘”use strict”;\n’ +
this.header() +
this.content({
onError: err => `throw ${err};\n`,
onResult: result => `return ${result};\n`,
onDone: () => “”,
rethrowIfPossible: true
})
);
break;
case “async”:
fn = new Function(
this.args({
after: “_callback”
}),
‘”use strict”;\n’ +
this.header() +
// 这个 content 调用的是子类类的 content 函数,
// 参数由子类传, 实际返回的是 this.callTapsSeries() 返回的类容
this.content({
onError: err => `_callback(${err});\n`,
onResult: result => `_callback(null, ${result});\n`,
onDone: () => “_callback();\n”
})
);
break;
case “promise”:
let code = “”;
code += ‘”use strict”;\n’;
code += “return new Promise((_resolve, _reject) => {\n”;
code += “var _sync = true;\n”;
code += this.header();
code += this.content({
onError: err => {
let code = “”;
code += “if(_sync)\n”;
code += `_resolve(Promise.resolve().then(() => { throw ${err}; }));\n`;
code += “else\n”;
code += `_reject(${err});\n`;
return code;
},
onResult: result => `_resolve(${result});\n`,
onDone: () => “_resolve();\n”
});
code += “_sync = false;\n”;
code += “});\n”;
fn = new Function(this.args(), code);
break;
}
// 把刚才 init 赋的值初始化为 undefined
// this.options = undefined;
// this._args = undefined;
this.deinit();

return fn;
}
最后生成的代码大致如下,参考文章
“use strict”;
function (options) {
var _context;
var _x = this._x;
var _taps = this.taps;
var _interterceptors = this.interceptors;
// 我们只有一个拦截器所以下面的只会生成一个
_interceptors[0].call(options);

var _tap0 = _taps[0];
_interceptors[0].tap(_tap0);
var _fn0 = _x[0];
_fn0(options);
var _tap1 = _taps[1];
_interceptors[1].tap(_tap1);
var _fn1 = _x[1];
_fn1(options);
var _tap2 = _taps[2];
_interceptors[2].tap(_tap2);
var _fn2 = _x[2];
_fn2(options);
var _tap3 = _taps[3];
_interceptors[3].tap(_tap3);
var _fn3 = _x[3];
_fn3(options);
}
ok,以上就是 Tapabled 的机制,然而本篇的主要对象其实是基于 tapable 实现的 compile 和 compilation 对象。不过由于他们都是基于 tapable,所以介绍的篇幅相对短一点。
compile
compile 是什么
compiler 对象代表了完整的 webpack 环境配置。这个对象在启动 webpack 时被一次性建立,并配置好所有可操作的设置,包括 options,loader 和 plugin。当在 webpack 环境中应用一个插件时,插件将收到此 compiler 对象的引用。可以使用 compiler 来访问 webpack 的主环境。
也就是说,compile 是 webpack 的整体环境。
compile 的内部实现
class Compiler extends Tapable {
constructor(context) {
super();
this.hooks = {
/** @type {SyncBailHook<Compilation>} */
shouldEmit: new SyncBailHook([“compilation”]),
/** @type {AsyncSeriesHook<Stats>} */
done: new AsyncSeriesHook([“stats”]),
/** @type {AsyncSeriesHook<>} */
additionalPass: new AsyncSeriesHook([]),
/** @type {AsyncSeriesHook<Compiler>} */
……
……
some code
};
……
……
some code
}
可以看到,Compier 继承了 Tapable, 并且在实例上绑定了一个 hook 对象,使得 Compier 的实例 compier 可以像这样使用
compiler.hooks.compile.tapAsync(
‘afterCompile’,
(compilation, callback) => {
console.log(‘This is an example plugin!’);
console.log(‘Here’s the `compilation` object which represents a single build of assets:’, compilation);

// 使用 webpack 提供的 plugin API 操作构建结果
compilation.addModule(/* … */);

callback();
}
);
compilation
什么是 compilation
compilation 对象代表了一次资源版本构建。当运行 webpack 开发环境中间件时,每当检测到一个文件变化,就会创建一个新的 compilation,从而生成一组新的编译资源。一个 compilation 对象表现了当前的模块资源、编译生成资源、变化的文件、以及被跟踪依赖的状态信息。compilation 对象也提供了很多关键时机的回调,以供插件做自定义处理时选择使用。
compilation 的实现
class Compilation extends Tapable {
/**
* Creates an instance of Compilation.
* @param {Compiler} compiler the compiler which created the compilation
*/
constructor(compiler) {
super();
this.hooks = {
/** @type {SyncHook<Module>} */
buildModule: new SyncHook([“module”]),
/** @type {SyncHook<Module>} */
rebuildModule: new SyncHook([“module”]),
/** @type {SyncHook<Module, Error>} */
failedModule: new SyncHook([“module”, “error”]),
/** @type {SyncHook<Module>} */
succeedModule: new SyncHook([“module”]),

/** @type {SyncHook<Dependency, string>} */
addEntry: new SyncHook([“entry”, “name”]),
/** @type {SyncHook<Dependency, string, Error>} */
}
}
}
具体参考上面提到的 compiler 实现。
编写一个插件
了解到 tapablecompilercompilation 之后,再来看插件的实现就不再一头雾水了 以下代码源自官方文档
class MyExampleWebpackPlugin {
// 定义 `apply` 方法
apply(compiler) {
// 指定要追加的事件钩子函数
compiler.hooks.compile.tapAsync(
‘afterCompile’,
(compilation, callback) => {
console.log(‘This is an example plugin!’);
console.log(‘Here’s the `compilation` object which represents a single build of assets:’, compilation);

// 使用 webpack 提供的 plugin API 操作构建结果
compilation.addModule(/* … */);

callback();
}
);
}
}
可以看到其实就是在 apply 中传入一个 Compiler 实例,然后基于该实例注册事件,compilation 同理,最后 webpack 会在各流程执行 call 方法。
compiler 和 compilation 一些比较重要的事件钩子
compier

事件钩子
触发时机
参数
类型

entry-option
初始化 option

SyncBailHook

run
开始编译
compiler
AsyncSeriesHook

compile
真正开始的编译,在创建 compilation 对象之前
compilation
SyncHook

compilation
生成好了 compilation 对象,可以操作这个对象啦
compilation
SyncHook

make
从 entry 开始递归分析依赖,准备对每个模块进行 build
compilation
AsyncParallelHook

after-compile
编译 build 过程结束
compilation
AsyncSeriesHook

emit
在将内存中 assets 内容写到磁盘文件夹之前
compilation
AsyncSeriesHook

after-emit
在将内存中 assets 内容写到磁盘文件夹之后
compilation
AsyncSeriesHook

done
完成所有的编译过程
stats
AsyncSeriesHook

failed
编译失败的时候
error
SyncHook

compilation

事件钩子
触发时机
参数
类型

normal-module-loader
普通模块 loader,真正(一个接一个地)加载模块图 (graph) 中所有模块的函数。
loaderContext module
SyncHook

seal
编译 (compilation) 停止接收新模块时触发。

SyncHook

optimize
优化阶段开始时触发。

SyncHook

optimize-modules
模块的优化
modules
SyncBailHook

optimize-chunks
优化 chunk
chunks
SyncBailHook

additional-assets
为编译 (compilation) 创建附加资源(asset)。

AsyncSeriesHook

optimize-chunk-assets
优化所有 chunk 资源(asset)。
chunks
AsyncSeriesHook

optimize-assets
优化存储在 compilation.assets 中的所有资源(asset)
assets
AsyncSeriesHook

总结
插件机制并不复杂,webpack 也不复杂,复杂的是插件本身.. 另外,本应该先写流程的,流程只能后面补上了。
引用
不满足于只会使用系列: tapable webpack 系列之二 Tapable 编写一个插件 Compiler Compilation compiler 和 comnpilation 钩子 看清楚真正的 Webpack 插件

正文完
 0