首页
/ Electron ASAR 归档完全指南:打包格式、虚拟文件系统机制与 Node API 兼容边界

Electron ASAR 归档完全指南:打包格式、虚拟文件系统机制与 Node API 兼容边界

2026-09-06 18:59:01作者:董斯意

ASAR(Atom Shell Archive)是 Electron 面向应用打包场景设计的一种简单的扩展归档格式:应用分发(distribution)之后,源码通常会被打进一个 .asar 文件里随 Electron 一起发布。本文基于 docs/tutorial/asar-archives.md 官方教程,结合本仓库中 lib/node/asar-fs-wrapper.ts 的落地实现与 spec/asar-spec.ts 的测试覆盖,系统讲解 ASAR 的打包动机、虚拟文件系统的读取方式、把归档当作普通文件的校验手段,以及 Node API 在 ASAR 上的全部能力边界与应对策略(如 --unpack)。读完你将能够理解 app.asar 内部的工作机制,并写出在 ASAR 环境下健壮、可移植的 Electron 代码。

ASAR 是什么:为什么应用源码要被「装进一个文件」

在创建了应用分发版本之后(详见 application-distribution.md,其中描述了将 app 目录改名为 app.asar 后放入 Electron resources 目录的标准布局),应用源码通常会被打包进 ASAR 归档。它是一个简单的扩展归档格式,专门为 Electron 应用设计,打包带来的收益主要有三点:

  1. 缓解 Windows 上的超长路径问题:将成千上万个深层嵌套的源码文件合并成一个单文件后,文件路径整体变短,规避了 Windows 路径长度限制(MAX_PATH)带来的安装与访问故障。
  2. 加快 require 的速度:单文件归档使模块查找与磁盘 IO 更集中,配合索引信息可以减少目录遍历开销。
  3. 隐蔽源码,防止随意翻阅:打包后的内容不再是裸奔的明文目录,可以阻碍粗略的源码检查(注意:这只是「conceal」,不是加密,源码仍可通过反汇编等手段还原)。

打包后的应用运行在一个虚拟文件系统上——归档内的文件并不真实存在于磁盘。绝大多数 API 在这种虚拟环境下可以正常工作,但仍有少量场景因为虚拟化的固有边界而需要显式处理,这正是下文展开的内容。

在虚拟文件系统中读写:Node API 与 Web API 两套通路

Electron 运行时内置两套文件 API:由 Node.js 提供的 Node API 与由 Chromium 提供的 Web API。二者都支持从 ASAR 归档读取文件。

Node API:把 ASAR 目录当普通目录用

借助 Electron 的特制补丁(special patches),fs.readFilerequire 等 Node API 会把 ASAR 归档视作虚拟目录、把其中的文件视为普通文件。假设在 /path/to 下存在 example.asar,其内容清单如下:

$ asar list /path/to/example.asar
/app.js
/file.txt
/dir/module.js
/static/index.html
/static/main.css
/static/jquery.min.js

读取归档中的文件:

const fs = require('node:fs')

fs.readFileSync('/path/to/example.asar/file.txt')

列出归档根目录下的所有文件:

const fs = require('node:fs')

fs.readdirSync('/path/to/example.asar')

从归档中加载模块:

require('./path/to/example.asar/dir/module.js')

归档中的目录还支持用 fs.opendir 进行遍历;对归档内存储的符号链接,可以用 fs.readlink 查看——它返回的是相对链接自身的链接目标,与真实文件系统的语义一致。用 BrowserWindow 加载 ASAR 归档中的页面同样可行:

const { BrowserWindow } = require('electron')

const win = new BrowserWindow()

win.loadURL('file:///path/to/example.asar/static/index.html')

Web API:通过 file: 协议请求归档内文件

在网页中,可以通过 file: 协议请求归档里的文件。与 Node API 一致,ASAR 归档在此处同样被当作目录处理。例如用 $.get 获取文件内容:

<script>
let $ = require('./jquery.min.js')
$.get('file:///path/to/example.asar/file.txt', (data) => {
  console.log(data)
})
</script>

实现层:补丁是怎么打上去的

从源码看,这套「透明虚拟化」发生在 Node 的 fs 模块上。lib/node/init.ts 在 Node 侧初始化时执行 wrapFsWithAsar(require('fs')),把整套补丁挂载到 fs;核心实现在 lib/node/asar-fs-wrapper.ts

  • splitPath 负责解析传入路径,判断其中是否含有 .asar 段(asarRe = /\.asar/i),并将其拆分为归档路径与归档内路径;
  • cachedArchives(一个 Map)按归档路径缓存 asar.Archive 对象,避免反复解析归档索引;
  • wrapFsWithAsar 逐一覆写 readFilestatlstatreaddiropendirrealpathopen 等函数的同步、回调与 promise 形态。

这意味着补丁是按路径段识别的:只有路径中出现 .asar 段才会进入归档分支(if (!pathInfo.isAsar) return old.apply(this, args)),普通路径仍是零开销直通原始实现。

把 ASAR 当作普通文件读取

某些场景需要读取 ASAR 归档文件本身的内容而非其中的某个条目——典型例子是校验归档的哈希/校验和。此时要绕过 fs 的 asar 支持,有两条途径。

途径一:使用内置的 original-fs 模块。 它提供不含 asar 支持的原始 fs API:

const originalFs = require('original-fs')

originalFs.readFileSync('/path/to/example.asar')

途径二:设置 process.noAsar = true,关闭 fs 模块对 asar 的支持:

const fs = require('node:fs')

process.noAsar = true
fs.readFileSync('/path/to/example.asar')

值得注意的是,original-fsfs 出自同一套源码,若共享绑定对象就会连带继承上述归档补丁。为避免这一点,lib/node/asar-fs-wrapper.ts(L293-L304)在首次覆写前就把一份纯净的 binding 副本(fsfs_dir)冻结保存到 _electronOriginalBindings,而 script/node/generate_original_fs.py 负责生成指向这些原始 binding 的 original-fs 模块。测试目录 spec/fixtures/module/original-fs.js 也验证了子进程中 require('original-fs') 可用。

补充:除进程内的 process.noAsar 开关外,仓库还提供了环境变量 ELECTRON_NO_ASAR,用于禁用 ASAR 支持。按 docs/api/environment-variables.md 的说明,它只在被 fork/spawn、且设置了 ELECTRON_RUN_AS_NODE 的子进程中生效。在 lib/node/asar-fs-wrapper.ts 中可以看到对应实现:process.env.ELECTRON_NO_ASAR && process.type !== 'browser' && process.type !== 'renderer',即不会影响浏览器主进程与渲染进程内的 asar 读取。

Node API 的局限:虚拟文件系统的固有边界

尽管 Electron 尽力让 ASAR 在 Node API 中表现得像真实目录,但由于 Node API 属于底层系统调用级抽象,仍存在以下无法消除的限制。理解这些边界,是编写可靠 Electron 代码的前提。

归档是只读的

归档不可被修改,因此所有会写入/改动文件的 Node API 都无法作用于 ASAR 归档。在 lib/node/asar-fs-wrapper.ts 中,常量 kWriteFlagsO_WRONLY | O_RDWR | O_APPEND | O_TRUNC)被显式拒绝:用任何允许写入的 flag(war+……)打开归档内文件都会抛出 EACCES

工作目录不能设为归档内目录

虽然 ASAR 归档被当作目录,但文件系统里并不存在这些真实目录,因此你永远无法把工作目录(cwd)切到 ASAR 归档内部;把归档内路径作为某些 API 的 cwd 选项传入同样会报错。规避方式是先用 original-fs 或打包时用 --unpack 把需要作为工作目录的资源释放到磁盘。

归档内文件的文件描述符(fd)语义

fs.openfs.openSyncfs.promises.open 对归档内文件返回的是真实的文件描述符(以及 FileHandle),但它们背后由归档文件本身支撑,而非磁盘上某个真实存在的条目。由此带来一系列精细语义:

  • 基于 fd 的 API——fs.readfs.readvfs.fstatfs.readFile(fd)fs.createReadStreamFileHandle#readFileFileHandle#createReadStream 等——会直接读出归档内容,不做任何临时拷贝
  • 同理,fs.copyFilefs.cp 及其同步与 promise 变体,会直接从归档复制到目标路径,也不产生中间临时文件。

由于归档只读,用带写权限的 flag 打开归档内文件会以 EACCES 失败;对这类 fd 调用 fs.fchmodfs.fchownfs.futimes 同样返回 EACCES。此外,该 fd 只是向 Node 的 fs 模块标识归档条目的令牌,并非由文件内容支撑的真实句柄,因此把 fd 交给 fs 之外的代码使用(例如直接读取裸 fd 的原生 addon、child_processstdionet.Socket({ fd })http2stream.respondWithFile())会以 EBADF 失败,且这种用法不受支持。

从源码实现看,这一设计是刻意的:lib/node/asar-fs-wrapper.ts 中的 AsarEntryReader 通过归档自身的句柄按偏移量读取,asar.createSentinelFd() 产生的哨兵 fd 只是模块内部用于标识 reader 的"写句柄",对它的直接裸读会以 EBADF 明确报错——绕过 fs 的代码拿不到归档字节,归档自身句柄也永远不会暴露给调用方。测试中 fs.readSync / fs.read on packed filesfs.readvfs.createReadStream on packed filesfchmod / fchown / futimes on packed file descriptors 等用例(见 spec/asar-spec.ts)对该语义做了系统验证。

部分 API 需要"额外解包"(Extra Unpacking)

对那些需要把真实文件路径交给底层系统调用的 API,Electron 会把所需文件抽取到临时文件,再把临时文件路径传给 API,从而让它们正常工作。代价是为这些 API 增加了一点额外开销。需要额外解包的 API 有:

  • child_process.execFile
  • child_process.execFileSync
  • process.dlopen——require 原生模块时使用

实现层可见 lib/node/asar-fs-wrapper.tsoverrideAPI / overrideAPISync:当路径命中 asar 时,先调用 archive.copyFileOut(filePath) 把文件释放到磁盘再继续原始调用。也就是说,每调用一次这类 API,都会产生一次"解包到临时文件"的 I/O 开销。

fs.stat 的伪造统计信息

fs.stat 及其同类 API 在 ASAR 归档内文件上返回的 Stats 对象是猜测生成的——因为这些文件并不存在于文件系统。因此除了获取文件大小与判断文件类型外,不应信任该 Stats 对象的其他字段。具体到实现:lib/node/asar-fs-wrapper.ts 中时间戳统一取进程启动时的 fakeTime,uid/gid 取自当前进程,inode 由一个递增计数器 nextInode 模拟,文件模式则按惯例组合(普通文件 0644、目录与可执行文件 0755、符号链接 0777),以保证用该 mode 复制出的条目仍然可用。

执行 ASAR 归档内的二进制

child_process.execchild_process.spawnchild_process.execFile 都能执行二进制,但只有 execFile 支持执行 ASAR 归档内的二进制。原因在于:execspawn 接收的是 command(命令字符串)而非 file,命令由 shell 解释执行——既没有可靠办法判断命令字符串里是否用到了 asar 中的文件,即便判断出来,也无法保证在命令中替换路径不产生副作用。而 execFile 直接接收可执行文件路径,路径改写是安全且可预测的,因此唯独它得到支持。

向 ASAR 归档添加未打包文件:--unpack

前面提到,某些 Node API 在被调用时会"解包"文件到文件系统。除性能开销外,这种运行期解包行为还可能触发各种杀毒软件的扫描告警。

作为应对方案,可以在打包时使用 --unpack 选项,让特定文件保持不打包。例如把原生 Node 模块的共享库留在包外:

$ asar pack app app.asar --unpack *.node

执行上述命令后,你会注意到 app.asar 旁边多出一个名为 app.asar.unpacked 的文件夹,其中存放未打包的文件,它必须与 app.asar 一起发布、一起分发

从源码看,这个约定被原生与 JS 两层共同遵循:lib/node/asar-fs-wrapper.tsgetUnpackedPath${asarPath}.unpacked 拼接未打包条目的磁盘路径,与归档内记录 info.unpacked 的条目一一对应,openAsarEntry 遇到 unpacked 条目时直接返回该磁盘路径。这意味着对调用方来说,app.asar.unpacked 内的文件与包内文件在使用上几乎无差别,只是物理位置在归档之外。

从源码与测试看更多实现细节

若想深入验证 ASAR 的种种行为,仓库提供了直接的阅读与实验入口:

  • 入口lib/node/init.ts 执行 wrapFsWithAsar(require('fs')) 挂载补丁;全部覆写逻辑集中在 lib/node/asar-fs-wrapper.ts(约 2500 行),覆盖了 readFilereaddirstat/lstatopenreadcopyFilecpopendirreadlinkrealpathModule._extensions['.node'](原生模块 require)等维度。
  • 完整性校验:当归档携带 integrity 信息时,lib/node/asar-fs-wrapper.tsAsarEntryReader 按块(block)校验 SHA-256 哈希后才对外提供字节,哈希不匹配会直接以 ASAR Integrity Violation 终止进程。这是 asar-integrity.md 所述 fuses 完整性保护机制在读取链路上的落地(仓库根目录也附有专项测试 spec/asar-integrity-spec.ts)。
  • 开发调试:设置 ELECTRON_LOG_ASAR_READS 环境变量后,每次从 ASAR 读取都会把读取偏移量与文件路径记录到系统 tmpdir(实现于 logASARAccess,输出为 <name>-access-log.txt),产物可用于 asar 打包工具的文件顺序优化(见 docs/api/environment-variables.md)。
  • 测试覆盖spec/asar-spec.tsasar protocol(页面加载、__dirname、Worker/SharedWorker 加载归档文件)到 node apifs.readFileSyncfs.readdirSyncfs.cpfs.opendirfs.readlink、fd 语义、流读取、对打包文件的写入拒绝等)均有详尽的 it 用例,是理解各 API 边界行为最权威的行为说明书。

小结与建议

综合文档与实现可以总结出几条实用的工程准则:

  1. 默认把 ASAR 当虚拟目录用fs.readFilerequirereaddir、Web 的 file: 请求都能直接命中归档内容,无需特殊处理;需要校验归档整体、做哈希比对时,改用内置 original-fsprocess.noAsar = true
  2. 记住只读边界:任何写操作、把 cwd 设为归档目录、以写 flag 打开归档条目都会失败;基于 fd 的读取只应通过 fs 家族 API 进行,不要把归档内文件的 fd 传给原生插件或 net.Socket/http2
  3. 只有 execFile 能跑归档内二进制exec/spawn 请先解包或改用 unpacked 路径。
  4. 不要信任 fs.stat 的伪造元数据,只使用文件大小与类型判断。
  5. --unpack 提前释放易触发解包的文件(如 *.node 原生模块库),并记得将生成的 app.asar.unpacked 与归档一并分发,兼顾性能与杀软兼容性。

若想继续延伸,可配套阅读 application-distribution.mdapp.asarresources 目录的标准布局)、asar-integrity.md(基于 fuses 的完整性校验)以及 docs/api/environment-variables.mdELECTRON_NO_ASARELECTRON_LOG_ASAR_READS 等运行期开关)。

登录后查看全文
热门项目推荐
相关项目推荐