技术/工程化/Webpack
Github Tapable—Just a little module for plugins
Webpack看似复杂的插件机制,其核心在于Tapable
库,该库类似Nodejs中的EventEmitter
,可以用于控制钩子函数的订阅和发布,而且Tapable
支持多种执行方式,主要包括:
基于触发时间
- 同步顺序执行,函数返回时执行下个函数
- 异步顺序执行,函数最后一个参数作为回调
- 并行执行,同时执行所有函数
基于返回值
- 无返回值, 顺序执行所有函数
- Bail,函数顺序执行,直到其中一个有返回值
- 并行Bail: 所有函数同时执行,第一个返回的值作为结果
- Waterfall,所有函数都接受上个函数的结果作为参数
[image:63AA80F6-EB67-4B58-9610-2EB865BDDDFD-1498-000042990C6C7AC9/1.png]
[image:50B298AB-71E4-49F9-B3FE-A952FF957EBB-2549-0000040CDF0494C8/167f458ac2b1e527.png]
[image:92837DA5-94DA-4129-AFF2-D5D81CC1AC26-2549-00000414170AD995/167f458d6ff8424f.png]
Tapable源码分析
Tapable
暴露出许多钩子基类,方便在不同场景使用。
const {
SyncHook,
SyncBailHook,
SyncWaterfallHook,
SyncLoopHook,
AsyncParallelHook,
AsyncParallelBailHook,
AsyncSeriesHook,
AsyncSeriesWaterfallHook
} = require('tapable')
可以通过命令: npm install —save tapable
安装tapable库
Hook
基类
https://github.com/webpack/tapable/blob/master/lib/Hook.js
SyncHook
实现
SyncHook
继承自Hook
基类:
class SyncHook extends Hook {
// 同步hook不允许异步钩子
tapAsync() {}
tapPromise() {}
compile(options) {
factory.setup(this, options);
return factory.create(options);
}
}
初始化一个SyncHook实例:
const hook = new SyncHook(['arg1', 'arg2'])
注册事件:
hook.tab('start', function(arg1, arg2){})
hook.tab('end', function(arg1, arg2){})
下面分析tap()
方法:
// 处理参数
if (typeof options === 'string') options = { name: options };
if (typeof options !== 'object' || options === null) throw new Error('Invalid options!')
options = Object.assign({type: 'sync', fn: fn}, options)
if (typeof options.name !== 'string' || options.name === '') throw new Error('Missing tab name')
// 添加拦截器
options = this._runRegisterInterceptor(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;
// 确定item插入位置
while(i > 0) {
i --;
const x = this.taps[i];
this.taps[i + 1] = x;
const xStage = x.stage || 0;
// 判断x是否为item的before元素,是则跳过不调整位置
if (before) {
if (before.has(x.name)) {
before.delete(x.name);
continue;
}
if (before.size > 0) continue;
}
// 遍历元素的stage大于stage则继续调整顺序
if (xStage > stage) continue;
i ++;
break;
}
this.taps[i] = item;
}
上述方法,将注册的事件按照stage顺序放入taps数组。
触发事件:
hook.call(1, 2)
call()
方法继承自基类Hook
Object.defineProperties(Hook.prototype, {
_call: {
value: createCompileDelegate('call', 'sync'),
configurable: true,
writable: true
},
_promise: {}, // 'promise', 'promise'
_callAsync: {}, // 'callAsync', 'async'
})
_call
由createCompileDelegate
生成,返回的是一个lazyCompileHook,直到调用call才会编译出call函数。
而createCompileDelegate
则是调用_createCall
,_createCall
调用compile
,。
重点是compile
方法,该方法负责返回最后的call函数,但是该方法必须由子类重写,即由子类决定返回哪种call函数。SyncHook
中确实重写了该方法:
compile(options) {
factory.setup(this, options);
return factory.create(options);
}
其中factory由HookCodeFactory
提供,负责根据Hook类型生成不同的代码,详细代码可查看 https://github.com/webpack/tapable/blob/master/lib/HookCodeFactory.js
具体生程省略,最终返回的函数内容如下:
"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);
}
上面分析tapable核心的功能和实现方式,那么可以去研究一下,Webpack中的compiler和compilation是怎么基于tapable运行的。
Webpack中的compiler和compilation
Compiler对象
Compiler是webpack的主模块,可以实例一个compiler对象,代表当前的webpack环境,通过该实例的run方法可以创建一个compilation实例;另外,应用插件时,插件会接收compiler对象的引用从而访问webpack环境
Compiler实现:
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>} */
beforeRun: new AsyncSeriesHook(["compiler"]),
/** @type {AsyncSeriesHook<Compiler>} */
run: new AsyncSeriesHook(["compiler"]),
/** @type {AsyncSeriesHook<Compilation>} */
emit: new AsyncSeriesHook(["compilation"]),
/** @type {AsyncSeriesHook<Compilation>} */
afterEmit: new AsyncSeriesHook(["compilation"]),
}
}}
定义一系列编译过程中的钩子,用于插件注册事件,如run
事件,会在run()
方法执行时调用this.hooks.run.callAsync(this, callback)
来触发,另外在此之前会调用this.hooks.beforeRun.callAsync(this, callback)
来触发beforeRun
事件[https://github.com/webpack/webpack/blob/master/lib/Compiler.js#L308](https://github.com/webpack/webpack/blob/master/lib/Compiler.js#L308)
this.hooks.beforeRun.callAsync(this, err => {
if (err) return finalCallback(err);
this.hooks.run.callAsync(this, err => {
if (err) return finalCallback(err);
this.readRecords(err => {
if (err) return finalCallback(err);
this.compile(onCompiled);
});
});
});
Compiler对象的使用,则需要实现一个自定义插件,如:
function UglifyJsPlugin(options) { this.options = options}
module.exports = UglifyJsPlugin;
// 关键, 将插件注册到Compiler的生命周期钩子事件
UglifyJsPlugin.prototype.apply = function(compiler) {
compilr.plugin('compliation', function(compilation) {
compilation.plugin('build-module', callback(module));
compilation.plugin('optimize', callback(module))
})
}
Compilation对象
实例compiler执行run()
方法后,开始webpack的工作流程,则会创建compilation对象,该对象代表了一次资源版本构建。compilation不仅负责组织整个打包过程,包含了每个构建环节及输出环节所对应的方法如buildModule, addEntry等,而且还存放着所有module,chunk,assets以及用来生成最后打包文件的template信息
Compilation实现:
class Compilation extends Tapable {
constructor(compiler) {
super();
this.hooks = {
buildModule: new SyncHook(['module']),
rebuildModule: new SyncHook(['module']),
addEntry: new SyncHook(['entry', 'name'])
...
}
}
}
Compilation
对象同样继承自Tapable
类,并定义了一系列的钩子,如addEntry
事件,则会在执行addEntry()
方法时调用this.hooks.addEntry.call(entry, name)
通过compiler和compilation可以看出,想控制编译过程,其实就是在不同时机注册各种不同功能的插件即可。
Compiler对象几个关键的事件节点:
- compile,开始编译
- make, 创建模块对象
- build-module, 构建模块
- after-compile,完成构建
- seal,封装构建结果
- emit,把各个chunk输出到结果文件
- after-emit,完成输出
Compilation对象的几个关键事件节点:
add-entry
, 添加入口文件build-module
,构建模块seal
, 封装模块afterChunks
,划分chunk完成chunkAsset
, 一个 chunk 中的一个资源被添加到编译中
‘before run’
‘run’
compile:func//调用compile函数
‘before compile’
‘compile’//(1)compiler对象的第一阶段
newCompilation:object//创建compilation对象
‘make’ //(2)compiler对象的第二阶段
compilation.finish:func
“finish-modules”
compilation.seal
“seal”
“optimize”
“optimize-modules-basic”
“optimize-modules-advanced”
“optimize-modules”
“after-optimize-modules”//首先是优化模块
“optimize-chunks-basic”
“optimize-chunks”//然后是优化chunk
“optimize-chunks-advanced”
“after-optimize-chunks”
“optimize-tree”
“after-optimize-tree”
“should-record”
“revive-modules”
“optimize-module-order”
“advanced-optimize-module-order”
“before-module-ids”
“module-ids”//首先优化module-order,然后优化module-id
“optimize-module-ids”
“after-optimize-module-ids”
“revive-chunks”
“optimize-chunk-order”
“before-chunk-ids”//首先优化chunk-order,然后chunk-id
“optimize-chunk-ids”
“after-optimize-chunk-ids”
“record-modules”//record module然后record chunk
“record-chunks”
“before-hash”
compilation.createHash//func
“chunk-hash”//webpack-md5-hash
“after-hash”
“record-hash”//before-hash/after-hash/record-hash
“before-module-assets”
“should-generate-chunk-assets”
“before-chunk-assets”
“additional-chunk-assets”
“record”
“additional-assets”
“optimize-chunk-assets”
“after-optimize-chunk-assets”
“optimize-assets”
“after-optimize-assets”
“need-additional-seal”
unseal:func
“unseal”
“after-seal”
“after-compile”//(4)完成模块构建和编译过程(seal函数回调)
“emit”//(5)compile函数的回调,compiler开始输出assets,是改变assets最后机会
“after-emit”//(6)文件产生完成
看一眼实际生成的Compiler对象和Compilation对象:
关于compiler和compilation众多的钩子及钩子类型参考官网说明:
compilation 钩子
compiler 钩子
参考文章:
[Segmentfault-Tapable文档]
[知乎-Tapable源码分析]
[Tapable-钩子使用和原理]