electron-updater-for-render 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.zh-CN.md +359 -0
- package/Readme.md +356 -0
- package/dist/builder/index.cjs +112 -0
- package/dist/builder/index.d.ts +2 -0
- package/dist/builder/index.js +77 -0
- package/dist/builder/vite-plugin.cjs +133 -0
- package/dist/builder/vite-plugin.d.ts +5 -0
- package/dist/builder/vite-plugin.js +96 -0
- package/dist/main/index.cjs +372 -0
- package/dist/main/index.d.ts +43 -0
- package/dist/main/index.js +337 -0
- package/dist/types.d.ts +105 -0
- package/package.json +63 -0
package/README.zh-CN.md
ADDED
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
# electron-updater-for-render
|
|
2
|
+
|
|
3
|
+
[English](./README.md)
|
|
4
|
+
|
|
5
|
+
专为 Electron 渲染进程设计的工业级混合式增量热更新框架(支持 Vue / React / 纯 HTML)。
|
|
6
|
+
|
|
7
|
+
与 `electron-updater` 重新下载完整安装包的方案不同,本库通过替换 `.asar` 文件实现**渲染资源(HTML/CSS/JS)的无权限、无安装程序热更新**——用户完全无感知,秒级生效。
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## 🚀 核心特性
|
|
12
|
+
|
|
13
|
+
- **双轨更新模式**:内置原生弹窗自动更新 + 暴露 IPC 接口支持前端自定义进度 UI,两种方案自由切换
|
|
14
|
+
- **强制更新控制**:云端下发 `forceUpdate: 'prompt'` 或 `'silent'`,应对 P0 级线上事故时一键锁死用户退路
|
|
15
|
+
- **从容退场钩子**:`onBeforeRestart` 让渲染进程在 `app.relaunch()` 之前有充足时间保存草稿、提交日志
|
|
16
|
+
- **并发互斥锁**:内置 `isDownloading` 单例锁,防止手动触发与开机自检同时争写同一 ASAR 文件
|
|
17
|
+
- **智能冷库清道夫**:`maxVersionsToKeep` 自动淘汰最旧历史版本,保留可回滚备份,告别 C 盘无限膨胀
|
|
18
|
+
- **Pipeline 流式下载**:Node.js `stream/promises pipeline` + `Transform` 管道,内存零泄漏,网络背压自动控制
|
|
19
|
+
- **零配置版本探测**:默认自动读取 `process.cwd()/package.json` 中的版本号,无需任何额外配置
|
|
20
|
+
- **独立打包命令**:普通 `npm run build` 不受任何影响,只有 `npm run build:update` 才会触发 ASAR 打包
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## 📦 安装
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
npm install electron-updater-for-render
|
|
28
|
+
|
|
29
|
+
# 打包阶段依赖(在渲染层项目中安装)
|
|
30
|
+
npm install -D asar cross-env
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
## 🗺️ 工作原理
|
|
36
|
+
|
|
37
|
+
```
|
|
38
|
+
┌─────────────────────────────────────────────────────┐
|
|
39
|
+
│ 渲染层项目(如 Vue 工程) │
|
|
40
|
+
│ npm run build:update │
|
|
41
|
+
│ → vite build → 打包 ASAR → 生成 latest.json │
|
|
42
|
+
└───────────────┬─────────────────────────────────────┘
|
|
43
|
+
│ 上传 dist_updates/ 到服务器
|
|
44
|
+
▼
|
|
45
|
+
┌─────────────────────────────────────────────────────┐
|
|
46
|
+
│ 更新服务器(Nginx / OSS / CDN) │
|
|
47
|
+
│ 提供:latest.json + /1.0.2/renderer.asar │
|
|
48
|
+
└───────────────┬─────────────────────────────────────┘
|
|
49
|
+
│ 应用启动时发起 HTTP 请求
|
|
50
|
+
▼
|
|
51
|
+
┌─────────────────────────────────────────────────────┐
|
|
52
|
+
│ Electron 主进程 │
|
|
53
|
+
│ updater.checkForUpdatesAndNotify() │
|
|
54
|
+
│ → 检查版本 → 下载 ASAR → 重启应用 │
|
|
55
|
+
└─────────────────────────────────────────────────────┘
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
## 🛠️ 完整接入指南
|
|
61
|
+
|
|
62
|
+
### 第一步 — 渲染层项目:安装并配置 Vite 插件
|
|
63
|
+
|
|
64
|
+
```typescript
|
|
65
|
+
// vite.config.ts
|
|
66
|
+
import { defineConfig } from 'vite'
|
|
67
|
+
import { electronRenderUpdater } from 'electron-updater-for-render/vite'
|
|
68
|
+
|
|
69
|
+
export default defineConfig({
|
|
70
|
+
plugins: [
|
|
71
|
+
electronRenderUpdater({
|
|
72
|
+
outDir: './dist', // 必填:Vite 构建输出目录
|
|
73
|
+
updatesDir: './dist_updates', // 可选:更新包输出目录(默认 './dist_updates')
|
|
74
|
+
// version: '1.2.3' // 可选:显式指定版本号(优先级高于 package.json)
|
|
75
|
+
// packageJsonPath: './package.json' // 可选:自定义 package.json 路径
|
|
76
|
+
// forceUpdate: 'prompt' // 可选:'prompt' | 'silent',仅 P0 紧急修复时使用
|
|
77
|
+
})
|
|
78
|
+
]
|
|
79
|
+
})
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
**版本号解析优先级:**
|
|
83
|
+
1. `version` 字段(显式指定,优先级最高)
|
|
84
|
+
2. `packageJsonPath` 所指向文件中的 `version`
|
|
85
|
+
3. 自动检测:`process.cwd()/package.json`(默认,零配置)
|
|
86
|
+
|
|
87
|
+
### 第二步 — 渲染层项目:配置独立的打包命令
|
|
88
|
+
|
|
89
|
+
在 `package.json` 中新增 `build:update` 命令,与普通 `build` **完全分离**:
|
|
90
|
+
|
|
91
|
+
```json
|
|
92
|
+
{
|
|
93
|
+
"scripts": {
|
|
94
|
+
"dev": "vite",
|
|
95
|
+
"build": "vite build",
|
|
96
|
+
"build:update": "cross-env ELECTRON_PACK_UPDATE=1 vite build"
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
| 命令 | 行为 |
|
|
102
|
+
|---|---|
|
|
103
|
+
| `npm run build` | 普通前端打包,**不生成 ASAR**,与原有流程完全一致 |
|
|
104
|
+
| `npm run build:update` | 构建前端产物 + 打包 ASAR + 生成 `latest.json`,用于发布更新 |
|
|
105
|
+
|
|
106
|
+
### 第三步 — 部署更新文件到服务器
|
|
107
|
+
|
|
108
|
+
执行 `npm run build:update` 后,`dist_updates/` 目录会生成:
|
|
109
|
+
|
|
110
|
+
```
|
|
111
|
+
dist_updates/
|
|
112
|
+
├── latest.json ← 版本清单(客户端拉取此文件判断是否需要更新)
|
|
113
|
+
└── 1.0.2/
|
|
114
|
+
└── renderer.asar ← 实际的更新载荷
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
**将 `dist_updates/` 目录下的全部内容上传至更新服务器:**
|
|
118
|
+
|
|
119
|
+
```bash
|
|
120
|
+
# 示例:同步到 S3 存储桶
|
|
121
|
+
aws s3 sync dist_updates/ s3://your-bucket/auto-updates/
|
|
122
|
+
|
|
123
|
+
# 示例:上传到 Nginx 服务器
|
|
124
|
+
rsync -avz dist_updates/ user@your-server:/var/www/auto-updates/
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
客户端会依次请求:
|
|
128
|
+
- `GET https://your-server.com/latest.json` (获取版本清单)
|
|
129
|
+
- `GET https://your-server.com/1.0.2/renderer.asar` (下载更新包)
|
|
130
|
+
|
|
131
|
+
---
|
|
132
|
+
|
|
133
|
+
### 第四步 — Electron 外壳:配置主进程运行时
|
|
134
|
+
|
|
135
|
+
```typescript
|
|
136
|
+
// src/main/index.ts
|
|
137
|
+
import { app, BrowserWindow, ipcMain } from 'electron'
|
|
138
|
+
import { join } from 'path'
|
|
139
|
+
import { RenderUpdater } from 'electron-updater-for-render'
|
|
140
|
+
|
|
141
|
+
const updater = new RenderUpdater({
|
|
142
|
+
updateUrl: 'https://your-server.com/auto-updates', // 必填:更新服务器根地址
|
|
143
|
+
versionsDir: join(app.getPath('userData'), 'versions'), // 必填:本地版本存储目录
|
|
144
|
+
maxVersionsToKeep: 2, // 可选:保留最近 2 个旧版本供回滚(默认 2)
|
|
145
|
+
|
|
146
|
+
// 可选:重启前给渲染层时间保存状态
|
|
147
|
+
onBeforeRestart: async () => {
|
|
148
|
+
const [win] = BrowserWindow.getAllWindows()
|
|
149
|
+
if (win) {
|
|
150
|
+
win.webContents.send('updater:before-restart')
|
|
151
|
+
await new Promise(resolve => setTimeout(resolve, 2000))
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
app.whenReady().then(async () => {
|
|
157
|
+
const mainWindow = new BrowserWindow({
|
|
158
|
+
webPreferences: { preload: join(__dirname, '../preload/index.js') }
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
// 优先加载已下载的热更新 ASAR,否则回退到开发服务器
|
|
162
|
+
const loadUrl = updater.getLoadUrl()
|
|
163
|
+
mainWindow.loadURL(loadUrl || 'http://localhost:5173')
|
|
164
|
+
|
|
165
|
+
// 延迟触发自动检测(支持拦截强制更新,autoPrompt 为 true 时会弹窗提示)
|
|
166
|
+
setTimeout(() => updater.checkForUpdatesAndNotify(), 3000)
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
// 暴露 IPC 接口,供前端界面主动控制更新流程
|
|
170
|
+
ipcMain.handle('updater:check', () => updater.check())
|
|
171
|
+
ipcMain.handle('updater:download', () =>
|
|
172
|
+
updater.download((percent) => {
|
|
173
|
+
BrowserWindow.getAllWindows()[0]?.webContents.send('updater:progress', percent)
|
|
174
|
+
})
|
|
175
|
+
)
|
|
176
|
+
ipcMain.handle('updater:install', () => {
|
|
177
|
+
app.relaunch()
|
|
178
|
+
app.quit()
|
|
179
|
+
})
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
---
|
|
183
|
+
|
|
184
|
+
### 第五步 — Preload 脚本
|
|
185
|
+
|
|
186
|
+
```typescript
|
|
187
|
+
// src/preload/index.ts
|
|
188
|
+
import { contextBridge, ipcRenderer } from 'electron'
|
|
189
|
+
|
|
190
|
+
contextBridge.exposeInMainWorld('updater', {
|
|
191
|
+
check: () => ipcRenderer.invoke('updater:check'),
|
|
192
|
+
download: () => ipcRenderer.invoke('updater:download'),
|
|
193
|
+
install: () => ipcRenderer.invoke('updater:install'),
|
|
194
|
+
|
|
195
|
+
onProgress: (callback: (percent: number) => void) => {
|
|
196
|
+
const fn = (_: any, p: number) => callback(p)
|
|
197
|
+
ipcRenderer.on('updater:progress', fn)
|
|
198
|
+
return () => ipcRenderer.removeListener('updater:progress', fn)
|
|
199
|
+
},
|
|
200
|
+
|
|
201
|
+
onBeforeRestart: (callback: () => void) => {
|
|
202
|
+
const fn = () => callback()
|
|
203
|
+
ipcRenderer.on('updater:before-restart', fn)
|
|
204
|
+
return () => ipcRenderer.removeListener('updater:before-restart', fn)
|
|
205
|
+
}
|
|
206
|
+
})
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
---
|
|
210
|
+
|
|
211
|
+
### 第六步 — 渲染层 UI(Vue 示例)
|
|
212
|
+
|
|
213
|
+
```vue
|
|
214
|
+
<script setup lang="ts">
|
|
215
|
+
import { ref, onMounted, onUnmounted } from 'vue'
|
|
216
|
+
|
|
217
|
+
const status = ref('')
|
|
218
|
+
const progress = ref(0)
|
|
219
|
+
const hasUpdate = ref(false)
|
|
220
|
+
|
|
221
|
+
let removeProgress: (() => void) | null = null
|
|
222
|
+
let removeRestart: (() => void) | null = null
|
|
223
|
+
|
|
224
|
+
onMounted(() => {
|
|
225
|
+
// 监听重启通知,提前保存状态
|
|
226
|
+
removeRestart = window.updater.onBeforeRestart(() => {
|
|
227
|
+
status.value = '即将重启,保存状态中...'
|
|
228
|
+
localStorage.setItem('draft', JSON.stringify({ /* 你的状态 */ }))
|
|
229
|
+
})
|
|
230
|
+
})
|
|
231
|
+
|
|
232
|
+
onUnmounted(() => {
|
|
233
|
+
removeProgress?.()
|
|
234
|
+
removeRestart?.()
|
|
235
|
+
})
|
|
236
|
+
|
|
237
|
+
// 手动检查更新
|
|
238
|
+
const checkUpdate = async () => {
|
|
239
|
+
status.value = '检查中...'
|
|
240
|
+
const result = await window.updater.check()
|
|
241
|
+
if (result.updateAvailable) {
|
|
242
|
+
hasUpdate.value = true
|
|
243
|
+
status.value = `发现新版本:v${result.version}`
|
|
244
|
+
} else {
|
|
245
|
+
status.value = '当前已是最新版本'
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// 手动下载更新
|
|
250
|
+
const downloadUpdate = async () => {
|
|
251
|
+
status.value = '下载中...'
|
|
252
|
+
removeProgress = window.updater.onProgress((p) => {
|
|
253
|
+
progress.value = p
|
|
254
|
+
status.value = `下载中 ${p.toFixed(0)}%`
|
|
255
|
+
})
|
|
256
|
+
await window.updater.download()
|
|
257
|
+
removeProgress?.()
|
|
258
|
+
status.value = '下载完成,可以安装'
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// 安装并重启
|
|
262
|
+
const installUpdate = () => {
|
|
263
|
+
window.updater.install()
|
|
264
|
+
}
|
|
265
|
+
</script>
|
|
266
|
+
|
|
267
|
+
<template>
|
|
268
|
+
<div>
|
|
269
|
+
<p>{{ status }}</p>
|
|
270
|
+
<progress :value="progress" max="100" v-if="progress > 0" />
|
|
271
|
+
<button @click="checkUpdate">检查更新</button>
|
|
272
|
+
<button @click="downloadUpdate" v-if="hasUpdate">立即下载</button>
|
|
273
|
+
<button @click="installUpdate">安装并重启</button>
|
|
274
|
+
</div>
|
|
275
|
+
</template>
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
---
|
|
279
|
+
|
|
280
|
+
## ⚙️ API 参考
|
|
281
|
+
|
|
282
|
+
### `BuilderOptions`(Vite 插件参数)
|
|
283
|
+
|
|
284
|
+
| 参数 | 类型 | 必填 | 说明 |
|
|
285
|
+
|---|---|---|---|
|
|
286
|
+
| `outDir` | `string` | ✅ | Vite 构建输出目录 |
|
|
287
|
+
| `updatesDir` | `string` | — | 更新包输出目录,默认 `./dist_updates` |
|
|
288
|
+
| `version` | `string` | — | 显式指定版本号,优先级高于 `packageJsonPath` |
|
|
289
|
+
| `packageJsonPath` | `string` | — | 自定义 `package.json` 路径,不填则自动读取 `process.cwd()/package.json` |
|
|
290
|
+
| `asarName` | `string` | — | ASAR 文件名,默认 `renderer.asar` |
|
|
291
|
+
| `privateKeyPath` | `string` | — | RSA 私钥路径,用于为 ASAR 生成签名 |
|
|
292
|
+
| `releaseNotesPath` | `string` | — | 更新日志 Markdown 文件路径 |
|
|
293
|
+
| `forceUpdate` | `'prompt' \| 'silent'` | — | 强制更新模式,`'prompt'` 弹窗告知,`'silent'` 完全静默 |
|
|
294
|
+
|
|
295
|
+
### `UpdaterOptions`(主进程参数)
|
|
296
|
+
|
|
297
|
+
| 参数 | 类型 | 必填 | 说明 |
|
|
298
|
+
|---|---|---|---|
|
|
299
|
+
| `updateUrl` | `string` | ✅ | 更新服务器根地址 |
|
|
300
|
+
| `versionsDir` | `string` | ✅ | 本地 ASAR 版本存储目录 |
|
|
301
|
+
| `publicKey` | `string` | — | RSA 公钥(PEM 格式),用于验证 ASAR 签名 |
|
|
302
|
+
| `autoDownload` | `boolean` | — | 发现更新后自动下载,不弹窗询问。默认 `false` |
|
|
303
|
+
| `autoPrompt` | `boolean` | — | 使用内置原生弹窗引导更新。默认 `true` |
|
|
304
|
+
| `maxVersionsToKeep` | `number` | — | 本地保留的旧版本数量(不含当前),默认 `2` |
|
|
305
|
+
| `onUpdateAvailable` | `function` | — | 检测到更新时的自定义钩子:`(info, doDownload) => void` |
|
|
306
|
+
| `onDownloadProgress` | `function` | — | 下载进度回调:`(percent: number) => void` |
|
|
307
|
+
| `onDownloadComplete` | `function` | — | 下载完成钩子:`(info, doInstall) => void` |
|
|
308
|
+
| `onError` | `function` | — | 错误回调:`(error: Error) => void` |
|
|
309
|
+
| `onBeforeRestart` | `async function` | — | `app.relaunch()` 前的异步钩子,可用于保存状态 |
|
|
310
|
+
|
|
311
|
+
---
|
|
312
|
+
|
|
313
|
+
## 🛡️ 强制更新(P0 应急使用)
|
|
314
|
+
|
|
315
|
+
当线上出现资损、安全漏洞、核心功能崩溃等 P0 级故障时,通过 `forceUpdate` 剥夺用户的拒绝权:
|
|
316
|
+
|
|
317
|
+
```typescript
|
|
318
|
+
// vite.config.ts(打包前加上此行)
|
|
319
|
+
electronRenderUpdater({
|
|
320
|
+
outDir: './dist',
|
|
321
|
+
forceUpdate: 'prompt' // 或 'silent'
|
|
322
|
+
})
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
| 模式 | 行为 |
|
|
326
|
+
|---|---|
|
|
327
|
+
| `'prompt'` | 弹出系统级警告弹窗,**只有一个确认按钮**,用户无法取消或跳过。点击后立即开始下载并重启 |
|
|
328
|
+
| `'silent'` | 完全静默:后台自动下载、自动安装、自动重启,用户毫无感知 |
|
|
329
|
+
|
|
330
|
+
> ⚠️ 本功能会**永久剥夺用户本次的延迟权**,请仅在真正的 P0 级生产事故中使用。
|
|
331
|
+
|
|
332
|
+
---
|
|
333
|
+
|
|
334
|
+
## 🔐 RSA 签名验证(可选,推荐生产环境启用)
|
|
335
|
+
|
|
336
|
+
```bash
|
|
337
|
+
# 生成密钥对
|
|
338
|
+
openssl genrsa -out private.pem 2048
|
|
339
|
+
openssl rsa -in private.pem -pubout -out public.pem
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
```typescript
|
|
343
|
+
// vite.config.ts
|
|
344
|
+
electronRenderUpdater({ outDir: './dist', privateKeyPath: './private.pem' })
|
|
345
|
+
|
|
346
|
+
// main/index.ts
|
|
347
|
+
import { readFileSync } from 'fs'
|
|
348
|
+
new RenderUpdater({
|
|
349
|
+
updateUrl: '...',
|
|
350
|
+
versionsDir: '...',
|
|
351
|
+
publicKey: readFileSync('./public.pem', 'utf-8')
|
|
352
|
+
})
|
|
353
|
+
```
|
|
354
|
+
|
|
355
|
+
---
|
|
356
|
+
|
|
357
|
+
## License
|
|
358
|
+
|
|
359
|
+
MIT
|