起因
想研究一下Webpaack运行原理,发现都提到了tapable,搞得我云里雾里,那我们就好好研究一番,这到底是个啥库。
在Webpack官方文档上,查看Webpack的声明周期钩子函数,可以看到下图的内容:
可以看到run函数是AsyncSeriesHook类型的钩子函数,这个就是tapable提供的钩子类型了。
想理解Webpack的运行流程,先要了解这个钩子的使用,近而了解Webpack在运行的过程中,是如何调用各种插件的。
开始研究
先搭建一个最简单的项目
依照惯例,我们先搭建个最简单的项目:
安装必要的库:
npm install --save-dev wepback
npm install --save-dev webpack-cli
npm install --save-dev webpack-dev-server
npm install --save tapable
复制代码
我们在src下写我们的测试代码,然后运行起来,看我们的实验结果。 webpack.config.js的配置如下:
module.exports = {
entry: {
index: __dirname + "/src/index.js",
},
output: {
path: __dirname + "/dist",//打包后的文件存放的地方
filename: "[name].js", //打包后输出文件的文件名
chunkFilename: '[name].js',
},
mode: 'development',
devtool: false,
devServer: {
contentBase: "./dist",//本地服务器所加载的页面所在的目录
historyApiFallback: true,//不跳转
inline: true//实时刷新
},
}
复制代码
在package.json中配置好启动脚本,使用npm run server即可查看运行结果:
"scripts": {
"start": "webpack",
"server": "webpack-dev-server --open"
},
复制代码
同步钩子
第一个钩子 SyncHook
tapable的github地址是:github.com/webpack/tap…
这里给出的是tapable-1分支的地址,我看这个分支才是Webpack现在使用的。
依照它readme.md中介绍,tapable暴露了很多的Hook类,可以帮助我们为插件创建钩子。
const {
SyncHook,
SyncBailHook,
SyncWaterfallHook,
SyncLoopHook,
AsyncParallelHook,
AsyncParallelBailHook,
AsyncSeriesHook,
AsyncSeriesBailHook,
AsyncSeriesWaterfallHook
} = require("tapable");
复制代码
这么多Hook,我们一个一个来,先看看SyncHook怎么使用,在index.js中写下:
import { SyncHook } from 'tapable';
const hook = new SyncHook(); // 创建钩子对象
hook.tap('logPlugin', () => console.log('被勾了')); // tap方法注册钩子回调
hook.call(); // call方法调用钩子,打印出‘被勾了’三个字
复制代码
使用npm run server,在浏览器中运行成功。也成功打印‘被勾了’。用起来 还是很简单的。
这就是经典的事件注册和触发机制啊。实际使用的时候,声明事件和触发事件的代码通常在一个类中,注册事件的代码在另一个类(我们的插件)中。代码如下:
// Car.js
import { SyncHook } from 'tapable';
export default class Car {
constructor() {
this.startHook = new SyncHook();
}
start() {
this.startHook.call();
}
}
复制代码
// index.js
import Car from './Car';
const car = new Car();
car.startHook.tap('startPlugin', () => console.log('我系一下安全带'));
car.start();
复制代码
钩子的使用基本就是这个意思,Car中只负责声明和调用钩子,真正的执行逻辑,不再Car中,而是在注册它的index.js之中,是在Car之外。这样就做到了很好的解耦。
对于Car而言,通过这种注册插件的方式,丰富自己的功能。
向插件传递参数
我希望这样:
// index.js
import Car from './Car';
const car = new Car();
car.accelerateHook.tap('acceleratePlugin', (speed) => console.log(`加速到${speed}`));
car.accelerate(100); // 调用时,将100传给插件回调的speed
复制代码
可以这样写Car类:
import { SyncHook } from 'tapable';
export default class Car {
constructor() {
this.startHook = new SyncHook();
this.accelerateHook = new SyncHook(["newSpeed"]); // 在声明的时候,说明我这个Hook需要一个参数即可。
}
start() {
this.startHook.call();
}
accelerate(speed) {
this.accelerateHook.call(speed);
}
}
复制代码
这样就完成了带参数的Hook,SyncHook参数是传递个数组,就是说也可以让我们传递多个参数,如 new SyncHook(["arg1","arg2","arg3"])。这样在call的时候也可以传递三个参数,在回调函数,也能接收到call的三个参数。
我们的Car类,就是一个Tapable类,事件的声明和调用中心。
第二个钩子 SyncBailHook
Hook的注册/调用机制我们大致了解了,SyncHook的工作很完美,但是tapable还提供了很多Hook,这些Hook又是解决什么问题的呢?
原因在于对于某一个事件,我们可以注册多次,如下:
const car = new Car();
car.hooks.brake.tap('brakePlugin1', () => console.log(`刹车1`));
car.hooks.brake.tap('brakePlugin2', () => console.log(`刹车2`));
car.hooks.brake.tap('brakePlugin3', () => console.log(`刹车3`));
car.brake(); // 会打印‘刹车1’‘刹车2’‘刹车3’
复制代码
这里我们为Car类添加了hooks.brake的钩子,和一个brake方法。brake的钩子被注册了3次,我们调用brake方式的时候,3个插件都接受到了事件。
我们稍微重构了一下Car类,据说这种写法,更符合tapable使用的最佳实践,其实就是将钩子都放到一个hooks字段里。Car代码如下:
import { SyncHook, SyncBailHook } from 'tapable';
export default class Car {
constructor() {
this.hooks = {
start: new SyncHook(),
accelerate: new SyncHook(["newSpeed"]),
brake: new SyncBailHook(), // 这里我们要使用SyncBailHook钩子啦
};
}
start() {
this.hooks.start.call();
}
accelerate(speed) {
this.hooks.accelerate.call(speed);
}
brake() {
this.hooks.brake.call();
}
}
复制代码
我们现在要满足这样一个需求,不管你注册多少插件,我只想被刹两次,就不通知别的插件了。这时候就SyncBailHook就可以,代码如下:
import Car from './Car';
const car = new Car();
car.hooks.brake.tap('brakePlugin1', () => console.log(`刹车1`));
// 只需在不想继续往下走的插件return非undefined即可。
car.hooks.brake.tap('brakePlugin2', () => { console.log(`刹车2`); return 1; });
car.hooks.brake.tap('brakePlugin3', () => console.log(`刹车3`));
car.brake(); // 只会打印‘刹车1’‘刹车2’
复制代码
SyncBailHook就是根据每一步返回的值来决定要不要继续往下走,如果return了一个非undefined的值 那就不会往下走,注意 如果什么都不return 也相当于return了一个undefined。
由此推测,tabable提供各类钩子,目的是处理这些外部插件的关系。
第三个钩子 SyncWaterfallHook
搞明白了第二个钩子,接下来的钩子就很好理解了,这里直接给出SyncWaterfallHook的定义:它的每一步都依赖上一步的执行结果,也就是上一步return的值就是下一步的参数。
我们改造一下accelerate钩子为SyncWaterfallHook:
import { SyncHook, SyncBailHook, SyncWaterfallHook } from 'tapable';
export default class Car {
constructor() {
this.hooks = {
start: new SyncHook(),
accelerate: new SyncWaterfallHook(["newSpeed"]), // 重点在这里
brake: new SyncBailHook(),
};
}
start() {
this.hooks.start.call();
}
accelerate(speed) {
this.hooks.accelerate.call(speed);
}
brake() {
this.hooks.brake.call();
}
}
复制代码
// index.js
import Car from './Car';
const car = new Car();
car.hooks.accelerate.tap('acceleratePlugin1', (speed) => { console.log(`加速到${speed}`); return speed + 100; });
car.hooks.accelerate.tap('acceleratePlugin2', (speed) => { console.log(`加速到${speed}`); return speed + 100; });
car.hooks.accelerate.tap('acceleratePlugin3', (speed) => { console.log(`加速到${speed}`); });
car.accelerate(50); // 打印‘加速到50’‘加速到150’‘加速到250’
复制代码
第四个钩子 SyncLoopHook
SyncLoopHook是同步的循环钩子,它的插件如果返回一个非undefined。就会一直执行这个插件的回调函数,直到它返回undefined。
我们把start的钩子改成SyncLoopHook。
import { SyncHook, SyncBailHook, SyncWaterfallHook, SyncLoopHook } from 'tapable';
export default class Car {
constructor() {
this.hooks = {
start: new SyncLoopHook(), // 重点看这里
accelerate: new SyncWaterfallHook(["newSpeed"]),
brake: new SyncBailHook(),
};
}
start() {
this.hooks.start.call();
}
accelerate(speed) {
this.hooks.accelerate.call(speed);
}
brake() {
this.hooks.brake.call();
}
}
复制代码
// index.js
import Car from './Car';
let index = 0;
const car = new Car();
car.hooks.start.tap('startPlugin1', () => {
console.log(`启动`);
if (index < 5) {
index++;
return 1;
}
}); // 这回我们得到一辆破车,启动6次才会启动成功。
car.hooks.start.tap('startPlugin2', () => {
console.log(`启动成功`);
});
car.start(); // 打印‘启动’6次,打印‘启动成功’一次。
复制代码
异步钩子
当插件的回调函数,存在异步的时候。就需要使用异步的钩子了。
第五个钩子 AsyncParallelHook
AsyncParallelHook处理异步并行执行的插件。
我们在Car类中添加calculateRoutes,使用AsyncParallelHook。再写一个calculateRoutes方法,调用callAsync方法时会触发钩子执行。这里可以传递一个回调,当所有插件都执行完毕的时候,被调用。
// Car.js
import {
...
AsyncParallelHook,
} from 'tapable';
export default class Car {
constructor() {
this.hooks = {
...
calculateRoutes: new AsyncParallelHook(),
};
}
...
calculateRoutes(callback) {
this.hooks.calculateRoutes.callAsync(callback);
}
}
复制代码
// index.js
import Car from './Car';
const car = new Car();
car.hooks.calculateRoutes.tapAsync('calculateRoutesPlugin1', (callback) => {
setTimeout(() => {
console.log('计算路线1');
callback();
}, 1000);
});
car.hooks.calculateRoutes.tapAsync('calculateRoutesPlugin2', (callback) => {
setTimeout(() => {
console.log('计算路线2');
callback();
}, 2000);
});
car.calculateRoutes(() => { console.log('最终的回调'); }); // 会在1s的时候打印‘计算路线1’。2s的时候打印‘计算路线2’。紧接着打印‘最终的回调’
复制代码
我觉得AsyncParallelHook的精髓就在于这个最终的回调。当所有的异步任务执行结束后,再最终的回调中执行接下来的代码。可以确保所有的插件的代码都执行完毕后,再执行某些逻辑。如果不需要这个最终的回调来执行某些代码,那使用SyncHook就行了啊,反正你又不关心插件中的代码什么时候执行完毕。
AsyncParallelHook的Promise方式
除了使用tapAsync/callAsync的方式使用AsyncParallelHook。还可以使用tapPromise/promise的方式。
代码如下:
// Car.js
import {
SyncHook, SyncBailHook, SyncWaterfallHook, SyncLoopHook,
AsyncParallelHook,
} from 'tapable';
export default class Car {
constructor() {
this.hooks = {
...
calculateRoutes: new AsyncParallelHook(),
};
}
...
calculateRoutes() {
return this.hooks.calculateRoutes.promise();
}
}
复制代码
// index.js
import Car from './Car';
const car = new Car();
car.hooks.calculateRoutes.tapPromise('calculateRoutesPlugin1', () => {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log('计算路线1');
resolve();
}, 1000);
});
});
car.hooks.calculateRoutes.tapPromise('calculateRoutesPlugin2', () => {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log('计算路线2');
resolve();
}, 2000);
});
});
car.calculateRoutes().then(() => { console.log('最终的回调'); });
复制代码
只是用法上有区别,效果同tapAsync/callAsync一样的。
第六个钩子 AsyncParallelBailHook
这个我靠猜都知道它是怎么回事了,插件都并行执行,有一个执行成功并且传递的值不是undefined,就调用最终的回调。
来验证一下猜想:
// Car.js
import {
AsyncParallelBailHook,
} from 'tapable';
export default class Car {
constructor() {
this.hooks = {
drift: new AsyncParallelBailHook(),
};
}
drift(callback) {
this.hooks.drift.callAsync(callback);
}
}
复制代码
// index.js
import Car from './Car';
const car = new Car();
car.hooks.drift.tapAsync('driftPlugin1', (callback) => {
setTimeout(() => {
console.log('计算路线1');
callback(1); // 这里传递个1,不是undefined
}, 1000);
});
car.hooks.drift.tapAsync('driftPlugin2', (callback) => {
setTimeout(() => {
console.log('计算路线2');
callback(2); // 这里传递个2,不是undefined
}, 2000);
});
car.drift((result) => { console.log('最终的回调', result); });
// 打印结果是,等1s打印'计算路线1' ,马上打印‘最终的回调 1’,再到第2s,打印'计算路线2'
复制代码
我们来分析下打印结果,说明AsyncParallelBailHook在插件调用callback时,如果给callback传参数,就会立马调用最终的回调函数。但并不会阻止其他插件继续执行自己的异步,只不过最终的回调拿不到这些比较慢的插件的回调结果了。
同样的AsyncParallelBailHook也有promise的调用方式,与AsyncParallelHook类似,咱们就不实验了。
第七个钩子 AsyncSeriesHook
说完了并行,那一定有串行。就是插件一个一个的按顺序执行。
实验代码如下:
// Car.js
import {
AsyncSeriesHook,
} from 'tapable';
export default class Car {
constructor() {
this.hooks = {
calculateRoutes: new AsyncSeriesHook(),
};
}
calculateRoutes() {
return this.hooks.calculateRoutes.promise();
}
}
复制代码
// index.js
import Car from './Car';
const car = new Car();
car.hooks.calculateRoutes.tapPromise('calculateRoutesPlugin1', () => {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log('计算路线1');
resolve();
}, 1000);
});
});
car.hooks.calculateRoutes.tapPromise('calculateRoutesPlugin2', () => {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log('计算路线2');
resolve();
}, 2000);
});
});
car.calculateRoutes().then(() => { console.log('最终的回调'); });
// 1s过后,打印计算路线1,再过2s(而不是到了第2s,而是到了第3s),打印计算路线2,再立马打印最终的回调。
复制代码
我们这里直接使用promise的格式,一样执行。
第八个钩子 AsyncSeriesBailHook
串行执行,并且只要一个插件有返回值,立马调用最终的回调,并且不会继续执行后续的插件。
实验代码如下:
// Car.js
import {
AsyncSeriesBailHook,
} from 'tapable';
export default class Car {
constructor() {
this.hooks = {
calculateRoutes: new AsyncSeriesBailHook(),
};
}
calculateRoutes() {
return this.hooks.calculateRoutes.promise();
}
}
复制代码
// index.js
import Car from './Car';
const car = new Car();
car.hooks.calculateRoutes.tapPromise('calculateRoutesPlugin1', () => {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log('计算路线1');
resolve(1);
}, 1000);
});
});
car.hooks.calculateRoutes.tapPromise('calculateRoutesPlugin2', () => {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log('计算路线2');
resolve(2);
}, 2000);
});
});
car.calculateRoutes().then(() => { console.log('最终的回调'); });
// 1s过后,打印计算路线1,立马打印最终的回调,不会再执行计算路线2了。
复制代码
第九个钩子 AsyncSeriesWaterfallHook
串行执行,并且前一个插件的返回值,会作为后一个插件的参数。
代码如下:
// Car.js
import {
AsyncSeriesWaterfallHook,
} from 'tapable';
export default class Car {
constructor() {
this.hooks = {
calculateRoutes: new AsyncSeriesWaterfallHook(['home']), // 要标注一下,要传参数啦
};
}
calculateRoutes() {
return this.hooks.calculateRoutes.promise();
}
}
复制代码
// index.js
import Car from './Car';
const car = new Car();
car.hooks.calculateRoutes.tapPromise('calculateRoutesPlugin1', (result) => {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log('计算路线1', result);
resolve(1);
}, 1000);
});
});
car.hooks.calculateRoutes.tapPromise('calculateRoutesPlugin2', (result) => {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log('计算路线2', result);
resolve(2);
}, 2000);
});
});
car.calculateRoutes().then(() => { console.log('最终的回调'); });
// 1s过后,打印计算路线1 undefined,再过2s打印计算路线2 北京,然后立马打印最终的回调。
复制代码
打印结果如图:
结束语
tapable的简单使用,就研究到这里。它为插件机制提供了很强大的支持,不但让我们对主体(Car)注册各种插件,还能控制插件彼此的关系,控制自身相应的时机。
在Webpack中使用这样的库,再合适不过,Webpack是一个插件的集合,通过tapable,有效的将插件们组织起来,在合理的时机,合理的调用。