pkg 库原理介绍
pkg 库可以将一个 Node.js 引用直接打包成单独的二进制文件,这使得 Node.js 代码可以在不安装 Node.js 的情况下执行。今天看了一下这个库的原理。
参数解析与获取
这部分就不过多介绍了,主要是处理一些参数,如果没传使用默认值等。
如果没有指定目标平台的话,pkg 默认会将 linux、macos、windows 三个平台的产物都编译出来。
1 | targets = parseTargets(['linux', 'macos', 'win']); |
之后会根据各个平台生成不同的产物路径。
1 | // "/xxx/index-linux" |
Node.js 二进制文件获取
pkg 库得到的产物,会将 Node.js 与应用代码整体打包在一起,所以他必须能获取到 Node.js 二进制产物。
Node.js 二进制产物是使用 pkg-fetch 库来拉取的:
1 | import {need} from 'pkg-fetch'; |
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 }); |
二进制文件拼接
Node.js 二进制产物有了,应用代码也有了,接下来就是把他们合并成一个文件了。producer
函数来进行最终产物的生成。
1 | await producer({ |
producer
函数会创建一个 stream,依次输出所有文件:
count 为 0 时输出 Node.js 二进制产物。
count 为 1 时输出分隔符。
count 为 2 时依次输出所有应用文件。同时,还会输出 pkg 中的一个启动文件 prelude/bootstrap.js
bootstrap.js
bootstrap.js 文件非常长,其中做的主要工作是修改 Node,js 的一些原生 API。
其中,最关键的是修改了 Module.runMain
方法:
1 | Module.runMain = function runMain() { |
我们知道在 Node.js 启动过程中,会执行 runMain
方法来执行用户代码,这里 pkg 修改了这个方法,改为加载应用入口代码。
虚拟文件系统
由于所有应用相关的 JavaScript 代码都打包进了二进制文件中,所以 pkg 需要构建一个虚拟的文件系统,来加载这些文件。
这个虚拟文件系统其实就是一个对象:
其中记录了路径对应的 JavaScript 代码在二进制文件中的位置。
pkg-fetch 库对 Node.js 源码的修改
那么现在唯一没有解答的问题就是这个 bootstrap.js 是如何被 Node.js 加载的。
这点是通过修改 Node.js 的代码来实现的。
pkg-fetch 库中:
新增了一个 pkg.js 文件,这个文件在 Node.js 启动时会被加载,而 pkg.js 文件会在二进制文件中寻找 bootstrap.js 生成的代码并执行,这样就修改了 Module.runMain()
。