electron-updater-for-render 1.1.2-beta.5 → 1.1.2-beta.6

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  # electron-updater-for-render
2
2
 
3
- [English](./README.md)
3
+ [English](./Readme.md)
4
4
 
5
5
  专为 Electron 渲染进程设计的工业级混合式增量热更新框架(支持 Vue / React / 纯 HTML)。
6
6
 
@@ -11,13 +11,17 @@
11
11
  ## 🚀 核心特性
12
12
 
13
13
  - **双轨更新模式**:内置原生弹窗自动更新 + 暴露 IPC 接口支持前端自定义进度 UI,两种方案自由切换
14
+ - **三态检查系统**:`check()` 返回 `idle / available / ready` 三种状态,UI 始终知道应该显示"下载"还是"重启",彻底消除重复下载弹窗
15
+ - **就绪状态持久化**:用户点击"稍后安装"后,下载的更新会记录到磁盘。下次启动或刷新页面,UI 直接跳到"立即重启"——无需重新下载
16
+ - **实时状态推送**:`onStatusChanged` 钩子让主进程在更新就绪时(如静默后台下载完成)主动通知渲染层,UI 即时刷新无需用户手动操作
14
17
  - **强制更新控制**:云端下发 `forceUpdate: 'prompt'` 或 `'silent'`,应对 P0 级线上事故时一键锁死用户退路
15
18
  - **从容退场钩子**:`onBeforeRestart` 让渲染进程在 `app.relaunch()` 之前有充足时间保存草稿、提交日志
16
19
  - **并发互斥锁**:内置 `isDownloading` 单例锁,防止手动触发与开机自检同时争写同一 ASAR 文件
17
- - **智能冷库清道夫**:`maxVersionsToKeep` 自动淘汰最旧历史版本,保留可回滚备份,告别 C 盘无限膨胀
20
+ - **智能冷库清道夫**:`maxVersionsToKeep` 自动淘汰最旧历史版本,保留可回滚备份,告别磁盘无限膨胀
18
21
  - **Pipeline 流式下载**:Node.js `stream/promises pipeline` + `Transform` 管道,内存零泄漏,网络背压自动控制
19
22
  - **零配置版本探测**:默认自动读取 `process.cwd()/package.json` 中的版本号,无需任何额外配置
20
23
  - **独立打包命令**:普通 `npm run build` 不受任何影响,只有 `npm run build:update` 才会触发 ASAR 打包
24
+ - **History 模式 SPA 支持**:内置自定义协议处理器(`app://`),支持 Vue/React History 路由模式,零配置零样板代码
21
25
 
22
26
  ---
23
27
 
@@ -27,7 +31,7 @@
27
31
  npm install electron-updater-for-render
28
32
 
29
33
  # 打包阶段依赖(在渲染层项目中安装)
30
- npm install -D asar
34
+ npm install -D @electron/asar
31
35
  ```
32
36
 
33
37
  ---
@@ -38,7 +42,7 @@ npm install -D asar
38
42
  ┌─────────────────────────────────────────────────────┐
39
43
  │ 渲染层项目(如 Vue 工程) │
40
44
  │ npm run build:update │
41
- │ → 原生 build 命令 调用 CLI打包 ASAR → 生成 latest.json
45
+ │ → 原生 build → CLI 打包 ASAR → 生成 latest.json
42
46
  └───────────────┬─────────────────────────────────────┘
43
47
  │ 上传 dist_updates/ 到服务器
44
48
 
@@ -50,18 +54,30 @@ npm install -D asar
50
54
 
51
55
  ┌─────────────────────────────────────────────────────┐
52
56
  │ Electron 主进程 │
53
- │ updater.checkForUpdatesAndNotify()
54
- │ → 检查版本 → 下载 ASAR → 重启应用
57
+ │ updater.check() → 'idle' | 'available' | 'ready'
58
+ │ → 下载 ASAR → setUpdatePending()
59
+ │ → onStatusChanged 推送 → 渲染层 UI 即时更新 │
60
+ │ → 重启时:应用更新 → app.relaunch() │
55
61
  └─────────────────────────────────────────────────────┘
56
62
  ```
57
63
 
64
+ ### 三态更新生命周期
65
+
66
+ | 状态 | 含义 | UI 行为 |
67
+ |---|---|---|
68
+ | `idle` | 未找到更新 | 显示"检查更新"按钮 |
69
+ | `available` | 发现新版本,尚未下载 | 显示"立即下载"按钮 |
70
+ | `ready` | 更新已下载,等待重启 | 显示"立即重启安装"按钮 |
71
+
72
+ `ready` 状态会**持久化到磁盘**(`current.json`)。即使用户刷新页面或重新打开应用,UI 也能正确显示"立即重启",而不会再次要求下载。
73
+
58
74
  ---
59
75
 
60
76
  ## 🛠️ 完整接入指南
61
77
 
62
78
  ### 第一步 — 渲染工程:配置并声明生成规则
63
79
 
64
- 在您的任何前端项目根目录下(无论 Vite, Webpack,亦或普通网页)新建 `updater.config.ts`:
80
+ 在您的任何前端项目根目录下(无论 ViteWebpack,亦或普通网页)新建 `updater.config.ts`:
65
81
 
66
82
  ```typescript
67
83
  import { defineConfig } from 'electron-updater-for-render/builder'
@@ -123,8 +139,8 @@ rsync -avz dist_updates/ user@your-server:/var/www/auto-updates/
123
139
  ```
124
140
 
125
141
  客户端会依次请求:
126
- - `GET https://your-server.com/latest.json` (获取版本清单)
127
- - `GET https://your-server.com/1.0.2/renderer.asar` (下载更新包)
142
+ - `GET https://your-server.com/latest.json`(获取版本清单)
143
+ - `GET https://your-server.com/1.0.2/renderer.asar`(下载更新包)
128
144
 
129
145
  ---
130
146
 
@@ -148,6 +164,15 @@ const updater = new RenderUpdater({
148
164
  win.webContents.send('updater:before-restart')
149
165
  await new Promise(resolve => setTimeout(resolve, 2000))
150
166
  }
167
+ },
168
+
169
+ // 可选:当更新状态发生变化时(如后台下载完成),主动推送给渲染层
170
+ // 渲染层无需手动检查,按钮会自动切换到"立即重启"
171
+ onStatusChanged: (data) => {
172
+ const [win] = BrowserWindow.getAllWindows()
173
+ if (win) {
174
+ win.webContents.send('updater:status-changed', data)
175
+ }
151
176
  }
152
177
  })
153
178
 
@@ -166,12 +191,16 @@ app.whenReady().then(async () => {
166
191
 
167
192
  // 暴露 IPC 接口,供前端界面主动控制更新流程
168
193
  ipcMain.handle('updater:check', () => updater.check())
169
- ipcMain.handle('updater:download', () =>
170
- updater.download((percent) => {
194
+
195
+ // ⚠️ 重要:第一个参数必须是前端传来的 UpdateInfo 对象,第二个才是进度回调
196
+ // 这样可以避免主进程重复请求 latest.json,也杜绝了参数错位导致的崩溃
197
+ ipcMain.handle('updater:download', async (event, info) =>
198
+ updater.download(info, (percent) => {
171
199
  BrowserWindow.getAllWindows()[0]?.webContents.send('updater:progress', percent)
172
200
  })
173
201
  )
174
- ipcMain.handle('updater:install', () => {
202
+
203
+ ipcMain.on('updater:install', () => {
175
204
  app.relaunch()
176
205
  app.quit()
177
206
  })
@@ -187,8 +216,12 @@ import { contextBridge, ipcRenderer } from 'electron'
187
216
 
188
217
  contextBridge.exposeInMainWorld('updater', {
189
218
  check: () => ipcRenderer.invoke('updater:check'),
190
- download: () => ipcRenderer.invoke('updater:download'),
191
- install: () => ipcRenderer.invoke('updater:install'),
219
+
220
+ // 将 check() 返回的 UpdateInfo 对象透传给主进程
221
+ // 主进程凭此对象直接下载,无需重新请求 latest.json
222
+ download: (info?: any) => ipcRenderer.invoke('updater:download', info),
223
+
224
+ install: () => ipcRenderer.send('updater:install'),
192
225
 
193
226
  onProgress: (callback: (percent: number) => void) => {
194
227
  const fn = (_: any, p: number) => callback(p)
@@ -200,75 +233,170 @@ contextBridge.exposeInMainWorld('updater', {
200
233
  const fn = () => callback()
201
234
  ipcRenderer.on('updater:before-restart', fn)
202
235
  return () => ipcRenderer.removeListener('updater:before-restart', fn)
236
+ },
237
+
238
+ // 监听主进程的主动状态推送
239
+ // 当后台下载完成(如用户点"稍后安装"之后)时,UI 会自动更新到"立即重启"
240
+ onStatusChanged: (callback: (data: { status: string; version: string }) => void) => {
241
+ const fn = (_: any, data: any) => callback(data)
242
+ ipcRenderer.on('updater:status-changed', fn)
243
+ return () => ipcRenderer.removeListener('updater:status-changed', fn)
203
244
  }
204
245
  })
205
246
  ```
206
247
 
207
248
  ---
208
249
 
209
- ### 第六步 — 渲染层 UI(Vue 示例)
250
+ ### 第六步 — 渲染层 UI(Vue 完整示例)
251
+
252
+ 以下示例展示了完整的三态更新流程,包含:
253
+ - **启动自动检测**——无需用户手动点击即可感知更新
254
+ - **三态精准识别**——根据 `check()` 返回的 `status` 正确显示"下载"或"重启"
255
+ - **实时状态同步**——后台下载完成后,主进程推送通知,UI 立即刷新
210
256
 
211
257
  ```vue
212
258
  <script setup lang="ts">
213
259
  import { ref, onMounted, onUnmounted } from 'vue'
214
260
 
215
- const status = ref('')
261
+ // 状态:'idle' | 'checking' | 'available' | 'downloading' | 'ready' | 'no-update'
262
+ const status = ref<'idle' | 'checking' | 'available' | 'downloading' | 'ready' | 'no-update'>('idle')
216
263
  const progress = ref(0)
217
- const hasUpdate = ref(false)
264
+ const newVersion = ref('')
265
+ const updateInfo = ref<any>(null) // 存储完整的 UpdateInfo,供 download() 使用
266
+ const errMsg = ref('')
218
267
 
219
268
  let removeProgress: (() => void) | null = null
220
269
  let removeRestart: (() => void) | null = null
270
+ let removeStatusChanged: (() => void) | null = null
221
271
 
222
- onMounted(() => {
223
- // 监听重启通知,提前保存状态
224
- removeRestart = window.updater.onBeforeRestart(() => {
225
- status.value = '即将重启,保存状态中...'
226
- localStorage.setItem('draft', JSON.stringify({ /* 你的状态 */ }))
227
- })
228
- })
229
-
230
- onUnmounted(() => {
231
- removeProgress?.()
232
- removeRestart?.()
233
- })
272
+ const getUpdater = () => (window as any).updater
234
273
 
235
- // 手动检查更新
274
+ // ── 检查更新 ───────────────────────────────────────────────────────────────
275
+ // check() 返回 { status: 'idle' | 'available' | 'ready', version, info }
276
+ // 若状态为 'ready',说明更新已下载完毕只待重启,直接跳过下载流程
236
277
  const checkUpdate = async () => {
237
- status.value = '检查中...'
238
- const result = await window.updater.check()
239
- if (result.updateAvailable) {
240
- hasUpdate.value = true
241
- status.value = `发现新版本:v${result.version}`
242
- } else {
243
- status.value = '当前已是最新版本'
278
+ const updater = getUpdater()
279
+ if (!updater) return
280
+
281
+ status.value = 'checking'
282
+ try {
283
+ const res = await updater.check()
284
+ if (res.updateAvailable) {
285
+ newVersion.value = res.version
286
+ updateInfo.value = res.info
287
+ // 精确识别:'ready' 直接进入重启状态,不再显示下载按钮
288
+ status.value = res.status === 'ready' ? 'ready' : 'available'
289
+ } else {
290
+ status.value = 'no-update'
291
+ }
292
+ } catch (err: any) {
293
+ errMsg.value = '检查失败: ' + err.message
294
+ status.value = 'idle'
244
295
  }
245
296
  }
246
297
 
247
- // 手动下载更新
298
+ // ── 下载更新 ───────────────────────────────────────────────────────────────
299
+ // 将 check() 返回的 info 对象传给 download(),避免主进程重复请求 latest.json
248
300
  const downloadUpdate = async () => {
249
- status.value = '下载中...'
250
- removeProgress = window.updater.onProgress((p) => {
251
- progress.value = p
252
- status.value = `下载中 ${p.toFixed(0)}%`
301
+ const updater = getUpdater()
302
+ if (!updater) return
303
+
304
+ status.value = 'downloading'
305
+ progress.value = 0
306
+
307
+ removeProgress = updater.onProgress((percent: number) => {
308
+ progress.value = percent
253
309
  })
254
- await window.updater.download()
255
- removeProgress?.()
256
- status.value = '下载完成,可以安装'
310
+
311
+ try {
312
+ await updater.download(updateInfo.value)
313
+ status.value = 'ready'
314
+ } catch (err: any) {
315
+ errMsg.value = '下载失败: ' + err.message
316
+ status.value = 'available'
317
+ } finally {
318
+ removeProgress?.()
319
+ }
257
320
  }
258
321
 
259
- // 安装并重启
322
+ // ── 安装并重启 ─────────────────────────────────────────────────────────────
260
323
  const installUpdate = () => {
261
- window.updater.install()
324
+ getUpdater()?.install()
325
+ }
326
+
327
+ // ── 监听主进程主动推送的状态变更 ───────────────────────────────────────────
328
+ // 场景:用户点击"稍后安装"后,日后主进程 onStatusChanged 触发时
329
+ // UI 无需用户做任何操作,按钮自动变为"立即重启"
330
+ const initStatusListener = () => {
331
+ const updater = getUpdater()
332
+ if (updater?.onStatusChanged) {
333
+ return updater.onStatusChanged((data: { status: string; version: string }) => {
334
+ if (data.status === 'ready') {
335
+ status.value = 'ready'
336
+ newVersion.value = data.version
337
+ }
338
+ })
339
+ }
340
+ return null
262
341
  }
342
+
343
+ // ── 监听重启前通知 ─────────────────────────────────────────────────────────
344
+ const initRestartListener = () => {
345
+ const updater = getUpdater()
346
+ if (updater?.onBeforeRestart) {
347
+ return updater.onBeforeRestart(() => {
348
+ // 在此保存未提交的表单状态
349
+ console.log('应用即将重启,请保存状态...')
350
+ })
351
+ }
352
+ return null
353
+ }
354
+
355
+ // ── 生命周期 ───────────────────────────────────────────────────────────────
356
+ onMounted(() => {
357
+ removeStatusChanged = initStatusListener()
358
+ removeRestart = initRestartListener()
359
+
360
+ // 启动后 2 秒自动检查,不阻塞首屏渲染
361
+ setTimeout(checkUpdate, 2000)
362
+ })
363
+
364
+ onUnmounted(() => {
365
+ removeProgress?.()
366
+ removeRestart?.()
367
+ removeStatusChanged?.()
368
+ })
263
369
  </script>
264
370
 
265
371
  <template>
266
- <div>
267
- <p>{{ status }}</p>
268
- <progress :value="progress" max="100" v-if="progress > 0" />
269
- <button @click="checkUpdate">检查更新</button>
270
- <button @click="downloadUpdate" v-if="hasUpdate">立即下载</button>
271
- <button @click="installUpdate">安装并重启</button>
372
+ <div class="updater">
373
+ <div v-if="errMsg" class="error">{{ errMsg }}</div>
374
+
375
+ <!-- 无活跃更新任务 -->
376
+ <button v-if="status === 'idle' || status === 'no-update'" @click="checkUpdate">
377
+ {{ status === 'no-update' ? '当前已是最新,重新检查' : '主动检查新版本' }}
378
+ </button>
379
+
380
+ <!-- 检查中 -->
381
+ <span v-if="status === 'checking'">正在拉取更新配置...</span>
382
+
383
+ <!-- 发现新版本,尚未下载 -->
384
+ <div v-if="status === 'available'">
385
+ <p>🔥 发现新版本: v{{ newVersion }}</p>
386
+ <button @click="downloadUpdate">立即下载并缓存</button>
387
+ </div>
388
+
389
+ <!-- 下载中 -->
390
+ <div v-if="status === 'downloading'">
391
+ <p>正在下载... {{ progress.toFixed(1) }}%</p>
392
+ <progress :value="progress" max="100" />
393
+ </div>
394
+
395
+ <!-- 更新已就绪(刷新页面后此状态依然保持)-->
396
+ <div v-if="status === 'ready'">
397
+ <p>✅ v{{ newVersion }} 已就绪</p>
398
+ <button @click="installUpdate">立即重启并安装</button>
399
+ </div>
272
400
  </div>
273
401
  </template>
274
402
  ```
@@ -287,7 +415,7 @@ const installUpdate = () => {
287
415
  - **前端 (Router)**:使用 `createWebHashHistory()`。
288
416
 
289
417
  ### 2. History 模式(企业级方案)
290
- 支持美观的 URL 和标准浏览器重载行为。由于 `file://` 不支持标准路解析,本库会自动启用自定义协议(默认 `app://`)进行转发。
418
+ 支持美观的 URL 和标准浏览器重载行为。由于 `file://` 不支持标准路由解析,本库会自动启用自定义协议(默认 `app://`)进行转发。
291
419
 
292
420
  - **主进程**:
293
421
  ```typescript
@@ -298,9 +426,9 @@ const installUpdate = () => {
298
426
  protocol: 'my-app' // 可选:自定义协议名 (默认 'app')
299
427
  })
300
428
  ```
301
- > 💡 **全自动化:** 协议注册和特权赋权逻辑已在库内部**全自动处理**,您无需在顶层编写任何 `registerSchemesAsPrivileged` 等繁琐代码。
429
+ > 💡 **全自动化**:协议注册和特权赋权逻辑已在库内部**全自动处理**,您无需编写任何 `registerSchemesAsPrivileged` 等繁琐代码。
302
430
 
303
- - **前端构建工具 (Vite, Webpack, 等)**:**关键配置** —— 您必须将构建工具的 **公共路径 (Public Path / Base)** 设置为 `/`。这是为了确保生成的资源引用(JS/CSS/图片)为绝对路径,防止在深层路由(如 `/home/settings`)下刷新页面时出现资源加载 404。
431
+ - **前端构建工具(ViteWebpack 等)**:**关键配置** —— 必须将 **公共路径 (Public Path / Base)** 设置为 `/`,确保资源引用为绝对路径,防止在深层路由下刷新出现 404。
304
432
  - **Vite**:`base: '/'`
305
433
  - **Webpack**:`output.publicPath: '/'`
306
434
  - **前端路由 (Router)**:使用 `createWebHistory()`。
@@ -313,7 +441,7 @@ const installUpdate = () => {
313
441
 
314
442
  | 参数 | 类型 | 必填 | 说明 |
315
443
  |---|---|---|---|
316
- | `outDir` | `string` | ✅ | Vite 构建输出目录 |
444
+ | `outDir` | `string` | ✅ | 构建输出目录 |
317
445
  | `updatesDir` | `string` | — | 更新包输出目录,默认 `./dist_updates` |
318
446
  | `version` | `string` | — | 显式指定版本号,优先级高于 `packageJsonPath` |
319
447
  | `packageJsonPath` | `string` | — | 自定义 `package.json` 路径,不填则自动读取 `process.cwd()/package.json` |
@@ -331,14 +459,30 @@ const installUpdate = () => {
331
459
  | `publicKey` | `string` | — | RSA 公钥(PEM 格式),用于验证 ASAR 签名 |
332
460
  | `autoDownload` | `boolean` | — | 发现更新后自动下载,不弹窗询问。默认 `false` |
333
461
  | `autoPrompt` | `boolean` | — | 使用内置原生弹窗引导更新。默认 `true` |
334
- | `maxVersionsToKeep` | `number` | — | 本地保留的旧版本数量(不含当前),默认 `2` |
462
+ | `maxVersionsToKeep` | `number` | — | 本地保留的旧版本数量,默认 `2` |
463
+ | `routerMode` | `'hash' \| 'history'` | — | 路由模式选择。默认 `'hash'` |
464
+ | `protocol` | `string` | — | History 模式下的自定义协议名。默认 `'app'` |
335
465
  | `onUpdateAvailable` | `function` | — | 检测到更新时的自定义钩子:`(info, doDownload) => void` |
336
466
  | `onDownloadProgress` | `function` | — | 下载进度回调:`(percent: number) => void` |
337
467
  | `onDownloadComplete` | `function` | — | 下载完成钩子:`(info, doInstall) => void` |
468
+ | `onStatusChanged` | `function` | — | 状态变更推送钩子:`(data: { status: 'ready' \| 'available' \| 'idle', version: string }) => void` |
338
469
  | `onError` | `function` | — | 错误回调:`(error: Error) => void` |
339
470
  | `onBeforeRestart` | `async function` | — | `app.relaunch()` 前的异步钩子,可用于保存状态 |
340
- | `routerMode` | `'hash' \| 'history'` | — | 路由模式选择。默认 `'hash'` |
341
- | `protocol` | `string` | — | History 模式下的自定义协议名。默认 `'app'` |
471
+
472
+ ### `RenderUpdater` 实例方法
473
+
474
+ | 方法 | 返回值 | 说明 |
475
+ |---|---|---|
476
+ | `check()` | `Promise<CheckResult>` | 检查更新。返回 `{ updateAvailable, status, version, info }`,其中 `status` 为 `'idle' \| 'available' \| 'ready'` |
477
+ | `download(info?, onProgress?)` | `Promise<void>` | 下载更新。将 `check()` 返回的 `info` 传入,可避免重复请求 latest.json |
478
+ | `getLoadUrl()` | `string` | 返回最新 ASAR 的加载地址(`app://renderer/` 或 `file://...`),未安装任何更新时返回空字符串 |
479
+ | `installAndRestart()` | `Promise<void>` | 应用待安装更新并重启应用 |
480
+ | `checkForUpdatesAndNotify()` | `Promise<void>` | 一键式:检查 + 弹窗 + 下载 + 安装,使用内置原生弹窗 |
481
+ | `setUpdatePending(version)` | `void` | 将某版本标记为已下载待重启,同时触发 `onStatusChanged` |
482
+ | `useVersion(version)` | `void` | 立即切换磁盘和内存双重版本指针 |
483
+ | `activeVersion` | `string`(getter) | 当前内存中正在运行的版本(本次会话) |
484
+ | `pendingVersion` | `string`(getter) | 磁盘上最新已下载版本 |
485
+ | `isUpdatePending` | `boolean`(getter) | 若 `pendingVersion > activeVersion` 则为 `true` |
342
486
 
343
487
  ---
344
488
 
@@ -347,7 +491,7 @@ const installUpdate = () => {
347
491
  当线上出现资损、安全漏洞、核心功能崩溃等 P0 级故障时,通过 `forceUpdate` 剥夺用户的拒绝权:
348
492
 
349
493
  ```typescript
350
- // updater.config.ts (加入此行)
494
+ // updater.config.ts
351
495
  export default defineConfig({
352
496
  outDir: './dist',
353
497
  forceUpdate: 'prompt' // 或 'silent'
@@ -356,7 +500,7 @@ export default defineConfig({
356
500
 
357
501
  | 模式 | 行为 |
358
502
  |---|---|
359
- | `'prompt'` | 弹出系统级警告弹窗,**只有一个确认按钮**,用户无法取消或跳过。点击后立即开始下载并重启 |
503
+ | `'prompt'` | 弹出系统级警告弹窗,**只有一个确认按钮**,用户无法取消或跳过,点击后立即开始下载并重启 |
360
504
  | `'silent'` | 完全静默:后台自动下载、自动安装、自动重启,用户毫无感知 |
361
505
 
362
506
  > ⚠️ 本功能会**永久剥夺用户本次的延迟权**,请仅在真正的 P0 级生产事故中使用。