多进程打包:thread-loader 源码(1)
一、背景
这不又到年初了,老板又拉了一大票成长计划(O(JB)KR
),我头脑一热果断选择了一个我最不太熟悉的领域——并发程(多线程/进程)打包。
然而最近业务繁重,根本时间搞,连摸鱼的时间都没有了,咋办?
思虑再三,决定接着写小作文,一点一点磕,磕一点就写一点,这样也有动力,毕竟这种贡(刷)献(存)社(在)区(感)的摸鱼工作还是值得坚持下去的。
前面写的浅羲Vue源码这个大坑还没填完,但是又不得不开一个新坑,我保证这事儿干完接着填 Vue
源码的大坑。
多进程(线程)打包的实现有很多,比如大家都听过的子编译
、happypack
、thread-loader
... happypack 作者对 js
兴趣退散了,最近不咋维护了,所以我们选择了 thread-loader 这个方案。
不能再多说背景了,再多说就被同事发现我了,这个活儿是记名的,否则这番吐槽就要到老板耳朵里面了
二、thread-loader 是个啥?
thread-loder
是个 loader
,他不处理具体的转换模块到js
的工作,而是把他后面的 loader
扔进一个工作线程池(worker pool)中并发运行,上传送门# thread-loader
2.1 thread-loader 的限制
运行在 worker pool
中的 loader
是受限制的,主要体现在以下几方面:
- 这些
loader
不能 通过this.emitFile
生成一个新文件webpack 文档传送门 # this.emitFile - 这些
loader
不能使用插件自定义的loader
方法,所谓插件自定义就是通过插件向loaderContext
扩展自定义的方法,loaderContext
是webpack
提供的一个loader
运行时的上下文对象; - 这些
loader
同样也无法获取webpack
打包的配置对象;
2.2 独立进程
thread-loader
虽然介绍时使用的是 worker pool
但是,它却不是真正的 worker
线程,而是实实在在的子进程(child_process
);
官方说大约会有 600ms
左右的开销,并且进程间天然隔离,不能共享数据,只推荐在那些耗时比较长的 loader
中使用;
三、thread-loader 入口文件
3.1 搞源码
去 github 上克隆下来就行:
$ git clone git@github.com:webpack-contrib/thread-loader.git
大致文件结构如下:
├── CHANGELOG.md
├── LICENSE
├── README.md
├── babel.config.js
├── commitlint.config.js
├── example
.....
│ └── webpack.config.js
├── husky.config.js
├── lint-staged.config.js
├── package-lock.json
├── package.json
├── src
│ ├── WorkerError.js
│ ├── WorkerPool.js
│ ├── cjs.js
│ ├── index.js
│ ├── readBuffer.js
│ ├── serializer.js
│ ├── worker.js
│ └── workerPools.js
└── test
├── ....
3.2 入口文件
从 package.json
的 main
字段得知,这个包的入口文件为 dist/cjs.js
但是你会发现,上面的目录中压根没有 dist
目录。。。WHAT
?
此时别着急,这种情况下都是作者在本地打包好,然后用本地文件发包,只不过 git
仓库是忽略掉 dist
目录。这就要求我们看看作者是用啥打包的,一般的不外乎:webpack、esbuild、rollup、babel、snowpack...
打开 package.json
看 scripts
脚本,一般打包命令都写在这里面:
{
"name": "thread-loader",
"main": "dist/cjs.js",
"engines": {
"node": ">= 10.13.0"
},
"scripts": {
"build": "cross-env NODE_ENV=production babel src -d dist --copy-files"
},
"files": [
"dist"
],
}
所以这个包使用 babel
打包的了,并且是打包一个目录(src
)输出到 dist
目录,并且吧 src
目录下的文件复制到 dist
(--copy-files
),这部分内容属于 babel cli
执行下面的依赖安装和打包命令:
$ npm ci
$ npm run build
这样 dist
目录生成了:
dist 目录下的文件结构:
.
├── WorkerError.js
├── WorkerPool.js
├── cjs.js
├── index.js
├── readBuffer.js
├── serializer.js
├── worker.js
└── workerPools.js
这看起来和 src 下的文件别无二致,为啥还要打包?
打包的原因是作者用 ESModule
开发的,通过 Babel
打包成 CommonJS
;但是我们阅读,就从打包的入口文件(thread-loader/src/index.js
)看起就好了,这里算是分享一个我早期阅读源码的一个疑惑点。
四、loader.noraml & loader.pitch
4.1 入口模块的代码结构
import loaderUtils from 'loader-utils';
import { getPool } from './workerPools';
function pitch() {
// ...
}
function warmup(options, requires) {
// ....
}
export { pitch, warmup }; // eslint-disable-line import/prefer-default-export
在这个入口文件中定义并且导出了两个方法:pitch
、warmup
,我们先忽略方法中的具体逻辑,先说说这个结构。
值得一提的是 pitch
方法,这个名字不是瞎叫的,而是在 webpack
的整个生命周期中有这深远意义的名字;
4.2 pitch 方法
这需要你了解 webpack
对 loader
的执行的全过程,我在前面的文章中写过一些关于 pitch 的内容。
大致回顾一下,loader
的运行分为两个过程:pitch
和 normal
阶段,所谓 normal
就是实现具体功能的 loader
函数本身了,而 pitch
则是挂载 loader
上的一个特殊方法。
normal
阶段是大家熟知的按照 loader
添加的顺序倒序执行,但是在此之前还有一个 pitching
阶段,这个阶段会按照 loader
的添加顺如逐个执行 loader
的 pitch
方法;
pitch
是越过的意思,上官方文档### 越过 loader(Pitching loader)。
也就是说你声明一个 loader
时,还可以声明一个 loader.pitch
,用于跳过其余的 loader
进入到剩余 loader
的 normal
阶段;
比如我们对一个模块添加了三个 loader: [a, b, c]
,这些 loader
的整个执行过程如下:
a.pitch
b.pitch
c.pitch
request modeule 被拾取成为依赖
c.normal
b.normal
a.normal
那么 pitch
如何发挥作用实现跳过呢?只要让 loader.pitch
方法返回一个非 undefined
的值就可以了。比如 b.pitch
返回了一段新的 request
;
module.exports = function loaderB (content) {
return someSyncOperation(content, this.data.value);
};
// loaderB.pitch
module.exports.pitch = function(remainingRequest, precedingRequest, data) {
return 'module.exports = require(' + JSON.stringify('-!' + remainingRequest) + ');';
};
此时这些 loader
顺序变成 c.pitch/c.normal/b.normal
都被跳过,如下:
a.pitch
b.pitch return 一个新的request
a.normal
4.3 pitch 对于 thread-loader 的意义
结合前面的介绍,thread-loader
会把放在他后面的 loader
放到 worker pool
中并发执行。那么如何截住它后面的 loader
们呢?
正如你所料,thread loader
就是在 pitch loader
中把剩余的 loader
扔到线程池中运行;
五、总结
本篇小作文开启了一个新坑,本文主要讨论了以下几个问题:
thread-loader
作用;babel
打包thread-loader
及打包的入口文件;- 复习
loader.pitch
&normal
以及两者的顺序; thread-loader
利用pitch
截取后面的loader
扔到线程池;
转载自:https://juejin.cn/post/7103507305672474632