# tapable 0.2.8
在webpack 1-3中, 依赖着tapable0.x版本, 使用它来添加和应用插件. tapable 2.x对项目进行了重构, 代码非常值得去分析, 追本溯源, 了解其0.x版本的代码, 并对比重构前后的代码, 希望有所收获.
webpack在其编译过程中,提供了很多hooks, 插件可以在这些hooks上注册回调函数, 这些hooks也可以认为是事件.
# webpack 插件
一个插件应该是类似如下这样的
// MyPlugin.js
function MyPlugin(options) {
// Configure your plugin with options...
}
MyPlugin.prototype.apply = function(compiler) {
compiler.plugin("compile", function(params) {
console.log("The compiler is starting to compile...");
});
};
module.exports = MyPlugin;
2
3
4
5
6
7
8
9
10
11
12
13
# tapable实例
compiler是一个tapable子类(Compiler)的实例
插件提供apply方法, 通过compiler.plugin()
在compile hook上注册回调函数
Compiler的实现应该是类似这样的, 当然也可以使用es6语法
var Tapable = require("tapable");
function Compiler() {
Tapable.call(this);
}
Compiler.prototype = Object.create(Tapable.prototype);
2
3
4
5
6
7
8
Compiler实例可以调用注册的hooks回调方法
var compiler = new Compiler()
compiler.applyPlugins('compile', params)
2
webpack通过调用插件定义的apply方法, 来注册hooks回调函数, 并在合适的时机来执行这些回调函数. 因此tapable是和nodejs的EventEmitter是比较类似的. tapable0.x和EventEmitter同样都是使用观察者模式来实现的.
TIP
tapable0.x在注册hooks回调的时候非常不直观, 因为都是通过tapableInstance.plugin()
, 所以注册的是异步回调还是同步回调, 需要依据具体回调函数的实现
# tapable分析
# apply
void apply(plugins: Plugin...)
tapable实例提供了apply方法, 从而来调用插件提供的apply方法.
webpack3中是这样调用的 (opens new window)
if(options.plugins && Array.isArray(options.plugins)) {
compiler.apply.apply(compiler, options.plugins);
}
2
3
其实现也很简单
Tapable.prototype.apply = function apply() {
for(var i = 0; i < arguments.length; i++) {
arguments[i].apply(this);
}
};
2
3
4
5
遍历插件, 调用其apply方法, 并传入tapable实例作为参数.
TIP
tapable1.x以后, apply就被废弃了, webpack会直接调用插件的apply方法. 这样tapable就可以更加专注于hooks的注册和调用.
# plugin
void plugin(names: string|string[], handler: Function)
通过plugin方法, 可以注册一个或多个hooks回调函数
function Tapable() {
this._plugins = {};
}
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);
};
2
3
4
5
6
7
8
9
10
11
12
13
14
如果name
是数组, 则递归调用plugin
方法, 内部维护_plugins
对象, 来管理所注册的hooks回调.
# applyPlugins
void applyPlugins(name: string, args: any...)
针对注册的回调函数为同步函数
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);
};
2
3
4
5
6
7
根据传入的name
找到_plugins
下的所有回调函数, 遍历执行.
# applyPluginsWaterfall
any applyPluginsWaterfall(name: string, init: any, args: any...)
针对回调函数为同步函数, 使用Waterfall
的方式
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;
};
2
3
4
5
6
7
8
9
10
11
waterfall的方式会将上一个callback return的值,传给下一个callback.
applyPluginsWaterfall
的init
参数为传给第一个callback的值, 然后不断将args[0]
替换成callback return的值, 从而进行传递
# applyPluginsBailResult
any applyPluginsBailResult(name: string, args: any...)
针对回调函数为同步函数, 使用Bail
的方式
Tapable.prototype.applyPluginsBailResult = function applyPluginsBailResult(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++) {
var result = plugins[i].apply(this, args);
if(typeof result !== "undefined") {
return result;
}
}
};
2
3
4
5
6
7
8
9
10
11
bail的方式, 会顺序调用callbacks, 如果遇到某个callback return 非undefined
的值, 则跳出循环, 不再执行接下去的callbacks. 其代码和applyPlugins
非常类似.
# applyPluginsAsync/applyPluginsAsyncSeries
applyPluginsAsyncSeries(
name: string,
args: any...,
callback: (err: Error) -> void
)
2
3
4
5
6
注册的回调函数是异步的, 可以使用这个方法来保证各个回调函数是顺序执行的
Tapable.prototype.applyPluginsAsyncSeries = Tapable.prototype.applyPluginsAsync = function applyPluginsAsyncSeries(name) {
var args = Array.prototype.slice.call(arguments, 1);
// 最后一个参数是callback
var callback = args.pop();
var plugins = this._plugins[name];
// 若没有注册回调函数, 直接执行callback
if(!plugins || plugins.length === 0) return callback();
var i = 0;
var _this = this;
// 将callback包装为next函数
args.push(copyProperties(callback, function next(err) {
// 如果有错误
if(err) return callback(err);
i++;
// 所有注册的回调执行完毕, 执行callback
if(i >= plugins.length) {
return callback();
}
// 执行回调函数
plugins[i].apply(_this, args);
}));
plugins[0].apply(this, args);
};
function copyProperties(from, to) {
for(var key in from)
to[key] = from[key];
return to;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
注册的hook回调函数是异步的, 这个回调函数接收的最后一个参数是next(err: Error)
函数
通过调用next函数来执行下一个注册的回调函数, 在所有注册的回调函数都执行完后, 会执行callback(err: Error)
(applyPluginsAsyncSeries的最后一个参数)
又或者注册的回调函数给next传递了error, 则会终止后续逻辑, 并执行callback(err)
# applyPluginsAsyncWaterfall
applyPluginsAsyncWaterfall(
name: string,
init: any,
callback: (err: Error, result: any) -> void
)
2
3
4
5
6
注册的回调函数是异步的, 使用Waterfall
的方式
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);
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
可以发现其和applyPluginsAsyncSeries
的代码非常类似, 都是通过next函数来和下一个回调函数建立连接, 但是applyPluginsAsyncWaterfall
还可以通过next(null, value)来传递value给下一个回调函数.
TIP
applyPluginsWaterfall(name: String, init: any, args: any...)
除了传递init参数, 还可以继续传递参数
applyPluginsAsyncWaterfall
则只能传递一个init参数和一个callback参数
# applyPluginsAsyncSeriesBailResult
注册的回调函数是异步的, 使用Bail
的方式
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);
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
其实现和applyPluginsAsyncSeries
类似
# applyPluginsParallel
applyPluginsParallel(
name: string,
args: any...,
callback: (err?: Error) -> void
)
2
3
4
5
执行各个回调函数, 回调函数可能是异步的, 和Promise.all
类似
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;
}
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
执行各个注册的回调函数, 并记录还未执行完的数量, 当全部都执行完后, 就执行callback.
# applyPluginsParallelBailResult
applyPluginsParallelBailResult(
name: string,
args: any...,
callback: (err: Error, result: any) -> void
)
2
3
4
5
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);
}
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
如果注册回调如下,
tapableInstance.plugin('a', (cb) => {
setTimeout(() => {
cb(null, 'params1')
}, 300)
})
tapableInstance.plugin('a', (cb) => {
cb(null, 'params2')
})
tapableInstance.applyPluginsParallelBailResult('a', function(err, arg) {
console.log(arg) // params1
})
2
3
4
5
6
7
8
9
10
11
12
执行各个注册的回调函数, 如果某个函数调用了cb, 且传递了值(无论传了err, 或 result), 那么判断调在其前面注册的回调函数是否都执行过next(没有传值), 如果都执行过, 则调用callback(null, 'params')
TIP
applyPluginsParallelBailResult
, 执行callback的条件和applyPluginsBailResult
其实是类似的
applyPluginsBailResult
执行callback的条件是, 前面注册的回调函数都return undefined, 直到执行到某个回调函数return 非undefined.
applyPluginsParallelBailResult
执行callback的条件是,某个注册的回调函数调用了cb,且传值了, 且在其之前注册的回调函数全都调用了cb, 且未传值.
TIP
applyPluginsAsync***的方法, 虽说是针对注册的回调函数为异步的情况, 但是如果注册的回调函数是同步的, 也是适用的.
# 性能
在tapable0.x中的代码中还有applyPlugins0
,applyPlugins1
等方法, 内部使用函数的call
来代替apply
来执行回调, 从而提升性能, jsperf测试代码 (opens new window)
# 应用场景
TODO:
# 痛点
tapable0.x代码重复度还是比较高的