# webpack插件机制
上一节讲到了npm scripts运行webpack构建时候,实际查找的node_modules/webpack/bin/webpack.js文件 并判断CLI是否安装,如果按照了CLI就调用webpack-cli对参数分组转换为webpack可识别的编译配置项,最后再次 调用webpack执行compiler实例。
通过webpack源码我们可以了解到,webpack函数会创建compiler实例。
//webpack.js
...
const Compiler = require("./Compiler");
const MultiCompiler = require("./MultiCompiler");
...
const webpack = (options, callback) => {
...
let compiler;
// 如果有多个options,则创建多个compiler实例
if (Array.isArray(options)) {
compiler = new MultiCompiler(
Array.from(options).map(options => webpack(options))
);
} else if (typeof options === "object") {
// 如果只有一个,则实例化一个compiler
// 默认配置初始化
options = new WebpackOptionsDefaulter().process(options);
compiler = new Compiler(options.context);
compiler.options = options;
new NodeEnvironmentPlugin({
infrastructureLogging: options.infrastructureLogging
}).apply(compiler);
// 遍历插件并将compiler对象传递给插件,当compiler钩子触发时候实现plugin内部监听
if (options.plugins && Array.isArray(options.plugins)) {
for (const plugin of options.plugins) {
if (typeof plugin === "function") {
plugin.call(compiler, compiler);
} else {
plugin.apply(compiler);
}
}
}
compiler.hooks.environment.call();
compiler.hooks.afterEnvironment.call();
// 注入内部插件
compiler.options = new WebpackOptionsApply().process(options, compiler);
} else {
throw new Error("Invalid argument: options");
}
if (callback) {
if (typeof callback !== "function") {
throw new Error("Invalid argument: callback");
}
if (
options.watch === true ||
(Array.isArray(options) && options.some(o => o.watch))
) {
const watchOptions = Array.isArray(options)
? options.map(o => o.watchOptions || {})
: options.watchOptions || {};
return compiler.watch(watchOptions, callback);
}
compiler.run(callback);
}
return compiler;
};
...
而从Compiler.js源码可以看到,核心对象Compiler是对Tapable的继承和扩展。
//Compiler.js
...
const {
Tapable,
SyncHook,
SyncBailHook,
AsyncParallelHook,
AsyncSeriesHook
} = require("tapable");
const Compilation = require("./Compilation");
...
class Compiler extends Tapable {
constructor(context) {
super();
this.hooks = {
shouldEmit: new SyncBailHook(["compilation"]),
done: new AsyncSeriesHook(["stats"]),
beforeRun: new AsyncSeriesHook(["compiler"]),
run: new AsyncSeriesHook(["compiler"]),
emit: new AsyncSeriesHook(["compilation"]),
afterEmit: new AsyncSeriesHook(["compilation"]),
compilation: new SyncHook(["compilation", "params"]),
beforeCompile: new AsyncSeriesHook(["params"]),
compile: new SyncHook(["params"]),
make: new AsyncParallelHook(["compilation"]),
afterCompile: new AsyncSeriesHook(["compilation"]),
watchRun: new AsyncSeriesHook(["compiler"]),
failed: new SyncHook(["error"]),
watchClose: new SyncHook([]),
...
};
...
}
watch(watchOptions, handler) {...}
run(callback) {...}
...
compile(callback) {...}
}
module.exports = Compiler;
通过Comiler源码了解到,Compiler通过Compilation创建来的实例,分析Compilation.js源码可以看到,Compilation 通用继承至Tapable。
//Compilation.js
...
const {
Tapable,
SyncHook,
SyncBailHook,
SyncWaterfallHook,
AsyncSeriesHook
} = require("tapable");
...
class Compilation extends Tapable {
constructor(compiler) {
super();
this.hooks = {
buildModule: new SyncHook(["module"]),
rebuildModule: new SyncHook(["module"]),
failedModule: new SyncHook(["module", "error"]),
succeedModule: new SyncHook(["module"]),
addEntry: new SyncHook(["entry", "name"]),
failedEntry: new SyncHook(["entry", "name", "error"]),
succeedEntry: new SyncHook(["entry", "name", "module"]),
dependencyReference: new SyncWaterfallHook([
"dependencyReference",
"dependency",
"module"
]),
finishModules: new AsyncSeriesHook(["modules"]),
finishRebuildingModule: new SyncHook(["module"]),
unseal: new SyncHook([]),
seal: new SyncHook([]),
beforeChunks: new SyncHook([]),
afterChunks: new SyncHook(["chunks"]),
optimizeDependenciesBasic: new SyncBailHook(["modules"]),
optimizeDependencies: new SyncBailHook(["modules"]),
optimizeDependenciesAdvanced: new SyncBailHook(["modules"]),
afterOptimizeDependencies: new SyncHook(["modules"]),
optimize: new SyncHook([]),
optimizeModulesBasic: new SyncBailHook(["modules"]),
optimizeModules: new SyncBailHook(["modules"]),
optimizeModulesAdvanced: new SyncBailHook(["modules"]),
afterOptimizeModules: new SyncHook(["modules"]),
optimizeChunksBasic: new SyncBailHook(["chunks", "chunkGroups"]),
optimizeChunks: new SyncBailHook(["chunks", "chunkGroups"]),
optimizeChunksAdvanced: new SyncBailHook(["chunks", "chunkGroups"]),
afterOptimizeChunks: new SyncHook(["chunks", "chunkGroups"]),
optimizeTree: new AsyncSeriesHook(["chunks", "modules"]),
afterOptimizeTree: new SyncHook(["chunks", "modules"]),
optimizeChunkModulesBasic: new SyncBailHook(["chunks", "modules"]),
optimizeChunkModules: new SyncBailHook(["chunks", "modules"]),
optimizeChunkModulesAdvanced: new SyncBailHook(["chunks", "modules"]),
afterOptimizeChunkModules: new SyncHook(["chunks", "modules"]),
shouldRecord: new SyncBailHook([]),
reviveModules: new SyncHook(["modules", "records"]),
optimizeModuleOrder: new SyncHook(["modules"]),
advancedOptimizeModuleOrder: new SyncHook(["modules"]),
beforeModuleIds: new SyncHook(["modules"]),
moduleIds: new SyncHook(["modules"]),
optimizeModuleIds: new SyncHook(["modules"]),
afterOptimizeModuleIds: new SyncHook(["modules"]),
reviveChunks: new SyncHook(["chunks", "records"]),
optimizeChunkOrder: new SyncHook(["chunks"]),
beforeChunkIds: new SyncHook(["chunks"]),
optimizeChunkIds: new SyncHook(["chunks"]),
afterOptimizeChunkIds: new SyncHook(["chunks"]),
recordModules: new SyncHook(["modules", "records"]),
recordChunks: new SyncHook(["chunks", "records"]),
beforeHash: new SyncHook([]),
contentHash: new SyncHook(["chunk"]),
afterHash: new SyncHook([]),
recordHash: new SyncHook(["records"]),
record: new SyncHook(["compilation", "records"]),
beforeModuleAssets: new SyncHook([]),
shouldGenerateChunkAssets: new SyncBailHook([]),
beforeChunkAssets: new SyncHook([]),
additionalChunkAssets: new SyncHook(["chunks"]),
additionalAssets: new AsyncSeriesHook([]),
optimizeChunkAssets: new AsyncSeriesHook(["chunks"]),
afterOptimizeChunkAssets: new SyncHook(["chunks"]),
optimizeAssets: new AsyncSeriesHook(["assets"]),
afterOptimizeAssets: new SyncHook(["assets"]),
needAdditionalSeal: new SyncBailHook([]),
afterSeal: new AsyncSeriesHook([]),
chunkHash: new SyncHook(["chunk", "chunkHash"]),
moduleAsset: new SyncHook(["module", "filename"]),
chunkAsset: new SyncHook(["chunk", "filename"]),
assetPath: new SyncWaterfallHook(["filename", "data"]), // TODO MainTemplate
needAdditionalPass: new SyncBailHook([]),
childCompiler: new SyncHook([
"childCompiler",
"compilerName",
"compilerIndex"
]),
log: new SyncBailHook(["origin", "logEntry"]),
normalModuleLoader: new SyncHook(["loaderContext", "module"]),
optimizeExtractedChunksBasic: new SyncBailHook(["chunks"]),
optimizeExtractedChunks: new SyncBailHook(["chunks"]),
optimizeExtractedChunksAdvanced: new SyncBailHook(["chunks"]),
afterOptimizeExtractedChunks: new SyncHook(["chunks"])
};
...
}
...
}
Compilation.prototype.applyPlugins = util.deprecate(...);
Object.defineProperty(Compilation.prototype, "moduleTemplate", {...});
module.exports = Compilation;
至此,我们可以了解到webpack的核心对象Compiler和Compilation都是继承至Tapable,实现Tapable和webpack的关联。
而Tapable主要是暴露出钩子函数,为插件提供挂载的钩子,换言之,主要是控制钩子函数的发布与订阅,
控制着webpack的插件系统。
所以可以总结到,webpack可以理解为一种基于事件流的编程范例,一系列的插件运行流程。通过监听插件上定义的
compiler和compilation的关键节点实行相应的事件操作。
# Tapable
Tapable是一个发布订阅的库,主要功能是提供钩子函数的发布与订阅,
也是webpack插件系统的实现。
Tapable暴露了很多钩子,为插件提供挂载的钩子
const {
SyncHook, //同步钩子
SyncBailHook, //同步熔断钩子
SyncWaterfallHook, //同步流水钩子
SyncLoopHook, //同步循环钩子
AsyncParallelHook, //异步并发钩子
AsyncParallelBailHook, //异步并发熔断钩子
AsyncSeriesHook, //异步串行钩子
AsyncSeriesBailHook, //异步串行熔断钩子
AsyncSeriesWaterfallHook //异步串行流水钩子
} = require("tapable");
Tapable钩子类型
类型 | 方法 |
---|---|
Hook | 所有钩子的后缀 |
Waterfall | 同步方法,但是会传值给下一个函数 |
Bail | 熔断:当函数有任何返回值,就会在当前执行函数停止 |
Loop | 监听函数返回true表示继续循环,返回undefine表示结束循环 |
Sync | 同步方法 |
AsyncSeries | 异步串行钩子 |
AsyncParallel | 异步并行执行钩子 |
# Tapable的使用
一、new Hook新建钩子
由于Tapable暴露出来的是类方法,所有通过new新建钩子函数。
其中,类方法接受数组参数options(非必传)。类方法根据传参,接受通用数量的参数。
// 创建同步钩子
const hook = new SyncHook(["arg1", "arg2", "arg3"])
二、钩子的发布与订阅
Tapable提供了同步和异步绑定钩子的方法,并且它们都有绑定事件和执行事件对应的方法。
异步 | 同步 |
---|---|
绑定:tapAsync/tapPromise/tap | 绑定:tap |
执行:callAsync/promise | 执行:call |
三、hook基本用法
// 创建同步钩子
const hook1 = new SyncHook(["arg1", "arg2", "arg3"])
// 绑定事件到事件流
hook1.tap('hook1',(arg1,arg2,arg3)=>console.log(arg1,arg2,arg3)) //1,2,3
// 执行绑定的事件
hook1.call(1,2,3)
创建Car类,并定义同步钩子、异步钩子并在钩子上绑定和执行方法。
const {
SyncHook,
AsyncSeriesHook
} = require('tapable')
class Car {
constructor(){
this.hooks = {
accelerate: new SyncHook(['newspeed']),
brake: new SyncHook(),
calculateRoutes: new AsyncSeriesHook(["source", "target", "routesList"])
}
}
}
const myCar = new Car()
// 绑定同步钩子
myCar.hooks.brake.tap("WarningLampPlugin", () => console.log('WarningLampPlugin'))
// 绑定同步钩子并传参
myCar.hooks.accelerate.tap('LoggerPlugin', newSpeed => console.log('Accelerating to'+newSpeed))
//绑定一个异步Promise钩子
myCar.hooks.calculateRoutes.tapPromise('calculateRoutes tapPromise',(source, target, routesList)=>{
console.log('source',source)
return new Promise((resolve,reject)=>{
setTimeout(()=>{
console.log(`tapPromise to ${source} ${target} ${routesList}`)
resolve()
},1000)
})
})
//执行同步钩子
myCar.hooks.brake.call()
myCar.hooks.accelerate.call(10)
//执行异步钩子
myCar.hooks.calculateRoutes.promise('Async','hook','demo').then(()=>{
console.log('succ')
},err=>{
console.err(err)
})
运行结果:
//D:\github\blogCode\webpack\Tapable-demo>node car.js
WarningLampPlugin
Accelerating to10
source Async
tapPromise to Async hook demo
succ
# 模拟插件执行
至此,我们可以了解到了webpack通过Compiler提供hooks钩子函数,为插件plugin提供内部监听。
# 模拟Compiler.js
模拟Compiler.js,通过Tapable的钩子函数定义钩子并提供run方法。
module.exports = class Compiler {
constructor() {
this.hooks = {
accelerate: new SyncHook(['newspeed']),
brake: new SyncHook(),
calculateRoutes: new AsyncSeriesHook(["source", "target", "routesList"])
}
}
run(){
this.accelerate(10)
this.break()
this.calculateRoutes('Async', 'hook', 'demo')
}
accelerate(speed) {
this.hooks.accelerate.call(speed);
}
break() {
this.hooks.brake.call();
}
calculateRoutes() {
this.hooks.calculateRoutes.promise(...arguments).then(() => {
}, err => {
console.error(err);
});
}
}
# 模拟插件实现
模拟插件实现,绑定钩子函数
const Compiler = require('./Compiler')
class MyPlugin{
constructor() {
}
apply(compiler){
compiler.hooks.brake.tap("WarningLampPlugin", () => console.log('WarningLampPlugin'));
compiler.hooks.accelerate.tap("LoggerPlugin", newSpeed =>
console.log(`Accelerating to ${newSpeed}`)
);
compiler.hooks.calculateRoutes.tapPromise("calculateRoutes tapAsync",
(source, target, routesList) => {
return new Promise((resolve,reject)=>{
setTimeout(()=>{
console.log(`tapPromise to ${source} ${target} ${routesList}`)
resolve();
},1000)
});
});
}
}
# 模拟插件执行
模拟插件执行的过程,通过实例化compiler并遍历plugins,将compiler传递给plugin实现插件监听。
const myPlugin = new MyPlugin();
const options = {
plugins: [myPlugin]
}
const compiler = new Compiler();
for (const plugin of options.plugins) {
if (typeof plugin === "function") {
plugin.call(compiler, compiler);
} else {
plugin.apply(compiler);
}
}
compiler.run();
代码链接:https://github.com/PCAaron/blogCode/tree/master/webpack/Tapale-demo