热更新

This commit is contained in:
biss committed 2026-08-23 16:59:17 +08:00
1 parent 9e77c8470e
commit 0aa9c21531
2 files changed
+45 -6

No files matched your search

+4 -3
View File
@@ -94,10 +94,11 @@ ZIP 会生成到 `.artifacts/app-updates`,根目录直接包含 `index.html`
`versions.props` 的 `JiaowuFrontendVersion`(传入 `--version` 时也必须一致)。使用 `versions.props` 的 `JiaowuFrontendVersion`(传入 `--version` 时也必须一致)。使用
SuperAdmin 进入“运维与审计 → App 前端热更新”,上传 ZIP,填写目标平台、通道和 SuperAdmin 进入“运维与审计 → App 前端热更新”,上传 ZIP,填写目标平台、通道和
兼容的原生版本后先保存为草稿,再执行发布。当前 Android 工程的 `versionName` 为 兼容的原生版本后先保存为草稿,再执行发布。当前 Android 工程的 `versionName` 为
`1.0`,因此对应更新包的“兼容原生版本”应填写 `1.0`。 `2.6.0`,因此对应更新包的“兼容原生版本”应填写 `2.6.0`。
App 启动、以及从后台回到前台超过 5 分钟后,会向 `/api/app-updates/latest` 检查版本, App 每次启动、网络恢复以及从后台回到前台时,都会向 `/api/app-updates/latest` 检查版本,
在后台下载并校验服务端提供的 SHA-256;切到后台或下次启动时切换,并保留当前页面地址。 在后台下载并校验服务端提供的 SHA-256。下载完成后,用户可在常驻提示中确认立即重新加载;
若选择稍后,切到后台或下次启动时自动切换,并保留当前页面地址。
新资源若未能成功启动,原生更新器会自动回滚。再次发布 新资源若未能成功启动,原生更新器会自动回滚。再次发布
已归档版本即可回滚正式通道;不同原生版本、Android/iOS、测试/正式通道彼此隔离。 已归档版本即可回滚正式通道;不同原生版本、Android/iOS、测试/正式通道彼此隔离。
更新版本元数据和 ZIP 保存在数据库中,部署新服务端版本前必须先执行 更新版本元数据和 ZIP 保存在数据库中,部署新服务端版本前必须先执行
+41 -3
View File
@@ -1,5 +1,6 @@
import { App } from '@capacitor/app'
import { Capacitor } from '@capacitor/core' import { Capacitor } from '@capacitor/core'
import { CapacitorUpdater } from '@capgo/capacitor-updater' import { CapacitorUpdater, type BundleInfo } from '@capgo/capacitor-updater'
import { ElNotification } from 'element-plus' import { ElNotification } from 'element-plus'
import http from '../api/http' import http from '../api/http'
@@ -20,6 +21,7 @@ const updateChannel =
import.meta.env.VITE_APP_UPDATE_CHANNEL?.trim() || 'production' import.meta.env.VITE_APP_UPDATE_CHANNEL?.trim() || 'production'
let checkInProgress: Promise<void> | undefined let checkInProgress: Promise<void> | undefined
let updatePromptVisible = false
function absoluteApiUrl(relativePath: string) { function absoluteApiUrl(relativePath: string) {
const configuredBase = String(http.defaults.baseURL ?? '/api') const configuredBase = String(http.defaults.baseURL ?? '/api')
@@ -87,9 +89,12 @@ async function checkForAppUpdate() {
ElNotification({ ElNotification({
title: `新版本 ${data.version} 已就绪`, title: `新版本 ${data.version} 已就绪`,
message: message:
data.releaseNotes || '切换到后台或下次启动应用时将自动启用更新。', (data.releaseNotes
? `${data.releaseNotes} 点击此提示可立即启用。`
: '切换到后台或下次启动应用时将自动启用更新;点击此提示可立即启用。'),
type: 'success', type: 'success',
duration: 6000, duration: 0,
onClick: () => void applyDownloadedUpdate(downloaded),
}) })
} catch (error) { } catch (error) {
// OTA failure must never stop users from entering the bundled application. // OTA failure must never stop users from entering the bundled application.
@@ -102,6 +107,33 @@ async function checkForAppUpdate() {
return checkInProgress return checkInProgress
} }
async function applyDownloadedUpdate(bundle: BundleInfo) {
if (updatePromptVisible) return
updatePromptVisible = true
try {
await ElNotification.closeAll()
// `set` replaces the WebView bundle and immediately reloads, so ask for a
// deliberate confirmation instead of interrupting unsaved form input.
await import('element-plus').then(({ ElMessageBox }) =>
ElMessageBox.confirm(
'将重新加载应用以启用已下载的新版本。未保存的内容请先提交。',
'启用更新',
{
confirmButtonText: '立即启用',
cancelButtonText: '稍后',
type: 'info',
},
),
)
await CapacitorUpdater.set({ id: bundle.id })
} catch {
// Choosing "later" is safe: next() has already queued this bundle for
// activation when the app backgrounds or is launched again.
} finally {
updatePromptVisible = false
}
}
export async function initializeAppUpdates() { export async function initializeAppUpdates() {
if (!Capacitor.isNativePlatform()) return if (!Capacitor.isNativePlatform()) return
@@ -117,5 +149,11 @@ export async function initializeAppUpdates() {
// Every cold start checks the latest public release immediately. A bundle // Every cold start checks the latest public release immediately. A bundle
// downloaded here is activated when the app is next launched. // downloaded here is activated when the app is next launched.
void checkForAppUpdate() void checkForAppUpdate()
// Keep the native app aligned with the PWA behavior: returning to the app
// is another opportunity to discover a newly deployed web bundle. The
// single-flight guard prevents duplicate cold-start/foreground requests.
void App.addListener('appStateChange', ({ isActive }) => {
if (isActive) void checkForAppUpdate()
})
window.addEventListener('online', () => void checkForAppUpdate()) window.addEventListener('online', () => void checkForAppUpdate())
} }