桌面 RPA 有一个大部分 Electron 应用不会遇到的问题:除了应用本身,它还得把执行引擎和浏览器内核一起分发给用户。作为一个开源免费 RPA 项目,FreeRPA(https://github.com/freerpa/freerpa)的打包配置就是围绕这个问题展开的——它在 electron-builder.yml 和几个 scripts 里,把三套运行时和一个原生数据库模块安排得比较清楚。这篇就记录它的分发策略,以及背后的工程原因。
一、先看它要分发哪些东西
一个 RPA 客户端运行起来,需要的东西比普通应用多:
- Electron 应用壳(主进程 + 渲染进程代码,Vue 3 那套);
- Deno 运行时(执行引擎的宿主,作为独立二进制随包分发);
- Worker 运行时资源(节点执行器、引擎源码、依赖闭包);
- Chromium 浏览器内核(指纹浏览器内核,随包内置);
- sqlite3 原生模块(N-API 二进制,本地数据层依赖)。
难点在于:这些组件对"打包进 asar 还是放 asar 外"的诉求完全不同。
二、三个分发区:asar / asarUnpack / extraResources
electron-builder 提供了三种"放东西的地方",FreeRPA 对它们的使用很有代表性:
1. asar(应用代码)
files里把src/*、scripts/**、resources/**、browser/**全排除——应用代码只进 asar,其他东西按各自的方式走,避免重复打包。注释里写得很直白:浏览器内核只经 extraResources 按平台单独分发,防止重复打进 asar 导致包体膨胀。
2. asarUnpack(保留在 asar 外的原生二进制)
esbuild的平台原生二进制走asarUnpack。原因很具体:asar 是只读虚拟文件系统,无法 spawn 里面的可执行文件——如果不 unpack,打包插件功能会报spawn ENOTDIR(开发环境下 node_modules 是实体文件所以正常,打包后 asar 化才暴露)。这类"开发正常、打包炸"的问题,是打包工程里最值得写注释的地方,这个文件里就写清楚了。
3. extraResources(完整的外部资源)
三样大件都走这里:
resources/deno→deno:Deno 二进制;resources/worker→worker:worker 运行时资源,filter 排除了 node_modules;resources/worker/node_modules→worker/node_modules:依赖闭包单独显式复制。
第三行的存在很关键。worker 的依赖闭包是 deno cache 预填充生成的,目录布局已经实体化,如果不单独复制,运行时无法解析节点依赖——这是比"漏文件"更难排查的问题,所以它宁可拆成两条 extraResources 规则,也要保证依赖闭包一定在。
三、原生依赖:sqlite3 的 N-API 问题
sqlite3 是 N-API 模块,二进制靠prebuild-install下载(或 node-gyp 本地编译)。网络失败时node_sqlite3.node缺失,打包产物里就没有它,运行时直接报 “Could not locate the bindings file”。
ensure-native.mjs解决得很幂等:
- 先用
fs.realpathSync解析真实安装路径——因为 yarn 的.deno隔离布局下,node_modules/sqlite3是个符号链接,不解析真实路径会找错地方; - 检查
build/Release/node_sqlite3.node是否存在,存在就跳过(幂等); - 缺失才跑
prebuild-install,失败回退 node-gyp。
对应地,配置里npmRebuild: false——原生模块的构建不交给 electron-builder 默认流程,而是由这个脚本在打包前置阶段统一保证,避免两套机制互相打架。
四、worker 运行时:构建、而不是复制
worker 那部分不是简单把目录拷过去,而是有专门的构建脚本build-worker.mjs,产出到resources/worker/:
- worker 源码(
host.js/engine.js/bridge.js/worker-common.js/data-bridge.js/electron-bridge.js/import-map.json/core/**); - 节点执行器(
nodes/<type>/V<n>/execute.js,含同目录相对依赖); node_modules/**:deno cache 预填充的依赖闭包;version.json。
关键点是import map:deno 的模块解析依赖import-map.json把裸模块名映射到闭包路径,生产布局靠它才能解析节点里的 npm 依赖。构建脚本还区分了 dev / prod:--dev只复制源码、跳过 deno cache,开发时直接用项目根 node_modules——这样开发迭代快,产物不脏。
五、浏览器内核:按平台分发,只打包当前平台
Chromium 内核体积大,不能每个平台都塞一遍。electron-builder.yml 里 Windows、macOS、Linux 各自的extraResources只打包当前平台的目录:
- win:
browser/win32→browser/win32 - mac:
browser/darwin→browser/darwin - linux:
browser/linux→browser/linux
注释直接说明了意图:“内置浏览器内核:仅打包当前平台,随包分发无需下载”。这套"内置免费指纹浏览器内核随安装包走"的设计,从用户视角看就是免安装浏览器、开箱即用;从工程视角看,则是用 electron-builder 的按平台 extraResources 机制,把包体翻倍的问题挡在了配置层。
macOS 侧还配了 entitlements(沙盒权限声明)和相机/麦克风/文档目录的用途说明,notarize: false关闭了公证(本地分发场景的取舍);Windows 侧 NSIS 允许用户改安装目录;Linux 侧一次产出 AppImage / snap / deb 三个目标。
六、一条命令的构建流水线
最终打包不是散装的,package.json 里把步骤串成了一条流水线:
npm run ensure:native && npm run fetch:deno && npm run build:worker && npm run build即:保证 sqlite3 原生二进制 → 下载当前平台 Deno → 构建 worker 运行时 → electron-vite 构建 → electron-builder 出包。前置步骤全部幂等,重复执行不会重复下载或重复构建。
小结
这个项目打包设计最值得借鉴的地方,是把"分发什么、放哪个区、为什么"用注释和脚本写得很清楚:asar 管代码、asarUnpack 管"必须在 asar 外还能执行"的二进制、extraResources 管大件外部资源,原生依赖用幂等脚本前置保证,worker 运行时用构建而非复制的方式产出,浏览器内核按平台分发。对要分发多运行时或原生依赖的跨平台桌面应用,这份配置本身就是一份不错的参考模板。源码在 https://github.com/freerpa/freerpa 。