Tapable是webpack事件流的核心
安装
npm i --save-dev tapable
基本用法
Tapable提供了很多种hook,这里介绍webpack中compile类用到的hook,更多请参照Tapable Hooks
const {
SyncHook,
SyncBailHook,
AsyncParallelHook,
AsyncSeriesHook
} = require("tapable");
class App {
constructor() {
//param:描述需要传入的参数,无实际意义
this.hooks = {
syncHook: new SyncHook(["param"]),
syncBailHook: new SyncBailHook(["param"]),
asyncParallelHook: new AsyncParallelHook(["param"]),
asyncSeriesHook: new AsyncSeriesHook(["param"])
};
}
}
const app = new App();
//param:参数无实际意义,接收参数的个数需要与实例的参数个数一致
//同步:
//注册事件:tap 触发事件:call
//所有事件回调函数接收相同参数
app.hooks.syncHook.tap("CountLog", param => console.log(`收到的值:${param}`));
app.hooks.syncHook.tap("CurrentCount", param => console.log(`当前值:${param}`));
app.hooks.syncHook.call(100);
// 收到的值:100
// 当前值:100
//当某个事件回调函数返回任何非undefined的值便终止执行,call的返回值为最后一个事件回调函数的返回值
app.hooks.syncBailHook.tap("p1", param => {
console.log(`syncBailHook p1收到的值:${param}`);
if(param > 50) return param; //终止
});
app.hooks.syncBailHook.tap("p2", param => {
console.log(`syncBailHook p2收到的值:${param}`);
});
app.hooks.syncBailHook.call(Math.floor(Math.random() * 100) + 1);
// p1收到的值:13
// p2收到的值:13
// p1收到的值:94
//异步(并行):
//注册事件:tapAsync tapPromise 触发事件:callAsync promise
//事件回调函数的最后一个参数为callback,当callback的参数不为undefined时,立即调用callAsync传入的回调函数,后续事件回调函数的callback再次调用则不触发callAsync的回调函数,当所有事件回调函数的callback都没传入err时,callAsync的回调函数在所有事件回调函数的callback执行完成后执行
app.hooks.asyncParallelHook.tapAsync("p1", (param, callback) => {
console.log(`asyncParallelHook p1收到的值:${param}`);
callback();
});
app.hooks.asyncParallelHook.tapAsync("p2", (param, callback) => {
setTimeout(() => {
console.log(`asyncParallelHook p2收到的值:${param}`);
callback('p2');
}, 3000);
});
app.hooks.asyncParallelHook.tapAsync("p3", (param, callback) => {
console.log(`asyncParallelHook p3收到的值:${param}`);
callback('p3'); //p3先发生错误,程序不会被终止,但是p2的错误将不再触发callAsync回调函数
});
app.hooks.asyncParallelHook.callAsync(1, err =>{
console.log(`错误:${err}`);
});
// asyncParallelHook p1收到的值:1
// asyncParallelHook p3收到的值:1
// 错误:p3
// asyncParallelHook p2收到的值:1
//事件回调函数必须返回一个promise,调用reject后立即调用promise.catch,后续调用reject不触发promise.catch,没有reject调用则执行promise.then
app.hooks.asyncParallelHook.tapPromise("p1", (param) => {
return new Promise((resolve, reject) => {
console.log(`asyncParallelHook p1收到的值:${param}`);
resolve();
});
});
app.hooks.asyncParallelHook.tapPromise("p2", (param) => {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log(`asyncParallelHook p2收到的值:${param}`);
reject('p2');
}, 3000);
});
});
app.hooks.asyncParallelHook.tapPromise("p3", (param) => {
return new Promise((resolve, reject) => {
console.log(`asyncParallelHook p3收到的值:${param}`);
reject('p3');
});
});
app.hooks.asyncParallelHook.promise(1).then(res => {
console.log(res);
}).catch(err => {
console.log(`错误:${err}`);
});
// asyncParallelHook p1收到的值:1
// asyncParallelHook p3收到的值:1
// 错误:p3
// asyncParallelHook p2收到的值:1
//异步(串行):
//注册事件:tapAsync tapPromise 触发事件:callAsync promise
//事件回调函数的最后一个参数为callback,当callback的参数不为undefined时,立即调用callAsync传入的回调函数,并终止后续事件回调函数执行,当所有事件回调函数的callback都没传入err时,callAsync的回调函数在所有事件回调函数的callback执行完成后执行
app.hooks.asyncSeriesHook.tapAsync("p1", (param, callback) => {
console.log(`asyncSeriesHook p1收到的值:${param}`);
callback();
});
app.hooks.asyncSeriesHook.tapAsync("p2", (param, callback) => {
setTimeout(() => {
console.log(`asyncSeriesHook p2收到的值:${param}`);
callback('p2');
}, 3000);
});
app.hooks.asyncSeriesHook.tapAsync("p3", (param, callback) => {
console.log(`asyncSeriesHook p3收到的值:${param}`);
callback('p3'); //串行等待p2的执行结束,p2发生错误调用callAsync回调函数并终止程序执行
});
app.hooks.asyncSeriesHook.callAsync(1, err => {
console.log(`错误:${err}`);
});
// asyncSeriesHook p1收到的值:1
// asyncSeriesHook p2收到的值:1
// 错误:p2
webpack的工作流程
- initialize:根据shell和config参数调用createCompiler方法得到compiler对象,这里会预先加载所有的plugins,并调用插件的apply方法,传入compiler对象
这里我们拿到compiler对象,基于上面的tapable的知识,我们就可以在webpack的每个生命周期去做我们的事情。
//部分hooks
...
/** @type {SyncHook<[]>} */
initialize: new SyncHook([]),
/** @type {SyncBailHook<[Compilation], boolean>} */
shouldEmit: new SyncBailHook(["compilation"]),
/** @type {AsyncSeriesHook<[Stats]>} */
done: new AsyncSeriesHook(["stats"]),
/** @type {SyncHook<[Stats]>} */
afterDone: new SyncHook(["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<[string, AssetEmittedInfo]>} */
assetEmitted: new AsyncSeriesHook(["file", "info"]),
/** @type {AsyncSeriesHook<[Compilation]>} */
afterEmit: new AsyncSeriesHook(["compilation"])
...
- entryOption: webpack开始读取配置文件的Entries,递归遍历所有的入口文件
- run:程序即将进入构建环节
- compile:程序即将创建compilation实例对象,compilation对象也有许多钩子,并且可以操作打包后的文件
- 从webpack的源码Compiler.js中我们可以看到,当生成compilation对象时会调用两个钩子
- 因此我们在自己的plugin中便可以很容易的拿到compile和compilation两个对象,来完成我们自己的操作
newCompilation(params) {
const compilation = this.createCompilation(params);
compilation.name = this.name;
compilation.records = this.records;
//生成compilation对象时会触发以下两个钩子
this.hooks.thisCompilation.call(compilation, params);
this.hooks.compilation.call(compilation, params);
return compilation;
}
compile(callback) {
const params = this.newCompilationParams();
this.hooks.beforeCompile.callAsync(params, err => {
if (err) return callback(err);
this.hooks.compile.call(params);
const compilation = this.newCompilation(params);
......
}
}
//自己的插件
class MyPlugin{
apply(compiler) {
compiler.hooks.thisCompilation.tap('getCompilation', (compilation, compilationParams)=> {
//do someting
});
}
}
- make:compilation实例启动对代码的编译和构建
- emit:所有打包生成的文件内容已经在内存中按照相应的数据结构处理完毕,下一步会将文件内容输出到文件系统,emit钩子会在生成文件之前执行(通常想操作打包后的文件可以在emit阶段编写plugin实现)
- assetEmitted:执行 Compiler 的 emitAssets 方法把所有的chunk到文件输出到 output 的目录中后触发
- done:编译后的文件已经输出到目标目录,整体代码的构建工作结束时触发
compilation下的钩子含义如下:
- buildModule:在模块构建开始之前触发,这个钩子下可以用来修改模块的参数
- seal:构建工作完成了,compilation对象停止接收新的模块时触发
- optimize:优化阶段开始时触发
compiler进入make阶段后,compilation实例被创建出来,它会先触发buildModule阶段定义的钩子,此时- compilation实例依次进入每一个入口文件(entry),加载相应的loader对代码编译
代码编译完成后,再将编译好的文件内容调用 acorn 解析生成AST语法树,按照此方法继续递归、重复执行该过程
所有模块和和依赖分析完成后,compilation进入seal 阶段,对每个chunk进行整理,接下来进入optimize阶段,开启代码的优化和封装
附上一张完整流程图