Nodejs 在 v20.0.0 开始支持 Single executable applications (简称:sea),但流程较为复杂,这篇文章将介绍如何使用它,并提供一个脚本方便使用。

下面核心代码来自官网,不做过多介绍,这里主要对目录做了调整

打包代码

# 生成的可执行文件名
EXEC_NAME="hello"
# 入口文件
ENTRY="index.js"
# 临时文件目录
SEA_PATH=".sea"

mkdir -p "$SEA_PATH"

# 创建配置文件
echo "{ \"main\": \"$ENTRY\", \"output\": \"$SEA_PATH/sea-prep.blob\" }" > "$SEA_PATH/sea-config.json"

# Generate the blob that contains your application script
node --experimental-sea-config "$SEA_PATH/sea-config.json"

# Copy the Node.js binary and name it as needed
cp $(command -v node) "$SEA_PATH/$EXEC_NAME"

# 注意:下面的代码每个平台都有一点区别,由于笔者使用 macOS,所以只测试过 macOS 下的效果
# 如果是mac,使用下面代码
codesign --remove-signature "$SEA_PATH/$EXEC_NAME"
npx postject "$SEA_PATH/$EXEC_NAME" NODE_SEA_BLOB "$SEA_PATH/sea-prep.blob" \
    --sentinel-fuse NODE_SEA_FUSE_fce680ab2cc467b6e072b8b5df1996b2 \
    --macho-segment-name NODE_SEA
codesign --sign - "$SEA_PATH/$EXEC_NAME"

# 其它平台参考:https://nodejs.org/api/single-executable-applications.html

echo "生成的可执行文件在:$SEA_PATH/$EXEC_NAME"

存在问题

如果在使用过程中遇到 SyntaxError: Cannot use import statement outside a module,是因为当前只支持 commonjs ,可以使用 babel 将其转换成 commonjs

set -e
# 先安装依赖
# npm install -D @babel/preset-env @babel/cli

# 这里为了方便,直接使用命令行,没有生成 .babelrc 文件
SRC_DIR="src"
BABEL_OUT_DIR=".babel"

mkdir -p "$BABEL_OUT_DIR"
npx babel "$SRC_DIR" --out-dir "$BABEL_OUT_DIR" --copy-files --presets=@babel/preset-env

如果只是单个js文件,上面就已经可以了;但如果有依赖,就需要使用 webpack 等打包工具合并成一个js文件了,这里为了方便使用@vercel/ncc

set -e
NCC_OUT_DIR=".ncc"

mkdir -p "$NCC_OUT_DIR"

# 使用ncc打包成一个文件
npx ncc build index.js -o "$NCC_OUT_DIR"

最终代码

set -e

# 先安装依赖
# 需要转换成 commonjs
# npm install --save-dev @babel/preset-env @babel/cli
# 需要打包
# npm install --save-dev @vercel/ncc
# 这个必需安装
# npm install --save-dev postject

# 入口文件
ENTRY="src/index.js"
# 源文件目录,将转换成 commonjs
SRC_DIR="src"
BABEL_OUT_DIR=".babel"
# ncc 打包目录
NCC_OUT_DIR=".ncc"

# 生成的可执行文件名
EXEC_NAME="hello"
# 入口文件,因为使用了 ncc,所以入口文件需要调整路径
ENTRY="$NCC_OUT_DIR/$(basename "$ENTRY")"
# 临时文件目录
SEA_PATH=".sea"

# 开始:
mkdir -p "$BABEL_OUT_DIR"
mkdir -p "$NCC_OUT_DIR"
mkdir -p "$SEA_PATH"

# 1. 转换成 commonjs
# 这里为了方便,直接使用命令行,没有生成 .babelrc 文件
npx babel "$SRC_DIR" --out-dir "$BABEL_OUT_DIR" --copy-files --presets=@babel/preset-env

# 2. 打包

# 生成一个 package.json,相当于把 babel 转换后的目录当做项目目录
echo "{\"type\": \"commonjs\"}" > "$BABEL_OUT_DIR/package.json"

ORIGINAL_PWD=$PWD
# 切换到 babel 转换后的目录
cd "$BABEL_OUT_DIR"
# 入口文件切换到 babel 转换后的目录,因为cd到了babel目录,所以只需要文件名即可
ENTRY="$(basename "$ENTRY")"

npx ncc build "$ENTRY" -o "$ORIGINAL_PWD/$NCC_OUT_DIR"

# 切回原目录
cd -

# 3. 生成可执行文件
ENTRY="$NCC_OUT_DIR/$(basename "$ENTRY")"
# 创建配置文件
echo "{ \"main\": \"$ENTRY\", \"output\": \"$SEA_PATH/sea-prep.blob\" }" > "$SEA_PATH/sea-config.json"

# Generate the blob that contains your application script
node --experimental-sea-config "$SEA_PATH/sea-config.json"

# Copy the Node.js binary and name it as needed
cp $(command -v node) "$SEA_PATH/$EXEC_NAME"

# 注意:下面的代码每个平台都有一点区别,由于笔者使用 macOS,所以只测试过 macOS 下的效果
# 如果是mac,使用下面代码
codesign --remove-signature "$SEA_PATH/$EXEC_NAME"
npx postject "$SEA_PATH/$EXEC_NAME" NODE_SEA_BLOB "$SEA_PATH/sea-prep.blob" \
    --sentinel-fuse NODE_SEA_FUSE_fce680ab2cc467b6e072b8b5df1996b2 \
    --macho-segment-name NODE_SEA
codesign --sign - "$SEA_PATH/$EXEC_NAME"

# 其它平台参考:https://nodejs.org/api/single-executable-applications.html

echo "生成的可执行文件在:$SEA_PATH/$EXEC_NAME"
# mv $SEA_PATH/$EXEC_NAME . # 先把可执行文件移出来
# 再清除临时目录
# rm -rf "$BABEL_OUT_DIR"
# rm -rf "$NCC_OUT_DIR"
# rm -rf "$SEA_PATH"

示例地址:https://github.com/podul-examples/nodejs-sea-examples

需要依赖:

  1. @babel/cli 可选
  2. @babel/preset-env 可选
  3. @vercel/ncc 可选
  4. postject 必需

存在问题:

  1. 目前整个打包流程较为复杂
  2. 不支持交叉打包(也许可以通过复制其它平台的node二进制文件实现)
  3. 还在实验中,后面可能移除,也可能调整,有待观察。
  4. 打包出来的文件较大

总结

目前存在问题较多,可以使用,但不太建议。可以使用 https://github.com/vercel/pkg 等工具先替代。

参考

  1. Single executable applications
  2. https://github.com/vercel/ncc
  3. https://github.com/nodejs/postject
  4. https://github.com/vercel/pkg