webpack 是基于事件流的打包构建工具,也就是内置了不少 hooks。做为使用方,能够在这些钩子当中,去插入本身的处理逻辑,而这一切的实现都得益于 tapable 这个工具。它有多个版本,webpack 前期的版本是依赖于 tapable 0.2.8 这个版本,后来重构了,发了 2.0.0 beta 版本,由于源码都是经过字符串拼接,经过 new Function 的模式使用,因此看起来比较晦涩。html
那么既然如此,咱们先从早期的 0.2.8 这个版本了解下它的前身,毕竟核心思想不会发生太大的变化。node
tapable 的实现相似于 node 的 EventEmitter 的发布订阅模式。用一个对象的键存储对应的事件名称,键值来存储事件的处理函数,相似于下面:webpack
function Tapable () {
this._plugins = {
'emit': [handler1, handler2, ......]
}
}
复制代码
同时,原型上定义了不一样的方法来调用 handlers。git
咱们先来看下用法,github
plugin 与 applyPluginsweb
void plugin(names: string|string[], handler: Function)
void applyPlugins(name: string, args: any...)
复制代码
最基础的就是注册插件以及插件触发的回调函数。express
const Tapable = require('tapable')
const t = new Tapable()
// 注册插件
t.plugin('emit', (...args) => {
console.log(args)
console.log('This is a emit handler')
})
// 调用插件
t.applyPlugins('emit', '参数1')
// 打印以下
[ '参数1' ]
This is a emit handler
复制代码
源码以下数组
Tapable.prototype.applyPlugins = function applyPlugins(name) {
if(!this._plugins[name]) return;
var args = Array.prototype.slice.call(arguments, 1);
var plugins = this._plugins[name];
for(var i = 0; i < plugins.length; i++)
plugins[i].apply(this, args);
};
Tapable.prototype.plugin = function plugin(name, fn) {
if(Array.isArray(name)) {
name.forEach(function(name) {
this.plugin(name, fn);
}, this);
return;
}
if(!this._plugins[name]) this._plugins[name] = [fn];
else this._plugins[name].push(fn);
};
复制代码
很简单,内部维护 _plugins 属性来缓存 plugin 名称以及 handler。缓存
apply闭包
void apply(plugins: Plugin...)
复制代码
接收 plugin 做为参数,每一个 plugin 必须提供 apply 方法,也就是 webpack 在编写 plugin 的规是插件实例必须提供 apply 方法。
const Tapable = require('tapable')
const t = new Tapable()
// 声明一个 webpack 插件的类,对象必须声明 apply 方法
class WebpackPlugin {
constructor () {}
apply () {
console.log('This is webpackPlugin')
}
}
const plugin = new WebpackPlugin()
// tapable.apply
t.apply(plugin) // print 'This is webpackPlugin'
复制代码
源码以下
Tapable.prototype.apply = function apply() {
for(var i = 0; i < arguments.length; i++) {
arguments[i].apply(this);
}
};
复制代码
也很简单,依次执行每一个插件的 apply 方法。
applyPluginsWaterfall
any applyPluginsWaterfall(name: string, init: any, args: any...)
复制代码
依次调用插件对应的 handler,传入的参数是上一个 handler 的返回值,以及调用 applyPluginsWaterfall 传入 args 参数组成的数组,提及来很绕,看看下面的例子:
t.plugin('waterfall', (...args) => {
// print ['init', 'args1']
console.log(args)
return 'result1'
})
t.plugin('waterfall', (...args) => {
// print ['result1', 'args1']
console.log(args)
return 'result2'
})
const ret = t.applyPluginsWaterfall('waterfall', 'init', 'args1') // ret => 'result2'
复制代码
源码以下
Tapable.prototype.applyPluginsWaterfall = function applyPluginsWaterfall(name, init) {
if(!this._plugins[name]) return init;
var args = Array.prototype.slice.call(arguments, 1);
var plugins = this._plugins[name];
var current = init;
for(var i = 0; i < plugins.length; i++) {
args[0] = current;
current = plugins[i].apply(this, args);
}
return current;
};
复制代码
上一个 handler 返回的值,会做为下一个 handler的第一个参数。
applyPluginsBailResult
any applyPluginsBailResult(name: string, args: any...)
复制代码
依次调用插件对应的 handler,传入的参数是 args,若是正执行的 handler 的 返回值不是 undefined,其他的 handler 都不会执行了。 bail
是保险的意思,即只要任意一个 handler 有 !== undefined
的返回值,那么函数的执行就终止了。
t.plugin('bailResult', (...args) => {
// [ '参数1', '参数2' ]
console.log(args)
return 'result1'
})
t.plugin('bailResult', (...args) => {
// 由于上一个函数返回了 'result1',因此不会执行到这个handler
console.log(args)
return 'result2'
})
t.applyPluginsBailResult('bailResult', '参数1', '参数2')
复制代码
源码以下
Tapable.prototype.applyPluginsBailResult = function applyPluginsBailResult(name, init) {
if(!this._plugins[name]) return;
var args = Array.prototype.slice.call(arguments, 1);
var plugins = this._plugins[name];
for(var i = 0; i < plugins.length; i++) {
var result = plugins[i].apply(this, args);
if(typeof result !== "undefined") {
return result;
}
}
};
复制代码
只要 handler 返回的值 !== undefined
,就会中止调用接下来的 handler。
applyPluginsAsyncSeries & applyPluginsAsync(支持异步)
void applyPluginsAsync(
name: string,
args: any...,
callback: (err?: Error) -> void
)
复制代码
applyPluginsAsyncSeries 与 applyPluginsAsync 的函数引用都是相同的,而且函数内部支持异步。callback 在全部 handler 都执行完了才会调用,可是在注册 handler 的时候,函数内部必定要执行 next() 的逻辑,这样才能执行到下一个 handler。
t.plugin('asyncSeries', (...args) => {
// handler 的最后一个参数必定是 next 函数
const next = args.pop()
// 执行 next,函数才会执行到下面的 handler
setTimeout (() => {
next()
}, 3000)
})
t.plugin('asyncSeries', (...args) => {
// handler 的最后一个参数必定是 next
const callback = args.pop()
// 执行 next,函数才会执行到 applyPluginsAsyncSeries 传入的 callback
Promise.resolve(1).then(next)
})
t.applyPluginsAsyncSeries('asyncSeries', '参数1', (...args) => {
console.log('这是 applyPluginsAsyncSeries 的 callback')
})
复制代码
源码以下
Tapable.prototype.applyPluginsAsyncSeries = Tapable.prototype.applyPluginsAsync = function applyPluginsAsyncSeries(name) {
var args = Array.prototype.slice.call(arguments, 1);
var callback = args.pop();
var plugins = this._plugins[name];
if(!plugins || plugins.length === 0) return callback();
var i = 0;
var _this = this;
args.push(copyProperties(callback, function next(err) {
if(err) return callback(err);
i++;
if(i >= plugins.length) {
return callback();
}
plugins[i].apply(_this, args);
}));
plugins[0].apply(this, args);
};
复制代码
applyPluginsAsyncSeries 内部维护了一个 next 函数,这个函数做为每一个 handler 的最后一个参数传入,handler 内部支持异步操做,可是必须手动调用 next 函数,才能执行到下一个 handler。
applyPluginsAsyncSeriesBailResult(支持异步)
void applyPluginsAsyncSeriesBailResult(
name: string,
args: any...,
callback: (result: any) -> void
)
复制代码
函数支持异步,只要在 handler 里面调用 next 回调函数,而且传入任意参数,就会直接执行 callback。
t.plugin('asyncSeriesBailResult', (...args) => {
// handler 的最后一个参数必定是 next 函数
const next = args.pop()
// 由于传了字符串,致使直接执行 callback
next('跳过 handler 函数')
})
t.plugin('asyncSeriesBailResult', (...args) => {
})
t.applyPluginsAsyncSeriesBailResult('asyncSeriesBailResult', '参数1', (...args) => {
console.log('这是 applyPluginsAsyncSeriesBailResult 的 callback')
})
// print '这是 applyPluginsAsyncSeriesBailResult 的 callback'
复制代码
源码以下
Tapable.prototype.applyPluginsAsyncSeriesBailResult = function applyPluginsAsyncSeriesBailResult(name) {
var args = Array.prototype.slice.call(arguments, 1);
var callback = args.pop();
if(!this._plugins[name] || this._plugins[name].length === 0) return callback();
var plugins = this._plugins[name];
var i = 0;
var _this = this;
args.push(copyProperties(callback, function next() {
if(arguments.length > 0) return callback.apply(null, arguments);
i++;
if(i >= plugins.length) {
return callback();
}
plugins[i].apply(_this, args);
}));
plugins[0].apply(this, args);
};
复制代码
applyPluginsAsyncSeriesBailResult 内部维护了一个 next 函数,这个函数做为每一个 handler 的最后一个参数传入,handler 内部支持异步操做,可是必须手动调用 next 函数,才能执行到下一个 handler,next 函数能够传入参数,这样会直接执行 callback。
applyPluginsAsyncWaterfall(支持异步)
void applyPluginsAsyncWaterfall(
name: string,
init: any,
callback: (err: Error, result: any) -> void
)
复制代码
函数支持异步,handler 的接收两个参数,第一个参数是上一个 handler 经过 next 函数传过来的 value,第二个参数是 next 函数。next 函数接收两个参数,第一个是 error,若是 error 存在,就直接执行 callback。第二个 value 参数,是传给下一个 handler 的参数。
t.plugin('asyncWaterfall', (value, next) => {
// handler 的最后一个参数必定是 next 函数
console.log(value)
next(null, '来自第一个 handler')
})
t.plugin('asyncWaterfall', (value, next) => {
console.log(value)
next(null, '来自第二个 handler')
})
t.applyPluginsAsyncWaterfall('asyncWaterfall', '参数1', (err, value) => {
if (!err) {
console.log(value)
}
})
// 打印以下
参数1
来自第一个 handler
来自第二个 handler
复制代码
源码以下
Tapable.prototype.applyPluginsAsyncWaterfall = function applyPluginsAsyncWaterfall(name, init, callback) {
if(!this._plugins[name] || this._plugins[name].length === 0) return callback(null, init);
var plugins = this._plugins[name];
var i = 0;
var _this = this;
var next = copyProperties(callback, function(err, value) {
if(err) return callback(err);
i++;
if(i >= plugins.length) {
return callback(null, value);
}
plugins[i].call(_this, value, next);
});
plugins[0].call(this, init, next);
};
复制代码
applyPluginsAsyncWaterfall 内部维护了一个 next 函数,这个函数做为每一个 handler 的最后一个参数传入,handler 内部支持异步操做,可是必须手动调用 next 函数,才能执行到下一个 handler,next 函数能够传入参数,第一个参数为 err, 第二参数为上一个 handler 返回值。
applyPluginsParallel(支持异步)
void applyPluginsParallel(
name: string,
args: any...,
callback: (err?: Error) -> void
)
复制代码
并行的执行函数,每一个 handler 的最后一个参数都是 next 函数,这个函数用来检验当前的 handler 是否已经执行完。
t.plugin('parallel', (...args) => {
const next = args.pop()
console.log(1)
// 必须调用 next 函数,要否则 applyPluginsParallel 的 callback 永远也不会回调
next('抛出错误了1', '来自第一个 handler')
})
t.plugin('parallel', (...args) => {
const next = args.pop()
console.log(2)
// 必须调用 next 函数,要否则 applyPluginsParallel 的 callback 永远也不会回调
next('抛出错误了2')
})
t.applyPluginsParallel('parallel', '参数1', (err) => {
// print '抛出错误了1'
console.log(err)
})
复制代码
源码以下
Tapable.prototype.applyPluginsParallel = function applyPluginsParallel(name) {
var args = Array.prototype.slice.call(arguments, 1);
var callback = args.pop();
if(!this._plugins[name] || this._plugins[name].length === 0) return callback();
var plugins = this._plugins[name];
var remaining = plugins.length;
args.push(copyProperties(callback, function(err) {
if(remaining < 0) return; // ignore
if(err) {
remaining = -1;
return callback(err);
}
remaining--;
if(remaining === 0) {
return callback();
}
}));
for(var i = 0; i < plugins.length; i++) {
plugins[i].apply(this, args);
if(remaining < 0) return;
}
};
复制代码
applyPluginsParallel 并行地调用 handler。内部经过闭包维护了 remaining 变量,用来判断内部的函数是否真正执行完,handler 的最后一个参数是一个函数 check。若是 handler 内部用户想要的逻辑执行完,必须调用 check 函数来告诉 tapable,进而才会执行 args 数组的最后一个 check 函数。
** applyPluginsParallelBailResult **(支持异步)
void applyPluginsParallelBailResult(
name: string,
args: any...,
callback: (err: Error, result: any) -> void
)
复制代码
并行的执行函数,每一个 handler 的最后一个参数都是 next 函数,next 函数必须调用,若是给 next 函数传参,会直接走到 callback 的逻辑。callback 执行的时机是跟 handler 注册的顺序有关,而不是跟 handler 内部调用 next 的时机有关。
t.plugin('applyPluginsParallelBailResult', (next) => {
console.log(1)
setTimeout(() => {
next('has args 1')
}, 3000)
})
t.plugin('applyPluginsParallelBailResult', (next) => {
console.log(2)
setTimeout(() => {
next('has args 2')
})
})
t.plugin('applyPluginsParallelBailResult', (next) => {
console.log(3)
next('has args 3')
})
t.applyPluginsParallelBailResult('applyPluginsParallelBailResult', (result) => {
console.log(result)
})
// 打印以下
1
2
3
has args 1
虽然第一个 handler 的 next 函数是延迟 3s 才执行,可是注册的顺序是在最前面,因此 callback 的 result 参数值是 'has args 1'。
复制代码
源码以下
Tapable.prototype.applyPluginsParallelBailResult = function applyPluginsParallelBailResult(name) {
var args = Array.prototype.slice.call(arguments, 1);
var callback = args[args.length - 1];
if(!this._plugins[name] || this._plugins[name].length === 0) return callback();
var plugins = this._plugins[name];
var currentPos = plugins.length;
var currentResult;
var done = [];
for(var i = 0; i < plugins.length; i++) {
args[args.length - 1] = (function(i) {
return copyProperties(callback, function() {
if(i >= currentPos) return; // ignore
done.push(i);
if(arguments.length > 0) {
currentPos = i + 1;
done = fastFilter.call(done, function(item) {
return item <= i;
});
currentResult = Array.prototype.slice.call(arguments);
}
if(done.length === currentPos) {
callback.apply(null, currentResult);
currentPos = 0;
}
});
}(i));
plugins[i].apply(this, args);
}
};
复制代码
for 循环里面并行的执行 handler,handler 的最后一个参数是一个匿名回调函数,这个匿名函数必须在每一个 handler 里面手动的执行。而 callback 的执行时机就是根据 handler 的注册顺序有关。
从源码上来看,tapable 是提供了不少 API 来对应不一样调用 handler 的场景,有同步执行,有异步执行,还有串行异步,并行异步等。这些都是一些高级的技巧,无论是 express,仍是 VueRouter 的源码,都利用这些同异步执行机制,可是能够看出程序是有边界的。也就是约定成俗,从最后一个 applyPluginsParallel 函数来看,用户必须调用匿名回调函数,不然 tapable 怎么知道你内部是否有异步操做,而且异步操做在某个时候执行完了呢。
既然知道了 0.2.8 的核心思想,那么 2.0.0-beta 版的重构更是让人惊艳,目前的源码分析正在整理,连接以下。