Electron 桌面客户端 ASAR 热更新:替换一个文件完成版本切换
Electron 桌面应用的更新一直是个痛点——一次小小的 UI 修改就要让用户重新下载运行 100MB+ 的安装包。本文介绍一种利用 Electron ASAR 打包机制实现"零安装"版本切换的方案:只需替换一个 app.asar 文件,客户端下次启动自动加载新版本。|
一、ASAR 机制与更新思路
1. ASAR 是什么
ASAR(Atom Shell Archive)是 Electron 专用的打包格式,类似于 tar——将所有文件顺序拼接为单个文件,不做压缩但支持随机读取。Electron 通过补丁让 Node.js 的 fs 和 require API 将 .asar 文件视为虚拟目录:
1 // 读取 asar 内的文件(像普通目录一样) 2 fs.readFileSync('/path/to/app.asar/dist/main/main.js') 3 4 // require asar 内的模块 5 require('/path/to/app.asar/node_modules/electron-log')
Electron 打包后的应用目录结构:
1 安装目录/ 2 ├── electron.exe + lib/ ← Electron 框架(~70MB) 3 └── resources/ 4 └── app.asar ← 全部业务代码 + npm 依赖(~50MB)
app.asar 内包含:主进程代码(main.js)、IPC 桥接层(preload.js)、渲染进程代码(HTML/JS/CSS)以及全部 node_modules。Electron 启动时固定从
参考文档: |
2. 更新思路
既然全部业务代码都在 app.asar 一个文件里,替换这个文件就能完成版本切换。但有一个限制——Electron 官方明确说明,类似运行时不能替换 .exe。
解决方案:让客户端优先从外部目录加载 asar,而非安装目录。配合一个后台管理服务(独立于客户端进程),在客户端退出后管理外部 asar 文件。
3. 方案对比
| 方案 | 更新包大小 | 覆盖范围 | 客户端改动量 |
|---|---|---|---|
| 仅替换前端资源 | ~5MB | 仅渲染进程 | 中等 |
| Bootstrap + 外部目录加载 | ~5MB | 全部源码,不含 npm 依赖 | 中等 |
| 替换 app.asar(本方案) | ~50MB | 全部源码 + npm 依赖 | 极小(~15行) |
二、Bootstrap Loader:让 Electron 从外部加载
1. 核心问题
Electron 默认从
2. 实现方式
在安装目录的 app.asar 内新增一个 bootstrap.js 作为 Electron 入口(通过 package.json 的 main 字段指定),逻辑仅 ~15 行:
1 // src/main/bootstrap.ts — 永不变更 2 import path from 'path'; 3 import fs from 'fs'; 4 5 const BASE_DIR = path.join( 6 process.env.PROGRAMDATA || 'C:\\ProgramData', 7 'AppUpdater', 'MyApp', 8 ); 9 const CURRENT_FILE = path.join(BASE_DIR, '@current'); 10 const VERSION_RE = /^[\d.\-a-zA-Z]+$/; // 防路径注入 11 12 try { 13 // 1. 读指针文件,获取当前应加载的版本号 14 const version = fs.readFileSync(CURRENT_FILE, 'utf8').trim(); 15 if (!VERSION_RE.test(version)) throw new Error(`invalid: ${version}`); 16 17 // 2. 拼出版本化路径 18 const externalAsar = path.join(BASE_DIR, version, 'app.asar'); 19 if (!fs.existsSync(externalAsar)) throw new Error('not found'); 20 21 // 3. Electron require 支持 asar 虚拟路径,自包含 node_modules 22 const pkg = JSON.parse( 23 fs.readFileSync(path.join(externalAsar, 'package.json'), 'utf8'), 24 ); 25 require(path.join(externalAsar, pkg.main)); 26 } catch { 27 // 任何异常 → fallback 到安装目录内置的完整代码 28 require(path.join(__dirname, 'dist', 'main', 'main.js')); 29 }
关键设计:
- 版本化目录:外部目录按版本号组织(
- try/catch 兜底:指针文件损坏、asar 文件不存在、任何异常都回退到安装目录内置代码,客户端永远能正常启动
- 路径注入防护:
@current的内容做正则校验,仅允许数字、字母、点号 - require 支持 asar:Electron 的
require()天然支持 asar 虚拟路径,外部 asar 内的node_modules也能正确解析,无需额外配置
3. 本地文件布局
1 外部目录(后台服务管理): 2 %PROGRAMDATA%\AppUpdater\MyApp\ 3 ├── @current ← 指针文件,内容:"2505.3.0" 4 ├── 2505.2.0\ 5 │ └── app.asar ← 旧版本(保留便于回滚) 6 └── 2505.3.0\ 7 └── app.asar ← 当前激活版本 8 9 安装目录(永远的 fallback): 10 C:\Program Files\MyApp\resources\app.asar 11 ← 含 bootstrap.js + 完整业务代码
三、打包与 CI 适配
1. Webpack 构建配置修改
在现有的 webpack 主进程配置中新增 bootstrap 入口:
1 // .erb/configs/webpack.config.main.prod.ts 2 entry: { 3 main: path.join(srcMainPath, 'main.ts'), 4 preload: path.join(srcMainPath, 'preload.ts'), 5 bootstrap: path.join(srcMainPath, 'bootstrap.ts'), // 新增 6 }, 7 output: { 8 path: distMainPath, 9 filename: '[name].js', 10 },
同时修改
1 { "main": "./bootstrap.js" }
这样 Electron 启动时先执行 bootstrap.js,由它决定加载哪个版本的 main.js。
2. CI 流水线新增步骤
在 electron-builder 打包完成后,从 win-unpacked 中提取 app.asar 并上传:
1 # 提取 app.asar 2 $asarPath = ".\release\build\win-unpacked\resources\app.asar" 3 4 # 打包为 zip(仅 app.asar) 5 Compress-Archive -Path $asarPath -DestinationPath ".\release\publish\${TAG}.zip" 6 7 # 计算 sha256 8 $hash = (Get-FileHash -Path ".\release\publish\${TAG}.zip" -Algorithm SHA256).Hash 9 10 # 生成 version.json 11 @{ version = $TAG; sha256 = $hash } | 12 ConvertTo-Json | 13 Set-Content ".\release\publish\version.json"
发布流程设计为两步:upload(自动) 上传 zip 到归档目录,publish(手动) 覆盖 version.json 到 S3 根目录。后台服务轮询 version.json 感知新版本,下载 zip、校验 sha256、解压到版本目录、原子切换 @current 指针。
3. 整包更新与 asar 更新共存
已有的 electron-updater 整包更新机制不受影响,两者各司其职:
| 更新类型 | 触发场景 | 更新包 |
|---|---|---|
| asar 更新 | 日常迭代(UI、业务逻辑、npm 小版本) | ~50MB zip |
| 整包更新 | Electron 框架升级、外部工具更新 | ~180MB NSIS |
总结:利用 Electron 的 ASAR 虚拟文件系统特性,通过一个 ~15 行的 bootstrap loader 实现外部 asar 加载,配合后台管理服务的原子指针切换,以最小的客户端改动实现了完整的应用代码更新。版本化目录设计天然支持零成本回滚,try/catch 兜底确保客户端永远能正常启动。
引用链接:

浙公网安备 33010602011771号