webpack-Tapable事件流机制的使用和插件的模拟实现
Webpack的本质
Webpack
可以将其理解为是一种基于事件流的编程范例,一系列的插件运行。
它的工作流程就是将各个插件串联起来,而实现这一切的核心就是Tapable
,Webpack 中最核心的负责编译的 Compiler
和负责创建 bundles 的 Compilation
都是 Tapable
的子类,并且实例内部的生命周期也是通过 Tapable 库提供的钩子类实现的。
核心对象 Compiler
继承 Tapable
class Compiler extends Tapable {
// ...
}
核心对象 Compilation
继承 Tapable
class Compilation extends Tapable {
// ...
}
Tapable 是什么?
Tapable
是一个类似node EventEmitter
的发布订阅模式,但是更加强大,它包含了多种不同的监听和触发事件的方式。主要是控制钩子函数的发布
与订阅,控制着 webpack 的插件系统。
The tapable package expose many Hook classes,which can be used to create hooks for plugins.
tapable 提供了一些用于创建插件的钩子类。
Tapable
的钩子
const {
SyncHook, //同步钩子
SyncBailHook, //同步熔断钩子
SyncWaterfallHook, //同步流水钩子
SyncLoopHook, //同步循环钩子
AsyncParallelHook, //异步并发钩子
AsyncParallelBailHook, //异步并发熔断钩子
AsyncSeriesHook, //异步串行钩子
AsyncSeriesBailHook, //异步串行熔断钩子
AsyncSeriesWaterfallHook //异步串行流水钩子
} = require("tapable");
Tapable hooks
类型
Tapable
的使用
Tapable
暴露出来的都是类方法, new
一个类方法获得我们需要的钩子
举个最简单的例子🌰
const { SyncHook } = require('tapable');
const hook = new SyncHook(['arg1', 'arg2', 'arg3']);
hook.tap('hook1', (arg1, arg2, arg3) => {
console.log(arg1, arg2, arg3);
});
hook.call(1, 2, 3);
上述代码定义了一个同步钩子,可以通过hook
实例对象(SyncHook
本身也是继承自Hook类的)的tap
方法订阅事件,然后利用call
函数触发订阅事件,执行 callback
函数。
值得注意的是 call
传入参数的数量需要与实例化时传递给钩子类构造函数的数组长度保持一致,否则,即使传入了多个,也只能接收到实例化时定义的参数个数。
模拟实现webpack和plugin
既然我们已经知道了webpack
内部的事件流机制,那么他具体是怎么实现的呢?🤔
让我们通过一个插件例子来模拟实现下,首先来看官网给出的编写一个 plugin
的示例
Webpack.config.js
var HelloWorldPlugin = require('hello-world');
module.exports = {
// ... configuration settings here ...
plugins: [new HelloWorldPlugin({ options: true })]
};
class HelloWorldPlugin {
apply(compiler) {
compiler.hooks.done.tap('Hello World Plugin', (
compilation /* compilation is passed as an argument when done hook is tapped. */
) => {
console.log('Hello World!');
});
}
}
module.exports = HelloWorldPlugin;
上述编写了一个叫 HelloWorldPlugin
的类,里面存在 apply
方法,和 compiler
参数,这些都是插件的固定写法,看到 compiler.hooks.done.tap
聪明的你一定想到了,这是使用了 Tapable
的订阅模式。
通过阅读源码,webpack
在循环 options.plugins
配置项的时候会调用插件里面的 apply
方法,在生命周期的 done
节点(也就是上面我们提到的hook
)tap
一个监听事件,当 Webpack
全部流程执行完毕时,监听事件将会被触发。
下面我们来模拟实现 webpack
的 Compiler
,通过新建一个插件 plugin
,内部利用 Tapable
机制实现生命周期。
新建一个webpack-tapable
文件夹
npm init -y
npm i tapable -D
最终的目录结构如下
webpack-tapable
└─node_modules
└─index.js
└─myPlugin.js
└─compiler.js
└─package.json
compiler.js
const { SyncHook, AsyncSeriesHook } = require('tapable');
module.exports = class Compiler {
constructor() {
this.hooks = {
accelerate: new SyncHook(['newspeed']),
brake: new SyncHook(),
calculateRoutes: new AsyncSeriesHook(["source", "target", "routesList"])
}
}
// webpack生命周期,集合加速、刹车、计算路由
// ❗❗注意这里都是call
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);
});
}
}
上面的 Compiler
类定义了一些(伪)编译方法,方法里面调用 Tapable
的 hook
钩子函数。
注意:
run
里的方法都是call,发布hook
MyPlugin.js
class MyPlugin {
constructor() {}
apply(compiler) {
// ❗❗注意这里都是tap
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) => {
// 返回promise
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log(`tapPromise to ${source} ${target} ${routesList}`);
resolve();
}, 1000);
});
}
);
}
}
定义一个 MyPlugin
的插件,compiler
实例对象就是 webpack
里面的 new Compiler
,hooks.xxx
是上文 constructor
中定义的方法。
注意: 插件里都是tap,订阅hook
回到 webpack.config.js
中配置
// index.js
const Compiler = require('./Compiler');
const MyPlugin = require('./myPlugin');
// 这里类比webpack.config.js里面的插件配置
const options = {
// 其他配置,比如entry,devServer....
plugins: [new MyPlugin()],
};
在webpack
源码中,循环 options
配置,并进行相应处理
// index.js
// ...
const compiler = new Compiler();
for (const plugin of options.plugins) {
if (typeof plugin === "function") {
plugin.call(compiler, compiler);
} else {
plugin.apply(compiler);
}
}
当 plugin
为function
类型时候,调用函数本身的 call
方法,否则调用 MyPlugin
中的 apply
方法。
也就是说,如果你的插件逻辑很简单,你可以直接在插件的配置文件里写一个function
,去执行你的逻辑,也是可以通过的,不用非得按照 class
类的写法来实现插件😲。
还记得我们的两个注意嘛
run
里的方法都是call,发布hookplugin
插件里都是tap,订阅hook
怎么样触发呢? 对,就是最后的 run
方法。
// index.js
// ...
compiler.run();
插件中的 tap
监听事件注册到 Webpack
的 compiler、compilation(tapable类)
上,在最上层(源码中的 lib/Webpack.js
),调用 Compiler
类的 run
函数,完成触发,这也符合发布订阅的先监听后触发的逻辑顺序。
最后完整的 index.js
代码如下
// index.js
const Compiler = require('./Compiler');
const MyPlugin = require('./myPlugin');
// 这里类比webpack.config.js里面的插件配置
const options = {
// 其他配置,比如entry,devServer....
plugins: [new 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();
总结
tapable
作为 Webpack
的核心库,承接了 Webpack
最重要的事件流的运转,它巧妙的钩子设计很好的将实现与流程解耦开来,真正实现了插拔式的功能模块。
以上代码属于源码的模拟实现,既然我们已经知道了原理,就可以动手实现一个简易版的 mini-webpack
,还等什么,赶快敲起来~
转载自:https://juejin.cn/post/7132749082375749646