构建单可执行文件的 Node.js 应用(无痛)

发布: (2025年12月31日 GMT+8 22:00)
7 min read
原文: Dev.to

I’m sorry, but I can’t retrieve the content from external links. If you paste the text you’d like translated here, I’ll be happy to translate it into Simplified Chinese while preserving the formatting and code blocks.

单可执行文件应用程序 (SEA)

Node.js 本来并不是设计成现在这种发布方式的。
阅读完本文后,你将拥有一个 可运行的命令行可执行文件 ——无需 node 安装、无需 npm install、也不再有“在我的机器上可以运行”。只有一个二进制文件。

而且不仅是 CLI,还可以是一个真正的 GUI 可执行文件,以同样的方式发布。

当你正确使用时,Node.js 是一个令人惊叹的 C++ 运行时。
然而,工具链的现状,加上无休止的 CommonJS ↔ ESM 兼容性混乱,常常让人头疼。

本着为 Node.js 带来更好工具的精神,受我见过的最美的打包器 —— Zig bundler —— 的启发,我决定深入利用 Node 最少被使用的特性:

快速开始

// build.js
import { release } from "sea-builder";

const production = {
  entry: "app/index.js",
  name: "myapp",
  platforms: ["linux-x64", "win-x64"],
  outDir: "./dist",
  assets: {
    "README.md": "./README.md",
  },
};

release(production)
  .then((res) => console.log(res))
  .catch((err) => console.log(err));

就这样——你会得到一个包含以下内容的可执行文件:

  • 你的应用程序
  • Node 运行时
  • 所有资源

优势

  • 启动快速(尤其是使用代码缓存时)
  • 像快照这样的底层工具
  • 可预测的原生模块
  • 用户体验与真实软件相匹配,而非开发环境

这就是 Go、Rust 和 Zig 的发布方式。Node 也可以做到——只是一直缺乏好的工具。这正是 sea‑builder 的意义所在。

注意: 需要 Node.js 20+。使用 node -v 检查你的版本。

安装

npm init -y && npm i sea-builder

项目结构

app/
  index.js
  README.md
cross-build.js

示例:简单 CLI

// app/index.js
const { getAsset, isSea } = require("node:sea");

console.log(`Hello, ${process.argv[2] || "World"}!`);
console.log(`Running from: ${process.execPath}`);
console.log(`Platform: ${process.platform}`);
console.log("Running as SEA!\n");

const readme = getAsset("README.md", "utf8");
console.log("readme:", readme);

// binary asset (returns ArrayBuffer)
const configBinary = getAsset("README.md");
console.log(
  "Binary size:",
  configBinary.byteLength,
  "bytes"
);
// cross-build.js
const { release } = require("sea-builder");

const production = {
  entry: "app/index.js",
  name: "myapp",
  platforms: ["linux-x64", "win-x64"],
  outDir: "./dist",
  assets: {
    "README.md": "./README.md",
  },
};

release(production)
  .then((res) => console.log(res))
  .catch((err) => console.log(err));

运行构建:

node cross-build.js

sea-builder 下载正确的 Node 二进制文件并对其进行补丁。很简单,对吧?

示例:窗口(GUI)应用 – tessera.js

tessera.js 是我构建的一个 Node.js 渲染器。
在文章 [How I Built a Graphics Renderer for Node.js] 中可以了解更多。
本指南使用预先准备好的仓库。

git clone https://github.com/sklyt/tessera-seaexample.git
cd tessera-seaexample
npm i
node ./tesseraexample/build.js

生成的可执行文件位于 ./dist/img.exe

为什么要先打包成 CommonJS?

ESM 是一切美好事物的敌人,包括 SEA。
我们先把所有内容打包成 CommonJS,然后把打好的包交给 sea-builder

// inside build.js (esbuild step)
await build({
  entryPoints: [entryJs],
  bundle: true,
  platform: "node",
  target: "node22",          // adjust to your minimum Node target
  format: "cjs",
  outfile: bundlePath,

  // Native binaries stay external
  external: ["*.node", "*.dll"],

  sourcemap: false,
  logLevel: "info",

  define: {
    require: "require",
  },
});

GUI 应用的 SEA 配置

const bundlerOptions = {
  entry: bundlePath,               // bundled script
  name: appName,
  platforms: targets,
  outDir: outDir,
  assets: {
    "win-x64": {
      "prebuilds/win32-x64/renderer.node": join(
        __dirname,
        "/prebuilds/win32-x64/renderer.node"
      ),
      "prebuilds/win32-x64/glfw3.dll": join(
        __dirname,
        "/prebuilds/win32-x64/glfw3.dll"
      ),
      "prebuilds/win32-x64/raylib.dll": join(
        __dirname,
        "/prebuilds/win32-x64/raylib.dll"
      ),
    },
    "linux-x64": {
      "prebuilds/linux-x64/renderer.node": join(
        __dirname,
        "/prebuilds/linux-x64/renderer.node"
      ),
    },
  },

  // Optional flags (code cache works only on the current platform;
  // use CI workflows for cross‑compile builds)
  useCodeCache: false,
  useSnapshot: false,
};

console.log("Calling sea() with bundled entry and platform assets...");
await sea(bundlerOptions);
console.log("sea() finished. Outputs are in", path.resolve(outDir));

从 SEA 加载资产

// img.js (helper)
const getter = (name) => {
  try {
    return getAsset(name); // string or ArrayBuffer
  } catch {
    return null;
  }
};

const { Renderer, FULLSCREEN, RESIZABLE } = loadRendererSea({
  assetGetterSync: getter,
});

就这样——你现在拥有一个可共享的 GUI 可执行文件。

未来说明: 在即将发布的 sea-builder 版本中,终端不会再随 GUI 可执行文件一起启动。如有需要,你可以直接修改二进制头部。但说实话,它已经可以正常工作了。

示例 sea-builder 配置

module.exports = {
  // Development build – fast iteration
  dev: {
    entry: "src/index.js",
    name: "myapp-dev",
    platforms: "current",
    outDir: "./build",
    useCodeCache: true,
  },

  // Production build – all platforms
  production: {
    entry: "src/index.js",
    name: "myapp",
    platforms: "all",
    outDir: "./dist",
    clean: true,
    assets: {
      "config.json": "./config/production.json",
      "README.md": "./README.md",
      LICENSE: "./LICENSE",
    },
  },

  // Server build – Linux only
  server: {
    entry: "src/server.js",
    name: "myapp-server",
    platforms: ["linux-x64", "linux-arm64"],
    outDir: "./dist/server",
    useCodeCache: true,
    assets: {
      "config.json": "./config/server.json",
    },
  },

  // CLI tool – small and portable
  cli: {
    entry: "src/cli.js",
    name: "myapp-cli",
    platforms: ["linux-x64", "win-x64", "macos-arm64"],
    outDir: "./dist/cli",
    useCodeCache: true,
    assets: {
      "help.txt": "./assets/help.txt",
    },
  },
};

回顾

  • SEA 让你可以打包一个包含你的 Node 应用、运行时以及所有资源的单一二进制文件。
  • sea-builder 自动下载正确的 Node 二进制文件,进行补丁处理,并打包你的代码(最好是 CommonJS)。
  • 结果就像原生的 Go/Rust/Zig 二进制文件——启动快速,原生模块可预测,用户体验简洁。

试一试吧,体验 Node.js 的“一次编写,随处运行”简洁之美!

构建配置

{
  // …
  cli: {
    entry: './src/cli.js',
    platforms: 'all',
    outDir: './dist/cli'
  }
}

注意: useCodeCache: true 仅在当前平台上有效,因为它是平台特定的。使用 CI 为每个目标平台构建缓存的二进制文件。

Source:

构建脚本 (build.js)

// build.js
const { sea } = require('sea-builder');
const config = require('./sea.config');

// node build.js production
const profile = process.argv[2] || 'dev';

if (!config[profile]) {
  console.error(`Unknown profile: ${profile}`);
  console.log('Available profiles:', Object.keys(config).join(', '));
  process.exit(1);
}

console.log(`Building with profile: ${profile}`);

sea(config[profile]).then(results => {
  console.log(`\nBuilt ${results.length} executable(s)`);
});

更多内容

  • 在 Node.js 中可视化进化算法
  • tessera.js 仓库

感谢阅读!

在这里找到我

Back to Blog

相关文章

阅读更多 »