前两天看到ByteFE公众号发了篇文章,从源码去讲webpack。我之前也从很多方面去学习webpack,只是源码一直没太看得进去,这一次我想仔细地读一读。
简要介绍tapable
说实话,之前看webpack源码一直没看的太懂,看不进去,有一部分原因就是它的hooks我根本看不懂是在干什么。比如这样,一堆hooks,还有全是回调函数,看不进去
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| const run = () => { this.hooks.beforeRun.callAsync(this, err => { if (err) return finalCallback(err);
this.hooks.run.callAsync(this, err => { if (err) return finalCallback(err);
this.readRecords(err => { if (err) return finalCallback(err);
this.compile(onCompiled); }); }); }); };
|
我去看了一下,this.hooks
里面的各个属性,都是从tapable导出的类的实例。我就去了解了一下tapable这个包,它是一个基于发布订阅模式的处理消息的npm包,webpack的插件机制就是基于它实现的。
它的基础用法就是,先从tapable导出的某种hooks实例化一个对象A。然后通过一个方法比如A.tap,去给A绑定一个回调。
1 2 3 4 5 6
|
compiler.hooks.beforeRun.tap("plugin1-beforerun", () => { console.log("beforeRun in Plugin1"); });
|
然后我就可以在另一个地方,通过A.call来主动触发事件。此时,所有A.tap绑定的回调都会被触发。
1 2 3
| this.hooks.beforeRun.call();
|
tapable导出的hooks有很多种,有同步触发的,有异步触发的。有多个监听者并行触发的,有串行触发的。我们今天就只看最简单的一种,同步触发,来简易地模拟一下webpack的插件机制。
动手实践
首先我们pnpm init
,然后pnpm i tapable
(我确实喜欢pnpm,没有幽灵依赖,依赖版本明确,节省磁盘空间)
然后我们新建一个src目录,接下来我们就模仿webpack的代码,开始动手。
首先我们来写两个简单的webpack插件
src/Plugin/plugin1.js
我们先来用class来实现一个webpack插件。我们需要实现一个class,它有一个apply方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| class Plugin1 { apply(compiler) { compiler.hooks.beforeRun.tap("plugin1-beforerun", () => { console.log("beforeRun in Plugin1"); }); compiler.hooks.run.tap("plugin1-run", (stage) => { console.log(`${stage} in plugin1`); }); compiler.hooks.afterRun.tap( "plugin1-afterrun", (stage, timestamp, result) => { console.log( `${stage} in plugin1, timestamp is ${timestamp}, and the result is ${result}` ); } ); } }
module.exports = Plugin1;
|
src/Plugin/plugin2.js
接下来我们用function来实现一个webpack插件,我们直接在函数体里面给事件绑定回调就可以了,代码完全和上一个插件一样,只是打印出来的plugin1改成plugin2
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| function Plugin2(compiler) { compiler.hooks.beforeRun.tap("plugin2-beforerun", () => { console.log("beforeRun in Plugin2"); }); compiler.hooks.run.tap("plugin2-run", (stage) => { console.log(`${stage} in plugin1`); }); compiler.hooks.afterRun.tap( "plugin2-afterrun", (stage, timestamp, result) => { console.log( `${stage} in plugin2, timestamp is ${timestamp}, and the result is ${result}` ); } ); }
module.exports = Plugin2
|
接下来就是我们用来类比webpack的两个文件
src/Compiler.js
类比webpack中/lib/Compiler.js,我们只实现其中核心的run方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| const { SyncHook } = require("tapable"); class MyCompiler { constructor(options) { this.result = 0; this.hooks = { beforeRun: new SyncHook(), run: new SyncHook(["stage"]), afterRun: new SyncHook(["stage", "timestamp", "calResult"]), }; } run() { this.hooks.beforeRun.call(); this.hooks.run.call("run"); this.result++; const timestamp = new Date().getTime(); this.hooks.afterRun.call("afterRun", timestamp, this.result); } }
module.exports = MyCompiler;
|
src/webpack.js
类比webpack中/lib/webpack.js,只不过把调用webpack的代码也写进来了。
这个文件的核心部分写的相当简陋,webpack里面只做了创建compiler和compiler.run两件事
而createCompiler里面则负责将compiler实例化,并且注册插件。如果只是我这样简易的实现的话,没必要把注册插件的代码特地拿出来写一个方法,完全可以写到compiler的构造函数里。但是实际的webpack在createCompiler里面还做了很多事,不方便写到构造函数里。而我为了能够使读者更容易将我的代码和webpack类比起来,所以采用了和webpack相似的写法。
注册插件的时候,如果插件本身是一个函数,那么直接执行它就可以了。如果插件本身是一个类,那么则需要实例化,并且调用它的apply方法。以上两种方式,都会让插件去成功监听webpack触发的各种事件。插件可以根据自己的需要,给不同的阶段加上不同的回调
1 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 30
| const MyCompiler = require("./Compiler"); const Plugin1 = require("./Plugin/plugin1"); const plugin2 = require("./Plugin/plugin2");
const plugin1 = new Plugin1(); const options = { plugins: [plugin1, plugin2], };
const createCompiler = (options) => { const compiler = new MyCompiler(options); if (Array.isArray(options.plugins)) { for (const plugin of options.plugins) { if (typeof plugin === "function") { plugin.call(compiler, compiler); } else { plugin.apply(compiler); } } } return compiler; };
const webpack = (options) => { const compiler = createCompiler(options); compiler.run(); };
webpack(options);
|
后续
其实tapable远不止上面这么简单,比如各处监听者的回调触发的时机,各处回调之间的互相通信,等等。但是这个简单的例子,已经足够让大家了解webpack的插件机制了。后面的文章随缘更新,也许是继续深入了解webpack,也许是更进一步研究tapable