pkg 库原理介绍

pkg 库可以将一个 Node.js 引用直接打包成单独的二进制文件,这使得 Node.js 代码可以在不安装 Node.js 的情况下执行。今天看了一下这个库的原理。

参数解析与获取

这部分就不过多介绍了,主要是处理一些参数,如果没传使用默认值等。

如果没有指定目标平台的话,pkg 默认会将 linux、macos、windows 三个平台的产物都编译出来。

1
targets = parseTargets(['linux', 'macos', 'win']);

之后会根据各个平台生成不同的产物路径。

1
2
// "/xxx/index-linux"
file = stringifyTargetForOutput(output, target, different);

Node.js 二进制文件获取

pkg 库得到的产物,会将 Node.js 与应用代码整体打包在一起,所以他必须能获取到 Node.js 二进制产物。

Node.js 二进制产物是使用 pkg-fetch 库来拉取的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import {need} from 'pkg-fetch';
async function needWithDryRun({
forceBuild,
nodeRange,
platform,
arch,
}: NodeTarget) {
const result = await need({
dryRun: true,
forceBuild,
nodeRange,
platform,
arch,
});
assert(['exists', 'fetched', 'built'].indexOf(result) >= 0);
dryRunResults[result] = true;
}

pkg-fetch 这个库有很多作用:

  • 提供获取 Node.js 二进制产物的功能。
  • 维护了 Node.js 的构建流程,在 github 中会构建出不同版本的 Node.js 产物,可以直接拉取,也可以使用 Node.js 代码进行编译。
  • 对 Node.js 源码进行了一些修改,以 .patch 文件的形式保存,编译时先应用这些修改,再进行编译。

遍历应用代码,获取所有 js 文件

之后 pkg 会尝试遍历代码,找到所有的文件。除了遍历代码,也会将配置项中的文件都加入进来。

1
const walkResult = await walk(marker, entrypoint, addition, params);

所有的文件会通过一个 packer 方法合并成一个对象,其中 JavaScript 内容会通过 Buffer.from() 方法转换成字节码。

1
const backpack = packer({ records, entrypoint, bytecode, symLinks });

backpack

二进制文件拼接

Node.js 二进制产物有了,应用代码也有了,接下来就是把他们合并成一个文件了。producer 函数来进行最终产物的生成。

1
2
3
4
5
6
7
8
await producer({
backpack,
bakes,
slash: target.platform === 'win' ? '\\' : '/',
target: target as Target,
symLinks,
doCompress,
});

producer 函数会创建一个 stream,依次输出所有文件:

producer-stream

count 为 0 时输出 Node.js 二进制产物。

count 为 1 时输出分隔符。

count 为 2 时依次输出所有应用文件。同时,还会输出 pkg 中的一个启动文件 prelude/bootstrap.js

bootstrap.js

bootstrap.js 文件非常长,其中做的主要工作是修改 Node,js 的一些原生 API。

其中,最关键的是修改了 Module.runMain 方法:

1
2
3
4
Module.runMain = function runMain() {
Module._load(ENTRYPOINT, null, true);
process._tickCallback();
};

我们知道在 Node.js 启动过程中,会执行 runMain 方法来执行用户代码,这里 pkg 修改了这个方法,改为加载应用入口代码。

虚拟文件系统

由于所有应用相关的 JavaScript 代码都打包进了二进制文件中,所以 pkg 需要构建一个虚拟的文件系统,来加载这些文件。

producer-stream

这个虚拟文件系统其实就是一个对象:

producer-stream

其中记录了路径对应的 JavaScript 代码在二进制文件中的位置。

pkg-fetch 库对 Node.js 源码的修改

那么现在唯一没有解答的问题就是这个 bootstrap.js 是如何被 Node.js 加载的。

这点是通过修改 Node.js 的代码来实现的。

pkg-fetch 库中:

producer-stream

新增了一个 pkg.js 文件,这个文件在 Node.js 启动时会被加载,而 pkg.js 文件会在二进制文件中寻找 bootstrap.js 生成的代码并执行,这样就修改了 Module.runMain()