获取中...

-

Just a minute...

前两天看到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就是对象A。tap方法的第一个参数,我将它理解为是一个标记之类的东西
// 因为一个事件可以被好多地方监听,第一个参数就相当于是区分是哪一处代码在监听。实际用处不大
// 第二个参数就是回调方法
compiler.hooks.beforeRun.tap("plugin1-beforerun", () => {
console.log("beforeRun in Plugin1");
});

然后我就可以在另一个地方,通过A.call来主动触发事件。此时,所有A.tap绑定的回调都会被触发。

1
2
3
// 触发beforeRun事件(此处的this和上面代码的compiler是一个实例)
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) {
// 监听beforeRun事件
compiler.hooks.beforeRun.tap("plugin1-beforerun", () => {
console.log("beforeRun in Plugin1");
});
// 监听run事件,并接收一个参数
compiler.hooks.run.tap("plugin1-run", (stage) => {
console.log(`${stage} in plugin1`);
});
// 监听afterRun事件,并接收三个参数
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");
// 做个运算,来当做run了(类比compile代码)
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

相关文章
评论
分享
  • webpack运行时代码简要分析

    开个坑,主要也是怕自己后面忘了 简单写写,有空了补上 __webpack_modules是一个数组,每个元素都是一个函数,函数接受三个参数:第一个参数module好像没用到,第二个是一个对象(会把本模块要导出的属性,赋值给这个对象),...

    webpack运行时代码简要分析
  • webpack动态加载原理简述

    为什么需要动态加载上一篇讲我写webpack针对markdown的loader的时候,提到了希望能动态加载以优化性能。篇幅原因没有展开说,打算单独写一篇文章讲一下。 当我们访问网站的时候,浏览器会向服务器请求页面资源。首先请求的当然是...

    webpack动态加载原理简述
  • 编写一个简单的webpack loader的踩坑

    因为觉得hexo框架搭的博客功能一般,就有了想要自己写一个博客的想法。然后就想着如果把markdown文件也放到项目里的话,会比较方便,发现需要编写一个加载markdown的loader,于是就有了这篇博客,来记录一下踩过的坑和收获...

    编写一个简单的webpack loader的踩坑
  • 动手实现react-ssr 2

    仓库链接:本篇代码 运行方法参考readme,请使用pnpm,因为本次使用的rspack尚未推出1.0版本,预计后续可能会有很多breakchange,所以lock文件至关重要,而我只上传了pnpm的lock,非常抱歉。 环境:no...

    动手实现react-ssr 2
  • 动手实现react ssr

    关于ssr相关的介绍,可以移步之前的一篇博文,本文不再赘述 日常工作中,我们可能都接触过ssr,不过我们可能通常是借助于框架(例如Next.js)的支持来完成ssr。今天我想尝试,摆脱高度封装的前端框架,只依赖react相关的库,来实...

    动手实现react ssr
  • 深入js——作用域,作用域链,执行栈(一)

    最近在补js基础。当初自学js的时候,总是感觉学的不扎实。只能说不去上手,再怎么死读书,也理解不了。积累了经验,然后自己去思考,这个时候再去看理论,就容易理解了。 感觉工作一年以来,确实学到了很多,但是过于专注于应用方面的知识,反倒...

    深入js——作用域,作用域链,执行栈(一)
  • 简单通俗解释HTTP缓存

    开始前的碎碎念 最近加班真的很严重,组里同事们都已经苦不堪言。业务交付压力非常大,根本没时间去磨练本领,学习技术。真的很讨厌这种没有成长的日子,每天被业务需求赶着跑。每天下班之后,本来应该用于提升自我的时间也被压榨用来写业务代码。 ...

    简单通俗解释HTTP缓存
  • 从invalid hook call报错说到npm install相关

    遇到了invalid hook call报错上周,业务下游的同事反馈了一个生产问题,说我们传的值有问题,领导让我帮忙排查一下。我最近一直没做那个项目相关的需求,打开那个项目切到release分支,拉代码,npm i然后启动,结果就白屏...

    从invalid hook call报错说到npm install相关
  • useState为什么获取不到更新后的值,如何解决

    令人困扰的问题是因为异步吗大家都知道,react的useState和useReducer,在set和dispatch之后,马上去获取值,是拿不到最新的值的。这个原因之一是useState和useReducer是异步更新的,也就是说我们...

    useState为什么获取不到更新后的值,如何解决
  • 发布一个npm包的踩坑

    试着自己发布了一个npm包,记录一下自己踩的坑 package.json首先npm init,然后添加type:module字段,将其定义为一个es6模块。然后在files字段设置要发布上去的内容,比如dist文件夹。 接下来将开发用...

    发布一个npm包的踩坑
Please check the parameter of comment in config.yml of hexo-theme-Annie!