概述
笔者曾经有一篇技术博文,讨论了在PHP应用中,集成JS代码程序(《PHP应用集成JS代码进行的函数计算》)。当时已经提到,JS代码文件是可以直接使用Nodejs程序作为脚本程序来执行的,所以在PHP应用中,就可以使用命令行的方式(shell_exec),调用node程序来执行js代码。
这一使用方式虽然比较方便,但也有一些问题:
- 需要预先安装和部署一个nodejs环境
- 配置信息和代码是直接暴露在js原代码当中的,有一些安全隐患
针对这两个问题,笔者其实已经了解到,是有方法可以将一个Nodejs程序和项目,封装成为一个单一的可执行的(很多go程序就是这样来处理的)。这样显然大大的方便了移植和部署。
在本文中,我们就来讨论如何进行实现和操作。
SEA
这种单一的nodejs可执行应用程序,是有一个专门的技术名词的,就是单一可执行应用(SEA, Single Excuteable Application)。在比较新的Nodejs 22版本中,已经实验性的引入了这个特性。但遗憾的是,笔者经过研究,发现这个实现并不是很好。不知道是什么原因,整个设置和编译的过程,非常麻烦,过程也很慢,看来离成熟还有很长的一段时间。其实笔者倒是觉得SEA可能是一个很好的发展方向,nodejs社区应该很重视这个问题,也不一定要完全自己实现,可以考虑集成一个现有的解决方案,然后改进后作为官方特性提供。
所以在本例中,笔者并没有选择这个官方的技术方案,而是使用了以前使用过的一个技术方案: pkg。
PKG
PKG是一个独立的Npm,它的官方网站在这里:
特性和优势
在其中,有一些内容很好的总结了SEA的应用场景和优势:
- 在不提供源码的情况下,交付应用程序的商业版本
- 在不提供源码的情况下,提供应用程序的演示/评估/试用版本
- 交叉编译,可以实时即时为其他平台制作可执行文件
- 有助于制作自解压缩档案或安装程序
- 无需安装 Node.js 和 npm 来运行打包的应用程序
- 无需通过 npm install 下载数百个文件来部署你的应用程序
- 单一可执行文件更加方便移植和部署
- 在不安装新版本Node.js的情况下,测试你的应用程序与新版本Node.js的兼容性
笔者使用的感觉还是不错的,但同样不知道是什么原因,这个项目已经停止发展很久了。它的最新版本是5.8.1(2023-5-8),而实际上主要的程序代码停留在3年前,而且它支持的最新的nodejs版本,只到V18。
安装和环境
作为一个外部工具npm,我们一般使用全局的安全方式:
npm install -g pkg
pkg安装完成之后,我们就获得了一个pkg的命令行工具,使用它我们可以来进一步处理我们的JS代码文件或者nodejs项目。
笔者的操作环境是AlmaLinux,内核是4.18。工作用的Nodejs版本是22.12。这里需要理解,这个工作用的nodejs(pkg和npm程序运行的环境),和将来编译使用的nodejs,可以是不同的版本,甚至不同的架构。
[yanjh@localhost ulcode]$ cat /etc/redhat-release
AlmaLinux release 8.10 (Cerulean Leopard)
[yanjh@localhost ulcode]$ uname -a
Linux localhost.localdomain 4.18.0-553.27.1.el8_10.x86_64 #1 SMP Tue Nov 5 04:50:16 EST 2024 x86_64 x86_64 x86_64 GNU/Linux
[yanjh@localhost ulcode]$ node -v
v22.12.0
编译
pkg的使用是非常简单的。如果没有外部依赖,它甚至不需要package.json的支持,就是其实是无需一个完整的nodejs项目的,这就带来了极大的灵活性,可以在任何地方都启动这个编译过程,比如下面这个例子:
[yanjh@localhost ulcode]$ pkg ulcode.js --targets node18-linux-x64
> pkg@5.8.1
(node:50536) [DEP0040] DeprecationWarning: The `punycode` module is deprecated. Please use a userland alternative instead.
(Use `node --trace-deprecation ...` to show where the warning was created)
[yanjh@localhost ulcode]$ ls -l
total 45228
-rwxrwxr-x. 1 yanjh yanjh 46293849 Dec 15 21:52 ulcode
-rw-rw-r--. 1 yanjh yanjh 14242 Dec 11 04:23 ulcode.js
[yanjh@localhost ulcode]$
这个例子中,只有一个独立的ulcode.js代码文件,就可以直接使用pkg进行编译。编译后会在当前文件夹中,创建一个可执行文件。
笔者的理解,这个编译的过程,其实就是一个打包的过程。它会将js代码文件,和nodejs二进制执行文件打包在一个更大的可执行文件中。编译的结果保证了其内部的nodejs可以执行其入口指定的js文件,就像使用nodejs来执行js脚本文件的方式一样。
所以,我们可以看到,虽然原始代码非常小,但编译后的结果文件,其基础大小都有46M,它应该就是包括了nodejs的主体二进制文件后的结果。这样,我们就获得了一个可以直接执行的“js”文件。作为一个普通的单一可执行文件,这个尺寸其实已经不小了,但好在现在的计算机系统容量和处理性能都足够,还是可以接受的。这里多提一句,使用nodejs内置的SEA的功能编译,得到的可执行文件是100多兆,不知道为什么(不会是跨平台可执行程序吧?)。
选项
上一章节当然只是pkg最简单的使用方式,但实际上,它还是支持很多编译过程中的选项的。常用的包括:
- targets: 就是指定编译目标
从它的表述上,它是可以进行所谓的“交叉编译”的。例如,在linux系统上,也可以创建能够在windows系统上执行的代码文件。但笔者实际使用下来感觉它好像有一些限制,并不能完全可靠的在一个地方就创建能够在所有平台上运行的代码。所以,最好的方式,还是最好在目标平台上来创建在其系统中运行的程序。
- 目标平台
从示例中,可以看到,所谓的目标平台,其实是由三个部分构成的。nodejs的版本,现在好像只有16和18可以选择;linux或者windows,不知道是否有其他选项;CPU架构,最常用的x64。笔者也没有其他的环境,只编译过linux/windows-x64的版本,是可以支持的。
由于这个项目已经停止发展很久了,所以最新的nodejs版本,就是18,一般情况下也是够用了。
- h help
显示帮助信息,如一些选项的名称和作用。
- c config
也可以指定package.json文件,来支持Nodejs项目的编译。
- o output
指定输出位置,一般作为自动化编译使用,输出到指定的位置,支持自动化部署。
- d debug
编译时输出调试信息。
-C compress
编译压缩。笔者的经验,如果没有很多js文件,几乎没有什么效果。基础文件还是40多兆。
还有一些选项,在实际应用中应当使用的机会比较少,读者可以自行研究了解。
问题和解决方式
和其他的解决方案相比,其实pkg的使用相对是比较简单的,在大部分情况下实操的过程也比较顺利,所以虽然不是特别理想,笔者现在还是主要使用这个方式。
当然问题还是有的,最明显的问题就是它自动下载Nodejs二进制文件的过程。这个程序的工作流程设定是,编译的时候,需要先根据命令行参数,在一个缓存路径中,查找所需要的nodejs二进制文件,如果没有找到,就到互联网上去下载这个文件。如果一切正常,这个文件将会被下载到缓存路径中,然后继续后续的操作。问题就在这里,在我们这里,由于一些网络的问题,这个下载很可能是失败的,但失败的处理也不是那么明确,所以程序就退出了,用户通常也不知道是什么原因。
解决的方法其实也不复杂,笔者在一个相关的网站上,找到了这些二进制文件的下载链接:
然后可以手动下载下来,放在缓存路径中。这里面也有一些问题。 pkg的默认缓存路径,是保存在环境变量 PKG_CACHE_PATH 中的,如果没有,则使用默认的路径 ~/.pkg-cache。为了支持多个pkg的版本,它还使用了版本号作为名字空间。笔者后来想办法在线安装了nodejs二进制文件,最后的路径结构如下:
[yanjh@localhost ulcode]$ pkg -v
5.8.1
(node:50679) [DEP0040] DeprecationWarning: The `punycode` module is deprecated. Please use a userland alternative instead.
(Use `node --trace-deprecation ...` to show where the warning was created)
[yanjh@localhost ulcode]$ tree ~/.pkg-cache
/home/yanjh/.pkg-cache
├── fetched-v18.15.0-linux-x64
├── node
├── v3.0.0
├── v3.4
│ └── fetched-v18.5.0-linux-x64
└── v5.8
├── fetched-v18.15.0-linux-x64
└── node-v18.15.0-win-x64
4 directories, 4 files
看到了吧? 当前pkg的名义版本是5.8.1,但实际上,如果使用v5.8这个路径,是不能工作的。实际有效的路径是v3.4,而且,二进制文件的名称也修改了,完整正确的文件名称是 fetched-v18.5.0-linux-x64(使用18版本的时候),这个如果出错,会在错误提示中显示。所以除非是已经成功过,或者非常清楚的了解这些问题,才有可能正确的进行手动的设置。
稍微总结一下,这里会遇到的问题包括:
- 无法自动下载nodejs和继续编译
- 需要手动下载对应平台的nodejs二进制文件
- 默认缓存路径是 ~/.pkg-cache
- 使用版本路径v3.4
- 正确的文件名称为fetched-v18.5.0-linux-x64(名称修改了,而且具体版本不同)
交叉编译
笔者尝试了在linux系统中,进行了Window版本的交叉编译,也是可以的。上次记得好像在Windows下,是不能交叉编译Linux版本的。
[yanjh@localhost ulcode]$ pkg ulcode.js -C --targets node18-win-x64
> pkg@5.8.1
(node:50694) [DEP0040] DeprecationWarning: The `punycode` module is deprecated. Please use a userland alternative instead.
(Use `node --trace-deprecation ...` to show where the warning was created)
> Fetching base Node.js binaries to PKG_CACHE_PATH
fetched-v18.5.0-win-x64 [====================] 100%
[yanjh@localhost ulcode]$ ls -l
total 82012
-rwxrwxr-x. 1 yanjh yanjh 46293849 Dec 15 22:12 ulcode
-rw-rw-r--. 1 yanjh yanjh 37663330 Dec 16 00:39 ulcode.exe
-rw-rw-r--. 1 yanjh yanjh 14242 Dec 11 04:23 ulcode.js
小结
本文源自单一js文件执行的需求,进一步扩展到基于js文件,创建可以直接无依赖的在操作系统中可以直接执行的可执行文件。探讨了相关的解决方案,所使用的工具和实际的配置和操作过程。