Updater 版本更新
Rydeen 客户端更新组件(整包升级 + 资源热修复补丁)。
统一封装移动端(Android/iOS)基于 Capgo Capacitor Updater(@capgo/capacitor-updater)的资源更新能力,并预留 Electron 整包 + 文件级补丁的扩展位,以 一个接口 IUpdateService 打通多端更新。
功能特性概览
- 跨平台统一接口:通过
IUpdateService抽象出平台无关的更新能力,Angular 中通过UPDATER_SERVICE注入。 - 版本检查:从
checkDomain拉取最新VersionMetadata,支持static/dynamic两种模式。 - 下载与安装(移动端):基于 Capgo 的
download+set/next实现资源包的下载与切换。 - 事件链路:封装 Capgo 的下载、更新可用、无需更新、完成、失败等事件,并支持 App 的
pause/resume。 - 静默 / 非静默检查:
checkVersion(silence)支持静默自动升级或仅返回版本信息,由业务决定弹窗。 - 定时检查:
checkVersionTimer()按intervalHours周期检查。 - 回滚能力:
rollbackPrev()/rollbackOrigin()支持回滚到上一版本或原始版本。 - 本地版本管理:基于
@capacitor-rydeen/preferences存储本地版本,getLocalVersion()统一读取。
安装与依赖
npm install @rydeen/updater
@rydeen/updater通过peerDependencies声明所需三方依赖,不会自动安装,需要在业务工程中手动安装到自己的package.json中。
-
必需依赖(移动端能力)
@capgo/capacitor-updater@capacitor-rydeen/preferences@capacitor/app@capacitor/dialog(用于版本更新相关弹窗等交互)
-
必需依赖(Electron 整包 + 资源补丁能力)
electron-updater
典型安装方式如下:
# 仅集成移动端(Android / iOS)更新能力
npm install @rydeen/updater \
@capgo/capacitor-updater \
@capacitor-rydeen/preferences \
@capacitor/app \
@capacitor/dialog
# 同时集成移动端 + Electron 桌面整包更新
npm install @rydeen/updater \
@capgo/capacitor-updater \
@capacitor-rydeen/preferences \
@capacitor/app \
@capacitor/dialog \
electron-updater
上述依赖在
@rydeen/updater的package.json中以peerDependencies声明,
业务应用需要根据自己的实际平台(仅移动端 / 移动端 + Electron)按需安装,否则运行时会出现“模块未找到”或原生插件未注册等错误。
快速开始(5 分钟上手)
适用场景:Angular + Capacitor 移动端项目,想先跑通一个“手动检查更新”的最小实现。
- 第 1 步:安装依赖
npm install @rydeen/updater @capgo/capacitor-updater @capacitor-rydeen/preferences @capacitor/app
- 第 2 步:在
app.config.ts中接入provideRydeenUpdater
import { ApplicationConfig } from '@angular/core'
import { provideRydeenUpdater, type RydeenUpdaterOptions } from '@rydeen/updater'
import { environment } from './environments/environment'
const updaterOptions: RydeenUpdaterOptions = {
updateMode: 'dynamic',
enable: true,
autoCheck: false,
checkDomain: environment.checkPatchUrl,
downloadDomain: environment.patchDownloadDomain,
intervalHours: 24,
originVersion: {
hasAppUpdate: false,
hasAssetsUpdate: false,
assetsVersion: environment.assetsVersion,
checksum: ''
}
}
export const appConfig: ApplicationConfig = {
providers: [
// ...
...provideRydeenUpdater(updaterOptions)
]
}
- 第 3 步:创建一个最简单的更新服务并在页面中调用
// app-update.service.ts
import { inject, Injectable } from '@angular/core'
import { UPDATER_SERVICE, type IUpdateService, type VersionMetadata } from '@rydeen/updater'
@Injectable({ providedIn: 'root' })
export class AppUpdateService {
private readonly updater = inject<IUpdateService>(UPDATER_SERVICE)
async init(): Promise<void> {
await this.updater.registerEvents()
}
async checkForUpdates(): Promise<void> {
const meta: VersionMetadata = await this.updater.checkVersion(false)
if (!meta.hasAssetsUpdate) {
console.info('[版本升级]', '已经是最新版本。')
return
}
const plan = await this.updater.download(meta)
await this.updater.upgrade(plan, true)
}
}
// 某个组件中
import { Component, OnInit, inject } from '@angular/core'
import { AppUpdateService } from './core/services/app-update.service'
@Component({ /* ... */ })
export class SettingsPage implements OnInit {
private readonly appUpdate = inject(AppUpdateService)
async ngOnInit(): Promise<void> {
await this.appUpdate.init()
}
async onCheckUpdateClick(): Promise<void> {
await this.appUpdate.checkForUpdates()
}
}
上述代码让你在设置页加一个“检查更新”按钮即可完成最小集成。
若需要更复杂的弹窗、统计与可控流程,请参见后文的「业务封装模板:AppUpdateService(带弹窗与可控流程)」章节。
平台支持与能力矩阵
| 平台 | 版本检查 (checkVersion) | 资源热修复(资产更新) | 整包升级(应用版本) | 当前实现说明 |
|---|---|---|---|---|
| Android | ✅ | ✅(基于 Capgo 资源包) | ⚠️ 仅通过 hasAppUpdate 标记,由业务决定跳转商店或其他逻辑 | 使用 CapacitorUpdateService,完整事件链路与下载 / 升级能力 |
| iOS | ✅ | ✅(基于 Capgo 资源包) | ⚠️ 同 Android,需自行实现 App Store 升级 | 同 Android,共用 CapacitorUpdateService |
| Electron | ✅(通过 IPC 调用) | ✅ 默认 ZIP 资源补丁(可传回调覆盖) | ✅ 基于 electron-updater 的整包升级(在主进程实现) | 渲染进程使用 ElectronUpdateService,主进程通过 registerRydeenElectronUpdaterIpc 内置整包 + 资源补丁逻辑 |
| Web | ✅(始终返回本地版本) | ❌ 不支持 | ❌ 不支持 | NoopUpdateService,仅用于保持接口一致性 |
- 关键点
- 移动端实际下载 / 升级由 Capgo 完成,
@rydeen/updater做流程编排与事件封装。 - Electron 目前为占位实现,仅提供统一的
IUpdateService接口和日志提示,真正的整包 / 文件级热修复需要在 Electron 主进程 + 渲染进程中按项目规范接入。 - Web 仅用于开发阶段占位,不会执行任何真实更新逻辑。
- 移动端实际下载 / 升级由 Capgo 完成,
我应该先看哪一节?
- 只做移动端(Android / iOS):
- 先完成上面的「快速开始(5 分钟上手)」;
- 然后阅读「Angular 集成方式(推荐)」和「业务封装模板:AppUpdateService(带弹窗与可控流程)」。
- 有 Electron 桌面端:
- 在完成移动端集成后,再阅读「跨端完整 Sample:移动端 + Electron」和其中的 Electron preload / 主进程示例。
- 暂时不想深入类型细节:
- 可以跳过「核心数据结构与标志位含义」「
UpdateOptions」「核心接口IUpdateService」等章节; - 等需要自定义后端协议或版本策略时再回来查阅。
- 可以跳过「核心数据结构与标志位含义」「
核心数据结构与标志位含义
本节为 进阶参考,用于理解和自定义版本协议。
如果你只是想先跑通基础能力,可以暂时跳过,先看后面的「Angular 集成方式」和「业务封装模板」。
VersionMetadata —— 云端 / 本地版本元数据
用于描述“当前应用版本”和“云端最新版本”的关键信息:
hasAppUpdate: boolean- 含义:是否有“应用级”更新(例如 APK / IPA / Electron 安装包升级)。
- 典型用法:当为
true时,业务可提示用户去应用商店 / 下载页,或在 Electron 中触发整包升级流程。
hasAssetsUpdate: boolean- 含义:是否有“客户端资源包”更新(静态资源 / 客户端代码)。
- 典型用法:移动端走 Capgo 资源更新;Electron 走文件级补丁或 asar 资源替换。
assetsVersion: string- 含义:资源包版本号(通常与 Capgo Bundle 版本或补丁版本一一对应)。
- 典型用法:用于比对本地 / 云端资源版本,或作为下载参数。
newVersion?: string- 含义:应用的新版本号(如
1.2.0),通常对应原生应用版本。 - 典型用法:在 UI 中展示“发现新版本 x.x.x”,或与本地
App.getInfo().version对比。
- 含义:应用的新版本号(如
filename?: string- 含义:资源包的文件名(如
bundle-1.2.0.zip),与downloadDomain组合成下载地址时使用。
- 含义:资源包的文件名(如
checksum: string- 含义:资源包的校验值(通常是 HASH),用于防止下载包被篡改。
sign?: string- 含义:服务端返回的“加密下载地址”字段,内部会先解码出真实 URL;若不存在则退化为
downloadDomain + filename。
- 含义:服务端返回的“加密下载地址”字段,内部会先解码出真实 URL;若不存在则退化为
总结:
hasAppUpdate控制“是否弹窗提示整包升级(跳转商店 / Electron 全量更新)”;hasAssetsUpdate控制“是否执行资源热修复(Capgo 资源包 / Electron 文件补丁)”;assetsVersion和checksum用于版本比对与下载校验;sign/filename控制资源下载地址来源。
UpdatePlan —— 单次更新任务结果
由 download() 返回,描述一次“资源下载任务”的最终结果:
hasUpdate: boolean- 表示本次请求是否确实有可用更新(通常等同于
VersionMetadata.hasAssetsUpdate)。
- 表示本次请求是否确实有可用更新(通常等同于
status: BundleStatus- 更新过程的状态枚举,见下文
BundleStatus。
- 更新过程的状态枚举,见下文
message?: string- 人类可读的提示信息,如“下载成功”“下载出错,请重试”等。
code: MessageCode- 机器可读的消息代码,便于客户端根据不同错误码做差异化处理。常见示例:
DOWNLOAD_SUCCESS:下载成功;DOWNLOAD_ERROR:下载失败;- 其他业务自定义码。
- 机器可读的消息代码,便于客户端根据不同错误码做差异化处理。常见示例:
data?: BundleInfo- 对应 Capgo 的
BundleInfo对象(仅移动端可用),包含 bundle id / version / status 等信息。
- 对应 Capgo 的
metadata: VersionMetadata- 与本次下载对应的版本元数据。
BundleStatus —— 下载状态枚举
SUCCESS:下载 / 准备成功,可以进入upgrade()流程。ERROR:下载或校验失败,应提示用户或允许重试。PENDING:任务已创建但尚未开始(当前实现中较少使用,预留扩展)。DOWNLOADING:正在下载中,一般配合download事件中的进度使用。
UpdateOptions —— 组件初始化参数
UpdateOptions 决定了整个更新系统的工作模式:
updateMode: 'static' | 'dynamic'static:通过GET checkDomain获取版本信息,常用于静态 JSON 或 CDN 文件。dynamic:通过POST checkDomain携带应用信息(包名、build、当前资源版本)获取个性化版本策略。
enable: boolean- 是否启用热更新能力。
- 为
false时,checkVersion()直接返回一个“无更新”的VersionMetadata。
autoCheck: boolean- 是否开启定时自动检查。
- 配合
checkVersionTimer()使用。
checkDomain: string- 版本检查接口地址(HTTP/HTTPS)。
downloadDomain: string- 资源包基础下载地址。当
VersionMetadata.sign不存在时,使用downloadDomain + '/' + filename拼出最终 URL。
- 资源包基础下载地址。当
statisticsDomain?: string- 统计上报接口地址;若设置,
report()会向该地址上报设备 / 版本信息。
- 统计上报接口地址;若设置,
intervalHours: number- 自动检查间隔(小时为单位)。
originVersion: VersionMetadata- 应用原始版本信息(即未打补丁时的初始版本)。
- 在本地无任何版本记录时作为默认“本地版本”写入 Preferences。
- 事件回调(均为可选)
download?: DownloadProcessEvent:下载进度回调。noNeed?: NoNeedEvent:无需更新。updateAvailable?: UpdateAvailableEvent:发现可用更新。downloadComplete?: DownloadCompleteEvent:下载完成。updateFailed?: UpdateFailedEvent:更新失败。downloadFailed?: DownloadFailedEvent:下载失败。appReloaded?: AppReloadedEvent:应用被重载。appReady?: AppReadyEvent:Capgo 通知“应用准备就绪”。
设计意图:通过
UpdateOptions将“版本策略、网络入口、统计、交互回调”集中配置,运行时只关注IUpdateService的方法调用。
核心接口 IUpdateService
IUpdateService 是跨平台统一的更新接口,主要方法含义如下(伪代码签名):
interface IUpdateService {
registerEvents(): Promise<void>
checkVersion(silence?: boolean): Promise<VersionMetadata>
download(cloudVersion: VersionMetadata): Promise<UpdatePlan>
upgrade(plan: UpdatePlan, updateNow?: boolean): Promise<void>
rollbackPrev(): Promise<void>
rollbackOrigin(): Promise<void>
getLocalVersion(): Promise<VersionMetadata>
setAutoCheck(autoCheck: boolean): void
setIntervalHours(hours: number): void
checkVersionTimer(): void
}
- 移动端实现:
CapacitorUpdateService implements IUpdateService - Electron 占位实现:
ElectronUpdateService implements IUpdateService(内部多为日志 + 透传) - Web / 其他平台:
NoopUpdateService implements IUpdateService
API 说明(以 CapacitorUpdateService / 移动端为例)
-
new CapacitorUpdateService(options: UpdateOptions)- 构造函数,仅保存配置,不会自动发起网络请求。
-
registerEvents()- 注册 Capgo 的所有相关事件(download / noNeedUpdate / updateAvailable / downloadComplete / updateFailed / downloadFailed / appReloaded / appReady)。
- 注册 App 的
pause/resume,在resume时重新启动定时检查。 - 内部调用
CapacitorUpdater.notifyAppReady()。
-
checkVersion(silence?: boolean)- 调用内部
check(),根据updateMode请求checkDomain。 silence === false:仅返回VersionMetadata,不做任何下载或升级。silence === true:当hasAssetsUpdate === true时,会自动执行download()+upgrade()实现“静默更新”。
- 调用内部
-
download(cloudVersion: VersionMetadata)- 根据
cloudVersion.sign或downloadDomain + filename计算下载 URL。 - 调用 Capgo 的
download,返回封装好的UpdatePlan。
- 根据
-
upgrade(plan: UpdatePlan, updateNow = true)- 将下载好的资源设置为当前版本:
updateNow === true:立即切换(CapacitorUpdater.set)。updateNow === false:下次启动生效(CapacitorUpdater.next)。
- 写入本地 Preferences 的
localVersion。
- 将下载好的资源设置为当前版本:
-
rollbackPrev()/rollbackOrigin()- 封装 Capgo 的
reset能力,实现到上一成功版本或原始版本的回滚。
- 封装 Capgo 的
-
getLocalVersion()- 从 Preferences 读取
localVersion;不存在时返回originVersion。
- 从 Preferences 读取
-
setAutoCheck()/setIntervalHours()/checkVersionTimer()- 控制自动检查开关与间隔,并通过
setInterval周期调用checkVersion(true)。
- 控制自动检查开关与间隔,并通过
Angular 集成方式(推荐)
在 Angular 应用中推荐通过 provideRydeenUpdater + UPDATER_SERVICE 统一注入:
// app.config.ts
import { ApplicationConfig } from '@angular/core'
import {
provideRydeenUpdater,
UPDATER_SERVICE,
type RydeenUpdaterOptions,
type IUpdateService
} from '@rydeen/updater'
const hotfixOptions: RydeenUpdaterOptions = {
updateMode: 'static',
enable: true,
autoCheck: true,
checkDomain: 'https://your-server.com/version',
downloadDomain: 'https://your-server.com/assets',
intervalHours: 24,
originVersion: {
hasAppUpdate: false,
hasAssetsUpdate: false,
assetsVersion: '1.0.0',
checksum: ''
}
}
export const appConfig: ApplicationConfig = {
providers: [
// ...
...provideRydeenUpdater(hotfixOptions)
]
}
在业务服务 / 组件中使用:
import { inject, Injectable } from '@angular/core'
import { UPDATER_SERVICE, type IUpdateService } from '@rydeen/updater'
@Injectable({ providedIn: 'root' })
export class AppUpdateService {
private readonly updater = inject<IUpdateService>(UPDATER_SERVICE)
async init() {
await this.updater.registerEvents()
this.updater.checkVersionTimer()
}
async manualCheck() {
const meta = await this.updater.checkVersion(false)
if (meta.hasAssetsUpdate) {
const plan = await this.updater.download(meta)
await this.updater.upgrade(plan, true)
}
}
}
业务封装模板:AppUpdateService(带弹窗与可控流程)
在实际项目中,通常会在插件之上再封装一层业务服务,例如 AppUpdateService:
- 对外只暴露少量简单方法(
initial、silentlyCheckForUpdates、checkForUpdates、reportStatisticInfo等); - 内部统一处理版本检查、统计上报、进度上报、弹窗确认、是否立即重启等细节;
- 其它模块只需要依赖这个业务服务即可,不需要直接操作底层
IUpdateService。
下面给出一个推荐的封装模板,开发者可以直接复制,根据自己项目结构调整 environment 与 commons 路径即可:
// app.config.ts - 与旧项目类似,从环境变量中读取配置
import { ApplicationConfig } from '@angular/core'
import { provideRydeenUpdater, type RydeenUpdaterOptions } from '@rydeen/updater'
import { environment } from '../environments/environment'
const updaterOptions: RydeenUpdaterOptions = {
updateMode: 'dynamic',
enable: true,
autoCheck: true,
checkDomain: environment.checkPatchUrl,
downloadDomain: environment.patchDownloadDomain,
statisticsDomain: environment.versionReportUrl,
intervalHours: 24,
originVersion: {
hasAppUpdate: false,
hasAssetsUpdate: false,
assetsVersion: environment.assetsVersion,
checksum: ''
}
}
export const appConfig: ApplicationConfig = {
providers: [
// ...
...provideRydeenUpdater(updaterOptions)
]
}
// app.update.service.ts - 业务层封装(带弹窗、可控流程)
import { Injectable, inject } from '@angular/core'
import {
UPDATER_SERVICE,
type IUpdateService,
type BundleInfo,
BundleStatus,
type UpdatePlan,
type VersionMetadata
} from '@rydeen/updater'
import { environment } from '../environments/environment'
import { ToastDuration, Tools } from '@capacitor-rydeen/tools'
import { Capacitor } from '@capacitor/core'
import { LocalStorage } from '../commons/local.storage'
import { CacheKey } from '../commons/cache.key'
@Injectable({ providedIn: 'root' })
export class AppUpdateService {
private readonly updater = inject<IUpdateService>(UPDATER_SERVICE)
// 1. 初始化入口:启动时调用一次
async initial(): Promise<void> {
await this.registerEvents()
// 如需自动检查,可在此开启定时任务:
// this.updater.setAutoCheck(true)
// this.updater.setIntervalHours(24)
// this.updater.checkVersionTimer()
}
// 2. 对外暴露获取本地版本
async getLocalVersion(): Promise<VersionMetadata> {
return this.updater.getLocalVersion()
}
async registerEvents(): Promise<void> {
await this.updater.registerEvents()
}
/**
* 下载进度事件(当开始下载更新包的时候触发的事件,通常用于显示更新进度)
*/
private downloadProcessEvent(percent: number, bundle: BundleInfo): void {
// 这里可以接入你自己的事件总线 / 全局 Loading 等
console.info('[版本升级][下载进度事件]', percent, bundle)
}
private downloadFailedEvent(version: string): void {
console.error('[版本升级][下载失败]', version)
}
private updateAvailableEvent(bundle: BundleInfo): void {
console.info('[版本升级][有可用更新]', bundle)
}
private updateFailedEvent(bundle: BundleInfo): void {
console.error('[版本升级][更新失败]', bundle)
}
private appReloadedEvent(): void {
console.info('[版本升级][应用重载]')
}
private appReadyEvent(status: string, bundle: BundleInfo): void {
console.info('[版本升级][应用准备就绪]', status, bundle)
}
/**
* 上报统计信息:24 小时节流一次
*/
async reportStatisticInfo(): Promise<void> {
console.log('[版本升级]', '版本统计上报')
const timestamp = await LocalStorage.getItem<number>(CacheKey.LAST_STATISTICS_TIME)
if (timestamp != null && new Date().getTime() - timestamp < 24 * 60 * 60 * 1000) {
return
}
const deviceId = await LocalStorage.getItem<string>(CacheKey.DEVICE_ID)
if (deviceId == null) {
console.error('设备ID为空,无法上报设备信息。')
return
}
console.log('[版本升级]', `设备ID:${deviceId}`)
const ver = await Tools.getClientVersionInfo()
ver.webviewVersion = !ver.webviewVersion ? '' : ver.webviewVersion
const statisticInfo = {
deviceId,
platform: Capacitor.getPlatform(),
osVersion: ver.osVersion,
osLanguage: navigator.language,
webviewVersion: ver.webviewVersion,
userAgent: navigator.userAgent
}
try {
await fetch(environment.versionReportUrl, {
method: 'POST',
body: JSON.stringify(statisticInfo),
headers: {
cache: 'no-cache',
'Content-Type': 'application/json'
}
})
await LocalStorage.setItem(CacheKey.LAST_STATISTICS_TIME, new Date().getTime())
console.log('[版本升级]', '版本统计上报完成')
} catch (e) {
console.error('[版本升级] 版本统计上报失败', e)
}
}
/**
* 静默检查更新:不弹窗,适合启动时、后台场景
*/
async silentlyCheckForUpdates(): Promise<void> {
const meta = await this.updater.checkVersion(false)
if (!meta.hasAssetsUpdate) {
console.info('[版本升级]', '没有新的资源包版本。')
return
}
console.info('[版本升级]', `检查到新版本,开始下载…… (${JSON.stringify(meta)})`)
const plan = await this.download(meta)
if (plan.status === BundleStatus.ERROR) {
console.error('[版本升级]', '静默下载失败', plan.message)
return
}
await this.apply(plan, false) // 静默下载,重启后生效
}
/**
* 手工检查更新:带交互提示、可控流程
*/
async checkForUpdates(): Promise<void> {
await this.registerEvents()
let meta: VersionMetadata
try {
if (Capacitor.getPlatform() === 'web') {
// Web 端仅做示意,占位返回一个假数据,方便联调 UI
meta = {
hasAssetsUpdate: true,
hasAppUpdate: true,
assetsVersion: '20240619',
newVersion: '3.0.1',
sign: 'mock-sign'
} as VersionMetadata
} else {
meta = await this.checkVersion()
}
await this.reportStatisticInfo()
if (!meta.hasAssetsUpdate) {
await Tools.toast({
message: '已经是最新版本',
duration: ToastDuration.LENGTH_LONG
})
return
}
} catch (e) {
console.error('[版本升级]', e)
await Tools.toast({
message: '检查更新失败,请稍后重试',
duration: ToastDuration.LENGTH_LONG
})
return
}
// 1)询问用户是否下载补丁
const confirmDownload = await this.confirm({
message: `发现新资源版本:${meta.assetsVersion},是否立即下载?`,
okText: '立即下载',
cancelText: '暂不'
})
if (!confirmDownload) {
return
}
await Tools.toast({
message: '开始下载更新包,请稍候…',
duration: ToastDuration.LENGTH_SHORT
})
// 2)执行下载
const plan = await this.download(meta)
if (plan.status === BundleStatus.ERROR) {
await Tools.toast({
message: plan.message ?? '下载失败,请稍后重试',
duration: ToastDuration.LENGTH_LONG
})
return
}
// 3)下载成功后,再询问用户是“稍后重启”还是“立即重启”
const confirmRestart = await this.confirm({
message: '更新包下载完成,是否立即重启应用以完成升级?',
okText: '立即重启',
cancelText: '稍后'
})
if (confirmRestart) {
await this.apply(plan, true)
} else {
await this.apply(plan, false)
}
}
/**
* 统一的确认弹窗封装
* 这里仅给出接口形态,具体实现请对接你自己的弹窗组件(如 Ionic Alert、Material Dialog 等)
*/
private async confirm(options: { message: string; okText?: string; cancelText?: string }): Promise<boolean> {
// TODO: 替换为你自己的 UI 实现
// 例如:
// const result = await this.dialog.openConfirm({ ...options })
// return result === 'ok'
console.info('[版本升级][确认弹窗占位]', options)
return true
}
private async checkVersion(): Promise<VersionMetadata> {
return this.updater.checkVersion(false)
}
private async download(meta: VersionMetadata): Promise<UpdatePlan> {
return this.updater.download(meta)
}
private async apply(plan: UpdatePlan, immediately: boolean = true): Promise<void> {
await this.updater.upgrade(plan, immediately)
}
}
通过上述封装,业务层可以继续只依赖
UpdateService,而无需直接感知@rydeen/updater的底层实现细节。
迁移时主要工作是:替换导入路径、调整类型名(HotfixOptions->UpdateOptions等),以及根据新接口稍作参数适配。
非 Angular / 直接使用示例
import { CapacitorUpdateService, type UpdateOptions, type VersionMetadata } from '@rydeen/updater'
const options: UpdateOptions = {
updateMode: 'static',
enable: true,
autoCheck: true,
checkDomain: 'https://your-server.com/version',
downloadDomain: 'https://your-server.com/assets',
intervalHours: 24,
originVersion: {
hasAppUpdate: false,
hasAssetsUpdate: false,
assetsVersion: '1.0.0',
checksum: ''
}
}
const updater = new CapacitorUpdateService(options)
await updater.registerEvents()
updater.checkVersionTimer()
// 非静默检查:仅拿版本数据,由业务弹窗
const meta: VersionMetadata = await updater.checkVersion(false)
if (meta.hasAssetsUpdate) {
const plan = await updater.download(meta)
await updater.upgrade(plan, true)
}
跨端完整 Sample:移动端 + Electron
下面给出一个“工程级”完整示例,展示如何在 同一套业务代码 中同时支持 Android/iOS(Capgo)+ Electron(electron-updater)。
1. 安装依赖(业务工程根目录)
npm install @rydeen/updater @capgo/capacitor-updater @capacitor-rydeen/preferences @capacitor/app electron-updater
2. Angular 应用(渲染进程 / 移动端)接入
app.config.ts:
import { ApplicationConfig } from '@angular/core'
import {
provideRydeenUpdater,
UPDATER_SERVICE,
type RydeenUpdaterOptions
} from '@rydeen/updater'
const updaterOptions: RydeenUpdaterOptions = {
updateMode: 'static',
enable: true,
autoCheck: true,
checkDomain: 'https://your-server.com/version',
downloadDomain: 'https://your-server.com/assets',
intervalHours: 12,
originVersion: {
hasAppUpdate: false,
hasAssetsUpdate: false,
assetsVersion: '1.0.0',
checksum: ''
}
}
export const appConfig: ApplicationConfig = {
providers: [
// ...
...provideRydeenUpdater(updaterOptions)
]
}
app-update.service.ts(统一的更新入口,移动端 / Electron 共用):
import { inject, Injectable } from '@angular/core'
import { UPDATER_SERVICE, type IUpdateService } from '@rydeen/updater'
@Injectable({ providedIn: 'root' })
export class AppUpdateService {
private readonly updater = inject<IUpdateService>(UPDATER_SERVICE)
async init() {
await this.updater.registerEvents()
this.updater.checkVersionTimer()
}
async manualCheck() {
const meta = await this.updater.checkVersion(false)
if (meta.hasAppUpdate || meta.hasAssetsUpdate) {
// 这里可以弹出统一的“发现新版本”对话框
const plan = await this.updater.download(meta)
await this.updater.upgrade(plan, true)
}
}
}
在移动端(Android/iOS)运行时:
createPlatformUpdateService()返回CapacitorUpdateService,内部使用 Capgo 进行资源热修复。
在 Electron 中运行时:
createPlatformUpdateService()返回ElectronUpdateService,通过 IPC 调用主进程的 electron-updater / 补丁逻辑。
3. Electron preload:暴露 ipcRenderer
preload.ts:
import { contextBridge, ipcRenderer } from 'electron'
contextBridge.exposeInMainWorld('electron', {
ipcRenderer: {
invoke: (channel: string, ...args: any[]) => ipcRenderer.invoke(channel, ...args)
}
})
这样渲染进程中的 ElectronUpdateService 可以通过 window.electron.ipcRenderer.invoke(...) 调用主进程。
4. Electron 主进程:基于 electron-updater + 封装入口
推荐使用 @rydeen/updater/electron-main 提供的主进程封装,而不是手写所有 IPC:
main.ts(仅示意关键部分,实际工程中请按你的启动流程组织):
import { app, BrowserWindow } from 'electron'
import { registerRydeenElectronUpdaterIpc } from '@rydeen/updater/electron-main'
function createWindow() {
const win = new BrowserWindow({
webPreferences: {
preload: /* 你的 preload 路径 */,
contextIsolation: true
}
})
win.loadURL(/* 你的前端入口地址 */)
}
app.whenReady().then(() => {
// 注册与渲染进程约定好的 IPC,内部使用 electron-updater 完成整包更新
registerRydeenElectronUpdaterIpc({
autoCheckDefault: true,
intervalHoursDefault: 24,
async onAssetsPatchApply(meta) {
// 可选:这里实现“方案 B”文件级补丁逻辑
// - 根据 meta.assetsVersion / meta.checksum 下载补丁到 app.getPath('userData')
// - 使用 protocol.interceptFileProtocol 覆盖静态资源
}
})
createWindow()
app.on('activate', () => {
if (BrowserWindow.getAllWindows().length === 0) {
createWindow()
}
})
})
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
app.quit()
}
})
registerRydeenElectronUpdaterIpc会在主进程中注册一组rydeen-updater/*IPC 通道,
与渲染进程的ElectronUpdateService一一对应,并通过electron-updater完成整包更新。onAssetsPatchApply回调预留给你实现文件级补丁(方案 B),
例如下载补丁 zip 至app.getPath('userData'),并用protocol.interceptFileProtocol将补丁中的静态资源“覆盖”内置 asar 资源。
资源补丁包制作与清单规范
这一节主要回答两个问题:
- 热修复补丁包(资源 ZIP)应该 怎么打?
- 服务器端应该 怎么保存版本与文件信息,供客户端拉取?
Android / iOS:Capgo 资源补丁包制作思路
移动端由 @capgo/capacitor-updater 接管实际的资源加载逻辑,本插件只负责 下载 + 调用 Capgo。典型流程如下:
-
1. 本地构建前端资源
- 使用你的前端构建命令(如
npm run build),生成dist目录。 - 确保结构与 Capacitor 工程的
www目录一致(index.html、assets/...等),即「打包后的 H5 资源」。
- 使用你的前端构建命令(如
-
2. 生成 Capgo 兼容的 ZIP 包
- 将构建产物打包为一个 ZIP:
- ZIP 根目录下直接是
index.html、assets/等文件(不要再多加一层文件夹)。 - 命名示例:
app-frontend-20240701.zip。
- ZIP 根目录下直接是
- 将 ZIP 上传到你的对象存储(OSS、COS、S3 等),得到一个 可公开访问或后端代理访问的 URL。
- 将构建产物打包为一个 ZIP:
-
3. 在版本中心登记资源版本
- 数据库中维护一张「客户端资源版本表」,至少包含:
assetsVersion(如20240701);checksum(ZIP 的 HASH,用于校验完整性);filename(可选,如app-frontend-20240701.zip);sign(可选,加密后的下载地址字符串,方便隐藏真实 URL);- 是否有
hasAppUpdate/hasAssetsUpdate标记。
- 当客户端调用
checkDomain接口时,后端根据当前应用 ID / build 号 / 现有资源版本,返回一条VersionMetadata结构的数据(README 前文已有字段说明)。
- 数据库中维护一张「客户端资源版本表」,至少包含:
-
4. 客户端如何使用
- 本插件在移动端的下载逻辑是:
- 若
VersionMetadata.sign存在:解码得到真实 URL,传给CapacitorUpdater.download({ url, version }); - 否则:用
downloadDomain + '/' + filename拼出 URL 再下载。
- 若
- Capgo 解压并管理资源包,不需要你在客户端额外解压或手工覆盖文件。
- 本插件在移动端的下载逻辑是:
简单理解:移动端补丁包其实就是「一份新的 H5 资源 ZIP」,Capgo 负责将它挂载成当前 WebView 的资源根目录,本插件只负责下载和切换版本。
Electron:方案 B 资源 ZIP 补丁包制作
Electron 端的「方案 B」是 只更新前端静态资源,而不重新安装整个 Electron 应用。
在本插件的默认实现中,补丁包的约定如下:
-
1. 补丁资源的载入方式
- 主进程会在
app.getPath('userData')/rydeen_updater/assets下解压一个 ZIP 补丁包; - 注册一个自定义协议(默认
ryassets://),将该协议映射到上述assets目录; - 你的前端代码中,只要把需要支持热修复的静态资源改为用
ryassets://引用,例如:- 原来:
<img src="assets/logo.png"> - 现在:
<img src="ryassets://assets/logo.png">
- 原来:
- 补丁 ZIP 内只需要包含「有变更的文件」,并保持与协议路径一致的相对路径:
- 若你希望覆盖
ryassets://assets/logo.png,则 ZIP 中应包含assets/logo.png。
- 若你希望覆盖
- 主进程会在
-
2. 补丁 ZIP 的内容与命名
-
目录结构示例:
(ZIP root)├─ assets/│ ├─ logo.png│ └─ main.css└─ js/└─ chunk-12345.js -
命名建议:
electron-assets-<assetsVersion>.zip,例如:electron-assets-20240701.zip。 -
上传到对象存储后,获得一个基础路径,如:
https://cdn.example.com/patch/electron/。
-
-
3. 版本与下载地址如何配置
-
在 Electron 场景下,本插件会这样计算补丁下载 URL:
- 若
VersionMetadata.sign存在:直接视为完整 URL 使用(可由后端写入已经加密的真实地址); - 否则:使用
assetsPatchBaseUrl + '/' + (filename || (assetsVersion + '.zip'))生成地址。
- 若
-
也就是说,你需要在后端版本表中至少维护:
assetsVersion;checksum(可用于服务端自行校验);- 可选
filename(如electron-assets-20240701.zip); - 可选
sign(加密 URL)。
-
同时,在 Electron 主进程调用
registerRydeenElectronUpdaterIpc时提供:registerRydeenElectronUpdaterIpc({assetsPatchBaseUrl: 'https://cdn.example.com/patch/electron',assetsPatchProtocol: 'ryassets', // 可省略,默认即为 ryassets// 如需完全自定义补丁逻辑,也可以传 onAssetsPatchApply 覆盖默认实现})
-
-
4. 主进程默认补丁逻辑回顾
- 符合上述约定时,本插件会在 Electron 主进程自动执行以下步骤:
- 根据版本信息计算 ZIP 下载地址;
- 下载到
userData/rydeen_updater/patch.zip; - 清空并重新创建
userData/rydeen_updater/assets目录; - 用
extract-zip将 ZIP 解压到assets目录; - 注册
assetsPatchProtocol(默认ryassets)的interceptFileProtocol,将协议路径映射到上述目录。
- 符合上述约定时,本插件会在 Electron 主进程自动执行以下步骤:
这样,前端只要约定好用 ryassets:// 访问需要热修复的资源文件,就可以在不重装 Electron 应用的情况下,通过下发 ZIP 补丁来动态覆盖这些资源。
若你需要更复杂的清单文件(例如记录每个补丁中包含哪些文件、回滚策略等),推荐在 服务端版本中心 中维护一份 JSON 清单(例如
manifest字段),供管理平台使用。
当前插件的默认实现并不会在客户端解析这类清单,只依赖VersionMetadata+ ZIP 内容来完成补丁应用。
Electron:方案 A 整包更新(桌面应用新版本)
对于 Electron,「方案 A」指的是 通过安装包整体升级应用(例如从 1.0.0 升级到 1.1.0),本插件在主进程中使用 electron-updater 实现:
-
1. 打包与发布(electron-updater 标准流程)
- 使用
electron-builder或类似工具为桌面应用生成安装包(如*.exe/*.dmg/*.AppImage)以及latest.yml/app-update.yml等元数据文件; - 在
electron-builder配置中设置publish选项,指向你的更新服务器(generic HTTP、S3、OSS 等),确保autoUpdater.checkForUpdates()能从该地址获取到新版本信息; - 将安装包与对应的更新描述文件(如
latest.yml)一并上传到更新服务器,具体格式请参考electron-updater官方文档。
- 使用
-
2. 版本信息与
VersionMetadata.hasAppUpdateelectron-updater自身会根据远程latest.yml判断是否有新版本;- 本插件在主进程的
checkVersion中调用autoUpdater.checkForUpdates(),并将结果转换为VersionMetadata:hasAppUpdate:如果存在新安装包则为true;newVersion:桌面应用的新版本号,例如1.1.0。
- 你可以在服务端的版本中心中也维护一份桌面版本信息(可选),用于前端展示和与移动端资源版本联动,但整包更新本身由
electron-updater决定。
-
3. 业务触发整包更新的典型流程
-
在前端(Angular)侧,通过
AppUpdateService或类似业务服务发起检查:const meta = await this.updater.checkVersion(false)if (meta.hasAppUpdate) {// 弹出对话框,提示有新的桌面版本,例如 meta.newVersionconst ok = await this.confirm({message: `发现新的桌面版本:${meta.newVersion},是否立即下载并更新?`})if (ok) {const plan = await this.updater.download(meta)// 对于 Electron,download() 内部会调用 autoUpdater.downloadUpdate()// upgrade() 会调用 autoUpdater.quitAndInstall(),退出当前应用并启动安装程序await this.updater.upgrade(plan, true)}} -
需要注意:
- 一旦调用
upgrade(plan, true),electron-updater会关闭当前应用并运行安装程序,适合在用户确认后执行; - 若你希望「下载先完成,稍后再提醒用户安装」,可以在业务层自行拆分「只调用 download()」和「在合适时机再调用 upgrade()」,并配合自定义提示逻辑。
- 一旦调用
-
总结:
- 方案 A(整包更新):由
electron-updater根据发布的安装包与更新描述文件完成,插件负责触发checkForUpdates/downloadUpdate/quitAndInstall;- 方案 B(资源补丁):由本插件默认或自定义的 ZIP 补丁逻辑完成,只覆盖前端静态资源,不重新安装应用本体。
平台差异与注意事项
-
Android / iOS
- 必须先正确集成 Capgo(
@capgo/capacitor-updater)和@capacitor-rydeen/preferences。 hasAppUpdate仅作为业务提示标志,不会自动跳转商店或安装 APK。hasAssetsUpdate为true时才会触发资源下载与升级逻辑。
- 必须先正确集成 Capgo(
-
Electron
ElectronUpdateService通过 IPC 调用主进程的registerRydeenElectronUpdaterIpc所注册的通道,主进程内部基于electron-updater完成整包更新:checkVersion():调用autoUpdater.checkForUpdates(),并返回对应的VersionMetadata;download():调用autoUpdater.downloadUpdate(),并在有资源更新时可触发 ZIP 补丁下载与解压;upgrade():调用autoUpdater.quitAndInstall(),退出当前应用并运行安装程序。
- 推荐做法:
- 利用
VersionMetadata.hasAppUpdate决定是否启动整包升级(结合electron-updater); - 利用
hasAssetsUpdate/assetsVersion触发文件级补丁下载到app.getPath('userData')并在主进程挂载协议覆盖资源。
- 利用
-
Web / 其他平台
- 使用
NoopUpdateService,所有操作为“空实现”,仅返回originVersion,用于本地开发占位。
- 使用
服务端接口与协议建议
-
checkDomain返回结构建议- 至少包含:
hasAppUpdate: booleanhasAssetsUpdate: booleanassetsVersion: stringchecksum: string
- 如需由服务器控制下载地址:
- 推荐返回
sign(加密 URL),由客户端解码; - 或返回
filename,由客户端和downloadDomain拼接。
- 推荐返回
- 至少包含:
-
版本策略
- 对于
dynamic模式,建议按客户端上传的packageName、versionCode、assetsVersion做灰度 / 分渠道策略。
- 对于
使用注意事项(总结)
- Capgo 前置:移动端必须先集成并配置 Capgo,否则下载 / 安装会失败。
- Preferences 依赖:本地版本信息依赖
@capacitor-rydeen/preferences,需要在 Native 层正确注册。 - 静默更新策略:慎用
checkVersion(true),建议在良好网络 + 后台场景使用;前台交互推荐checkVersion(false)配合弹窗。 - 标志位协同:后端在下发版本信息时,请确保
hasAppUpdate、hasAssetsUpdate、assetsVersion、checksum等字段与实际资源一一对应,避免客户端误判。