工作笔记-将JS代码文件封装成单一可执行文件

398 阅读9分钟

概述

笔者曾经有一篇技术博文,讨论了在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,它的官方网站在这里:

github.com/vercel/pkg

特性和优势

在其中,有一些内容很好的总结了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二进制文件,如果没有找到,就到互联网上去下载这个文件。如果一切正常,这个文件将会被下载到缓存路径中,然后继续后续的操作。问题就在这里,在我们这里,由于一些网络的问题,这个下载很可能是失败的,但失败的处理也不是那么明确,所以程序就退出了,用户通常也不知道是什么原因。

解决的方法其实也不复杂,笔者在一个相关的网站上,找到了这些二进制文件的下载链接:

github.com/vercel/pkg-…

然后可以手动下载下来,放在缓存路径中。这里面也有一些问题。 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文件,创建可以直接无依赖的在操作系统中可以直接执行的可执行文件。探讨了相关的解决方案,所使用的工具和实际的配置和操作过程。