热更新
This commit is contained in:
1 parent
9e77c8470e
commit
0aa9c21531
2 files changed
+45
-6
No files matched your search
@@ -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 保存在数据库中,部署新服务端版本前必须先执行
|
||||||
|
|||||||
@@ -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())
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user