Web

Node SEA

Posted by Kerwen Blog on September 10, 2024

官方文档

官方的文档直接机翻成汉语不像人话,尝试以自己理解的方式翻译了一下。

生成单执行程序

Node.js 支持创建单个可执行应用程序,方法是注入由 Node.js 准备的 blob,其中可以包含捆绑脚本, 将js文件转为二进制文件。在启动过程中,程序会检查是否有任何内容注入。如果找到 blob,它将执行 blob 中的脚本。否则 Node.js 像往常一样运行。

blob

关于什么是Blob,官方在另外一个地方link了一个链接

Blob 对象表示一个不可变、原始数据的类文件对象。它的数据可以按文本或二进制的格式进行读取,也可以转换成 ReadableStream 来用于数据操作。

Blob 表示的不一定是 JavaScript 原生格式的数据。File 接口基于 Blob,继承了 blob 的功能并将其扩展以支持用户系统上的文件。

以下示例中,用一个 JSON 字符串构造一个 blob:

1
2
3
4
const obj = { hello: "world" };
const blob = new Blob([JSON.stringify(obj, null, 2)], {
  type: "application/json",
});

示例

Node SEA目前仅支持运行 使用 CommonJS 模块系统的单个嵌入式脚本。
以下是Hello示例

  1. 创建 JavaScript 文件:

    1
    
     echo 'console.log(`Hello, ${process.argv[2]}!`);' > hello.js
    
  2. 创建一个配置文件,构建一个可以注入到 单个可执行应用程序的blob:

    1
    
     echo '{ "main": "hello.js", "output": "sea-prep.blob" }' > sea-config.json
    
  3. 生成要注入的 blob:

    1
    
     node --experimental-sea-config sea-config.json
    
  4. 创建可执行文件的副本并根据您的需要命名它:

    1
    
     node -e "require('fs').copyFileSync(process.execPath, 'hello.exe')"
    

    文件名后缀必须是.exe

  5. 使用 signtool 删除二进制文件的签名:

    1
    
     signtool remove /s hello.exe
    
  6. 通过使用 以下选项将blob注入到hello.exe中:

    1
    
     npx postject hello.exe NODE_SEA_BLOB sea-prep.blob --sentinel-fuse NODE_SEA_FUSE_fce680ab2cc467b6e072b8b5df1996b2
    
    • hello / hello.exe- 可执行文件副本的名称 在步骤 4 中创建
    • NODE_SEA_BLOB- Blob 的名字
    • sea-prep.blob- 在步骤 1 中创建的 blob 的名称。
    • –sentinel-fuse NODE_SEA_FUSE_fce680ab2cc467b6e072b8b5df1996b2- Node.js 项目用于检测文件是否已注入的保险丝(fuse)。
  7. 对二进制文件进行签名
  8. 运行二进制文件:

    1
    2
    
     $ .\hello.exe world  
     Hello, world! 
    

生成blob

blob 可以使用node 命令--experimental-sea-config 来生成。需要传入一个JSON 格式的配置文件。

下面是一个示例:

1
2
3
4
5
6
7
8
9
10
11
{
  "main": "/path/to/bundled/script.js",
  "output": "/path/to/write/the/generated/blob.blob",
  "disableExperimentalSEAWarning": true, // Default: false
  "useSnapshot": false,  // Default: false
  "useCodeCache": true, // Default: false
  "assets": {  // Optional
    "a.dat": "/path/to/a.dat",
    "b.txt": "/path/to/b.txt"
  }
}

如果路径不是绝对路径,Node.js将使用相对于 当前工作目录的路径。用于生成blob的 Node.js 版本必须与 blob 将注入的nodejs版本相同。

注意:由于CodeCache和Snapshot只能在同一平台上加载, 在生成跨平台 SEA 时必须将这两个选项设置为 false 以避免生成不兼容的可执行文件。当exe尝试加载在不同平台上构建的 Code Cache 或 Snapshot时, 它可能会在启动时崩溃。

Assets

可以在json配置文件的assets区域,用 key-path 添加assets。在构建时,Node.js会从指定路径读取assets,并将其捆绑到blob中。在生成的exe中,用户可以使用 sea.getAsset()sea.getAssetAsBlob() API 来获取assets

1
2
3
4
5
6
7
8
{
  "main": "/path/to/bundled/script.js",
  "output": "/path/to/write/the/generated/blob.blob",
  "assets": {
    "a.jpg": "/path/to/a.jpg",
    "b.txt": "/path/to/b.txt"
  }
}

exe可以按如下方式访问assets:

1
2
3
4
5
6
7
8
9
const { getAsset, getAssetAsBlob, getRawAsset } = require('node:sea');
// Returns a copy of the data in an ArrayBuffer.
const image = getAsset('a.jpg');
// Returns a string decoded from the asset as UTF8.
const text = getAsset('b.txt', 'utf8');
// Returns a Blob containing the asset.
const blob = getAssetAsBlob('a.jpg');
// Returns an ArrayBuffer containing the raw asset without copying.
const raw = getRawAsset('a.jpg');

snapshot支持

在配置文件里将useSnapshot设为true来启用快照支持。脚本不会在exe启动时执行, 相反,它是在构建计算机上, 在准备 blob 时生成的。生成的blob 将包括一个快照,捕获脚本初始化的状态。 注入了blob 的exe在运行时,会反序列化快照。

当为 true 时,主脚本调用 v8.startupSnapshot.setDeserializeMainFunction()API 来配置exe启动时需要运行的代码

exe使用快照的典型模式是:

  1. 在build时,在build机上,主脚本将堆(heap)初始化为准备接受用户输入的状态。脚本还会使用 v8.startupSnapshot.setDeserializeMainFunction() 配置 main 函数。此函数被编译并序列化到快照中,但不在build的时候调用。

  2. 在运行时,main 函数将在反序列化的堆上运行,在用户计算机上处理用户输入并生成输出。

启动快照脚本的一般约束也适用于主 script 来为单个可执行应用程序构建快照, 并且主脚本可以使用 v8.startupSnapshot API 来适配 这些约束。

V8 代码缓存支持

当设置此选项时,在生成期blob期间,Node.js将编译脚本以生成 V8 代码缓存。生成的代码缓存将是blob的一部分,并最终注入到可执行文件exe中。当应用程序启动时,Node.js不会从头开始编译script,而是使用缓存来加快编译速度,这将提高启动性能。

注意:当 useCodeCache为true时import() 不起作用。

API

  1. sea.isSea()
    返回: 此脚本是否在单可执行文件中运行。

  2. sea.getAsset(key[, encoding])
    用于检索在build时,捆绑到exe里的assets。 如果找不到匹配的资产时,将抛出error。

    • key <string> 在配置文件中指定的assets的key。
    • encoding <string> 如果指定,则将Assets解码为一个字符串。接受任何的支持编码。 如果不指定,则返回一个包含该资产的副本。

    返回: string 或者 ArrayBuffer

  3. sea.getAssetAsBlob(key[, options])
    类似于 sea.getAsset(),但返回 Blob 中的结果。 如果找不到匹配的资产时,将抛出error。

    • key <string> 在配置文件指定的assets的key。
    • options <Object> type <string> blob 的可选 MIME 类型。?

    返回: <Blob>

  4. sea.getRawAsset(key)
    此方法可用于检索build时,捆绑到exe的assets。 当找不到匹配的资产时,将抛出error。

    与 2或 3不同,此方法不会 返回副本。相反,它会返回捆绑在可执行文件中的原始资源。

    目前,用户应避免往返回的数组缓冲区做写入操作。如果 注入的部分未标记为可写或未正确对齐, 往返回的数组缓冲区写入可能会导致崩溃。

    key <string> 在配置文件指定的assets的key
    返回: <string> |<ArrayBuffer>

require(id)在注入的主脚本中不是基于文件的

注入的主脚本中的 require() 与未注入模块可用的 require() 不同。它也没有非注入的 require() 所具有的任何属性(require.main 除外)。它只能用于加载内置模块。尝试加载只能在文件系统中找到的模块将引发错误。

用户可以将应用程序编译到一个 JavaScript 文件中,并注入可执行文件,而不是依赖基于文件的 require()。这还可以确保更确定的依赖关系图。

但是,如果仍然需要基于文件,也可以实现:

1
2
const { createRequire } = require('node:module');
require = createRequire(__filename);

__filename和注入的主脚本module.filename

注入的主脚本中_filename 和 _module.filename 的值 等于 process.execPath。

__dirname在注入的主脚本中#

注入的主脚本中的 __dirname 等于目录 process.execPath

当前状态概述

原文地址An Overview of the Current State

Node.js 新特性 SEA/单文件可执行应用尝鲜
https://stackoverflow.com/questions/78209775/with-node-sea-how-to-pack-node-modules-into-executable
An Overview of the Current State
How to bundle Node.js application to Single Executable Application (SEA) along Native Modules (NAPI)?