Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

将 Node.js 项目打包成单应用(Single executable applications)的尝试 #132

Open
lmk123 opened this issue Jun 19, 2024 · 0 comments

Comments

@lmk123
Copy link
Owner

lmk123 commented Jun 19, 2024

最近准备把一个 Node.js 项目打包成一个单一的可执行文件,因为对于没有编程经验的人来说,纯 Node.js 项目很难部署成功,我也顺便学习一下打包单应用的方法。

先说结论:最终我决定改为使用 Electron。现在,我来分享一下打包单应用的过程以及最后为什么做这个决定。

选择工具

Node.js 官方已经实验性的支持了打包单应用的功能:https://nodejs.org/api/single-executable-applications.html

但是考虑到这个功能还是实验性的,所以我还是准备先用第三方工具来尝试。

打包 Node.js 应用的工具在以前有两个:pkgnexe。其中 pkg 已经不再维护了,于是我准备试一下 nexe。尝试之前,我看到有人在 issues 里抱怨,说最简单的 hello world 都跑不起来,不过我还是试了一下,结果真的失败了。

最后我还是选择了 Node.js 官方的工具(后文称之为 SEA),至少 hello world 是能成功打包的。

第一个坑:可执行文件的大小

console.log('hello world') 打包成可执行文件后,生成的文件有 88.4 MB,这让我一下子想到了一个有类似情况的项目————Electron。

SEA 的原理其实是“注入”,即在 Node.js 的执行文件里注入要默认执行的代码,这样就不需要用户安装 Node.js 了,但这也意味着打包出来的可执行文件会包含一个完整的 Node.js 运行时,也就是体积至少有 88 MB。

算了,比 Electron 的 120 MB 强一点。

第二个坑:模块格式

由于文档上写着不支持 ESM,我开发的时候使用了 CommonJS 的模块格式,但是在开发阶段就报错了,原因是依赖的 node_modules 里有 ESM 的模块。

我选择先将这个 ESM 模块的版本降级到它支持 CommonJS 的那个版本,然后再打包,但到了运行的时候又报错了———因为在 SEA 的环境里面,不能引用 node_modules 里的模块,只能引用打包进去的模块。

这意味着我无论如何都逃不开 Webpack 这个工具,我需要将我的代码以及依赖的模块转变成 CommonJS 然后全都打包进一个文件,最后再把这个文件用 SEA 打包成一个可执行文件。

第三个坑:静态资源

我的设想是使用网页来填写配置项,所以我有一个 index.html 文件需要打包进去,SEA 是支持把静态资源打包进去的,类似于这样:

import express from 'express'
import { getAsset, isSea } from 'node:sea'

const app = express()
app.get('/', (req, res) => {
  // 在 SEA 运行环境则使用 getAsset
  if (isSea()) {
    const htmlString = getAsset('index.html', 'utf-8')
    res.set('Content-Type', 'text/html')
    res.send(htmlString)
  } else {
    // 开发环境则读取本地文件
    res.sendFile(path.join(__dirname, 'index.html'))
  }
})

但这里有个问题:我自己写的时候确实可以考虑到 SEA 的情况,但是别的模块就不一定了,比如有的模块会从 node_modules/external-module/ 文件夹下读取一些配置文件。

不过,不同于模块,静态资源不是必须要打包进去的。我们可以把静态资源跟可执行文件放在同一个目录下,然后在运行时用 fs 模块读取,大概是这样:

./
├── node_modules/external-module/
│   ├── config.xml
│   └── ...
└── sea-darwin-arm64

然后再次尝试了一下,还是不行。检查了一下,发现是 prisma 这个模块造成的。

@prisma/client 会在 postinstall 阶段下载一个二进制文件保存在 node_modules/.prisma 文件夹,里面有操作数据库需要的二进制文件。在经过一番调查后,我发现 prisma 可能还会在别的阶段下载二进制文件,见 Engines | Prisma Documentation

这意味着我还需要额外将这些二进制文件包含进去,那么目录大概会是这样:

./
├── node_modules/
├──── external-module/
│      ├── config.xml
│      └── ...
├──── .prisma/
│      └── client/
│          ├── libmerge_engine-darwin-arm64.dylib.node
│          └── libquery_engine-darwin-arm64.dylib.node
└── sea-darwin-arm64

想到这里,我决定改用 Electron 了。

为什么改用 Electron?

我认为的单应用的优势:

  • 文件数量只有一个,更方便分发
  • 比 Electron 体积小

但是这个项目走到了这一步,怎么看都更适合用 Electron,因为:

  • 由于需要额外的二进制文件,打包出来的已经不再是单一文件了,且 Node.js 加上额外的二进制文件后,体积也不小了
  • 比起仍处于实验性阶段的 SEA,Electron 更加成熟稳定

所以最终我决定改用 Electron。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

1 participant