
上周把一个 Electron 项目交付给运维后对方问了个很直接的问题为什么安装包里散着几十个文件夹看着就像没打包干净。我没法正面回答因为问题就出在打包策略上。后来我花了一个晚上把整个应用里的 JS、HTML、图片、静态资源全部塞进了一个叫 asar 的归档文件里安装包立刻从一堆零散目录变成了单个文件体积和启动速度都有立竿见影的变化。asar 是 Electron 生态里绕不开的一个概念既是命令名也是一种归档格式。很多人听说过它但真正用过asar pack命令的并不多更多人是在报错里第一次遇见它比如 “Cannot find module” 或者 “EROFS”然后才开始去了解这玩意儿。这篇文章不打算讲空泛的原理重点是我在实际项目里用 asar 打包、解包、排查问题的完整过程同时把踩过的坑、试出来的技巧一并写出来。无论你是刚接触 Electron 的开发者还是已经在做桌面应用分发的老手看完应该都能直接上手。1. 聊清楚 Asar 的本质为什么 Electron 需要这么个格式1.1 从“一堆文件”到“一个文件”的工程演化Electron 应用本质上是把一个 Node.js 后端和 Chromium 前端塞进同一个壳里项目目录天然就比普通 Web 应用复杂main.js、渲染进程代码、node_modules、各种资源文件、图标、语言包加起来少则几百个文件多则上万。早期 Electron 应用分发时就是直接把这个目录原样发出去。结果很现实Windows 上复制几万个小文件慢到让人怀疑人生U 盘拷贝能烤糊文件被误删一个应用启动就直接崩杀毒软件扫描这种目录型应用CPU 直接拉满。所以 Electron 团队借鉴了 Chromium 处理扩展包的思路做了一个类似“目录封包”的归档格式这就是 asar。它的核心目标不是加密也不是压缩而是把“一堆文件”合并成“一个文件”让分发、拷贝、读取都变得轻快。1.2 Asar 的内部结构其实不复杂我第一次用asar list查看包内容时以为会看到类似 zip 的压缩结构但实际看完结构后发现它比想象中简单得多本质上是一个“带索引的文件拼接器”。asar 文件的身体分为两部分前半部分是 JSON 格式的 header记录了整个目录树、每个文件的大小、在归档内的偏移位置后半部分就是原封不动的文件内容一个接一个顺序排列。读取某个文件时Electron 的操作系统封装层先查 header 拿到偏移量和长度然后直接定位到对应位置做读取不需要像 zip 那样先解压整个包。因为不压缩所以 asar 内的文件体积和原始文件差不多。这带来的好处是打包快、读取快坏处是如果你想靠 asar 缩小安装包体积那是走错了方向。真正想缩体积得靠压缩静态资源、移除无用依赖而不是指望 asar。1.3 哪些文件该进包、哪些不该进包这是我在实际项目中花时间最多的地方。很多人习惯把整个应用目录一股脑塞进 asar结果运行时报错一个接一个。适合放进 asar 的业务代码包括 main 进程和 renderer 里的 JSHTML、CSS、字体、图标静态配置文件比如默认设置、常量纯 JavaScript 实现的依赖库不应该放进 asar 的原生模块.node文件需要直接 spawn/exec 的可执行文件exe、脚本运行时要写入的数据日志、数据库、用户配置体积巨大且启动就要读取的媒体资源原因后面会逐步展开核心就一句话asar 对 Electron 是“虚拟目录”不是真实磁盘目录凡是需要在操作系统层面直接访问的文件都必须被排除到 asar 外面放到app.asar.unpacked目录里。2. asar 命令行工具安装和基础操作2.1 安装从 asar 到 electron/asar现在官方维护的 npm 包名是electron/asar老项目里可能还会看到asar这个包名。如果你是新项目直接装新包名npm install -g electron/asar不想全局安装的话用 npx 也行后面所有命令我都按npx asar来写。全局安装的好处是打包脚本里可以直接用asar命令少一层解析CI 环境里也稳定一些。安装完成后可以看一眼版本号和帮助信息asar --version asar --helpelectron/asar目前提供的子命令主要有pack、list、extract、extract-file。功能非常聚焦没有什么花哨的东西但就是这几个命令已经能覆盖我在真实打包流程里 95% 的需求了。2.2 pack 打包最初的体验一条命令把整个应用目录打包成单个 asar 文件npx asar pack ./app ./dist/app.asar./app是源目录./dist/app.asar是目标文件。执行完之后dist下会生成一个体积和源目录总和差不多的文件。你可以试试用编辑器打开它会看到 JSON 头信息和一堆看不懂的二进制内容——那些就是被拼接进去的原始文件。我第一次跑这个命令时犯过一个低级错误把目标文件路径直接写成了目录路径导致 asar 生成在目录内部路径嵌套了好几层后来list的时候怎么找都找不到。这里记住一个原则pack 的第二个参数是“文件路径”不是“目录路径”如果dist目录不存在命令不会自动创建它需要你先建好。打包完成后Electron 应用在启动时会默认去找resources/app.asar这个路径。所以更常见的做法是直接把产物放到应用的resources目录下npx asar pack ./app ./resources/app.asar如果你的应用不叫 app 这个名字Electron 其实也可以加载指定名称的 asar 文件只要在入口文件里配置好路径即可但默认约定就是app.asar尽量别违反约定否则后续排查问题时会被自己坑到。2.3 unpack 参数处理 native 模块和大文件的正确姿势只执行一条asar pack一般是跑不起来的因为很多应用都有原生模块。Electron 加载.node文件时是通过操作系统动态链接库机制来加载的这就要求文件必须真实存在于磁盘上而不能存在于一个虚拟归档里。这时候就需要--unpack参数npx asar pack ./app ./resources/app.asar --unpack *.node这条命令会把所有.node文件排除在 asar 之外同时在resources目录下生成一个app.asar.unpacked文件夹里面按原始目录结构放置这些原生模块。Electron 在运行时会自动识别这种拆分方式加载原生模块时去unpacked目录找加载普通 JS 时走 asar 虚拟路径。除了--unpack按文件后缀/路径匹配还有--unpack-dir按目录匹配npx asar pack ./app ./resources/app.asar --unpack-dir node_modules/ffi实际项目中我通常会把两者结合使用。比如某个应用既用了ffi这种动态库调用模块又包含一个需要以子进程方式启动的 Python 脚本那么打包命令就长这样npx asar pack ./app ./resources/app.asar --unpack *.node --unpack-dir {python,runtime}花括号语法支持同时匹配多个目录这个特性在处理多模块项目时非常实用。务必记住无论是--unpack还是--unpack-dir匹配路径都是相对于被打包的源目录而言的别把绝对路径写进去否则匹配不生效。2.4 检查包内容list、extract、extract-file打包完成后最重要的一步是检查包内容确认有没有漏文件、有没有误塞大文件。list命令可以直接列出归档内的完整目录树npx asar list ./resources/app.asar输出结果类似/package.json /main.js /renderer/index.html /renderer/assets/logo.png /node_modules/is-odd/index.js想检查某个具体文件在不在包里可以配合grepnpx asar list ./resources/app.asar | grep config.json如果就是想验证定位某个文件的内容不一定非得解包整个 asar用extract-file单独提取npx asar extract-file ./resources/app.asar package.json这条命令会把package.json提取到当前工作目录。注意它不保留目录层级直接以文件名输出所以如果归档内有多个同名文件后提取的会覆盖前一个。这种场景下还是用extract解包完整目录更稳妥npx asar extract ./resources/app.asar ./tmp/app-source解包出来就是原始的文件目录结构可以用来核对打包内容是否完整。这个命令在生产环境定位问题时非常好用比如怀疑某个文件没打进去直接解包看目录就一目了然。3. 运行时细节Electron 是怎么把 asar“伪装”成普通目录的3.1 fs 模块的透明接管与虚拟路径Electron 之所以能让你在代码里直接fs.readFileSync(/path/to/app.asar/config.json)是因为它在运行时拦截了操作系统的文件读取调用做了一层透明适配。当你访问一个包含app.asar段落的路径时Electron 不会真的对磁盘上的 asar 文件路径做解析而是解析到这个文件在归档内部的偏移位置直接读取对应字节。这层适配覆盖了绝大多数文件系统操作读文件、列目录、查文件状态都可以正常工作。但要注意它只在 Electron 运行时内有效如果你把 asar 路径丢给操作系统原生的spawn去执行应用一启动就会告诉你“文件不存在”或“格式不正确”。我经常用一句话解释给同事听asar 对 Electron 来说是“批发市场里的虚拟摊位”你能在里面看到商品、买到商品但市场外的保安不认它因为摊位本身不在真实地图上。真正需要外部程序访问的文件就必须从虚拟摊位搬到市场门口的真实仓库里也就是unpacked目录。3.2 路径陷阱__dirname、process.resourcesPath、realpath这是我在实际项目里被坑得最惨的一个点。在 asar 包内运行的代码__dirname返回的是像/path/to/resources/app.asar/src这样的虚拟路径。大部分情况下这没问题但一旦代码里需要用这个路径去拼接一个外部程序的绝对路径比如const exePath path.join(__dirname, ../tools/helper.exe); child_process.execFile(exePath)一旦执行就会失败。因为__dirname指向的是 asar 内部操作系统根本找不到helper.exe。正确的做法是定位到真实磁盘目录。Electron 提供了process.resourcesPath它指向resources目录的真实路径。配合.unpacked后缀就能拿到解包后的文件位置const realPath path.join(process.resourcesPath, app.asar.unpacked, tools, helper.exe); child_process.execFile(realPath);另外还有一个更让人头大的点fs.realpathSync在 asar 包内有时会直接返回 asar 虚拟路径有时会根据传入路径解析到磁盘路径行为表现并不完全一致。我见过一个项目在这上面翻车——用fs.realpathSync(__dirname)去推断应用根目录结果拿到一个混合路径后续所有资源拼接全部错位。所以我的建议是在主进程里不要依赖__dirname/realpath去判断应用的物理位置始终通过process.resourcesPath组合真实路径。这是 Electron 应用路径处理里最值得记住的铁律。3.3 asar 的只读特性和缓存机制asar 的内部文件是只读的。Electron 在运行时处理写入请求时会直接抛出一个EROFS错误意思是“只在只读文件系统上执行的操作”。很多新手刚接触时完全看不懂这个报错以为文件系统出问题了。实际上不是因为磁盘坏了而是 asar 设计上就不允许对归档内部文件做写入操作。你想写入日志、数据库、用户配置都必须把存储位置指向系统提供的userData目录const { app } require(electron); const logPath path.join(app.getPath(userData), logs, app.log);userData是 Electron 专门为应用提供的数据存储目录Windows 下通常位于%APPDATA%macOS 下位于~/Library/Application Support。记住一条判断标准凡是运行时需要改动的数据一律放 userData凡是打包时固定的资源才放进 asar。缓存方面Electron 对 asar 内的文件读取有内置缓存机制所以同一个文件频繁读取时性能并不差。但如果你打包进去的是一个 200MB 的媒体文件首次读取还是会把大量字节读进内存这对启动速度和内存占用都是压力。大文件优先走--unpack或直接放在额外资源目录里是更合理的选择。4. 实际上手把一个完整应用塞进 asar 并跑起来4.1 准备一个最小 Electron 项目先构造一个能跑起来的最小项目目录结构如下my-app/ package.json main.js renderer/ index.html lib/ util.jspackage.json里声明入口和依赖{ name: my-app, version: 1.0.0, main: main.js, dependencies: {} }main.js里创建一个窗口并读取lib/util.js里返回的文本展示在页面上。逻辑不用复杂目的是验证打包后文件和路径是否正常。4.2 手工 pack 并直接运行在项目根目录执行npx asar pack ./my-app ./my-app.asar然后新建一个用于测试的 Electron 应用壳把生成的my-app.asar放到它的resources目录下启动时 Electron 会默认加载app.asar所以需要把生成的文件重命名mv my-app.asar test-shell/resources/app.asar启动壳应用后如果窗口正常显示说明打包后的应用运行成功。这个过程虽然简单却是理解 asar 各种行为的最快路径——以后遇到路径问题、文件缺失问题都会第一时间联想到是不是 asar 的虚拟目录在作祟。我建议每一个做 Electron 的开发者都亲手走一遍这个流程哪怕你的项目之后全靠 electron-builder 一条命令打完整包。因为当构建工具把报错装饰得漂漂亮亮时你很难看懂底层到底发生了什么而手动 pack、手动放目录、手动改名每一步都在告诉你“归档文件到底安放在哪个位置”。4.3 与 electron-builder 联动asar 配置和 asarUnpack手工流程适合学习和排查实际分发用的还是 electron-builder 这类打包工具。electron-builder 默认就会把应用打包成 asar但如果你需要微调 unpack 规则就在package.json的build配置里加{ build: { asar: true, asarUnpack: [ **/*.node, runtime/** ] } }asarUnpack的匹配规则支持 glob 模式**/*.node匹配任意目录下的原生模块runtime/**匹配整个运行时目录。工具会自动生成app.asar和app.asar.unpacked并在安装目录里保持两者共存。有一个细节我提醒过很多人electron-builder 打包时如果你在asarUnpack里排除了大量文件安装包体积会增加但应用的启动速度和对原生模块的兼容性会更好。这里需要做一个平衡不要无脑全部 unpack也不要固执地拒绝 unpack。我一般默认只 unpack 原生模块和必需的静态可执行文件其他资源按需调整。5. 踩坑合集我见过的常见问题与排查思路5.1 应用启动直接白屏控制台报“Cannot find module”这类问题大概率是某个 JS 文件没被正确包含在 asar 内。比如代码里require(/project/utils/config.js)用了绝对路径但打包时project目录并不在你的应用目录里打包工具根本找不到它。排查思路分三步用asar list查看归档内是否存在对应文件检查require路径是不是引用了代码目录之外的绝对路径如果确认文件存在但依然报错检查是不是路径大小写问题——Windows 不区分大小写Linux 下区分Electron 打包后运行在 Linux 或 macOS 上时大小写敏感路径经常暴露问题5.2 代码读不到 exechild_process 和 asar 内文件的边界程序里要用child_process.execFile启动一个辅助进程但无论如何都提示 “ENOENT”因为文件路径指向了 asar 内部操作系统不认账。正确做法是把这些可执行文件排除出 asar并在代码里通过process.resourcesPath /app.asar.unpacked/...拼路径。记住这句判断标准凡是交给操作系统直接执行的都必须真实存在于磁盘不能活在 asar 里。同理动态链接库.dll、.dylib、.so也一样老实用 unpack 处理。5.3 数据库写入失败 EROFS别把可写数据放 asarSQLite 数据库写到一半报EROFS日志文件死活写不进去这些问题都是因为程序把动态数据放到 asar 包内的目录了。asar 在设计上就只支持读取不支持写入Electron 运行时不会放行这类操作。解决方式就是改造数据存储逻辑统一把所有运行时数据放到app.getPath(userData)下。第一次初始化时把默认配置从 asar 里读出来再写入到 userData 里的用户配置。这一点在项目初期就要规划好否则后期大范围重构成本非常高。5.4 误把 asar 当加密方案源码保护的真相最后说一个很多人问过的问题asar 能防止别人看源码吗不能。asar 只是一个归档格式它不加密、不混淆只是把文件拼在一起。网络上有现成的工具可以解包 asar几秒钟就能把全部源码还原出来。如果你的应用里包含敏感的密钥、前置服务器地址这类信息放在 asar 里等于裸奔。真要保护源码得考虑额外的方案混淆 JavaScript、部分逻辑移动到远程服务端、或者用字节码编译。但这些都会增加复杂度要结合产品实际需求来选择。这里最重要的结论是不要指望 asar 做安全边界。最后再分享一个我实测好用的技巧在 CI 构建流程里可以用asar list和grep做一次产物校验比如检查是否有config.json、main.js、关键原生模块是否都在包内。别小看这层自动化检查它能及时拦截“打包时目录没更新导致线上版本缺文件”这种低级事故。我吃过一次亏后就再也没跳过这一步。