|
|
@@ -1,27 +1,30 @@
|
|
|
import config from "@/utils/config";
|
|
|
|
|
|
/**
|
|
|
- * 这里用本地存储做一个非常轻量的“启动期热更新锁”。
|
|
|
- *
|
|
|
- * 为什么不用纯内存变量:
|
|
|
- * 1. App.vue 的 onLaunch 和页面组件 mounted 不在同一个文件里,直接共享内存状态不直观。
|
|
|
- * 2. 现有项目已经在登录页、首页挂了旧的整包升级弹窗组件,我们需要一个跨文件、最小改动的互斥标记。
|
|
|
- * 3. 用 storage 的好处是组件侧只要读一个 key 就能知道“启动阶段是否已经有热更新流程在跑”。
|
|
|
- *
|
|
|
- * 注意:
|
|
|
- * 这个锁只是为了解决“启动期并发弹窗”的问题,不是分布式锁,也不是强一致锁。
|
|
|
- * 对当前这个 uni-app 项目来说,这样的复杂度已经足够,而且侵入性最低。
|
|
|
+ * 旧版本使用过的持久化锁 key,仅用于启动时清理遗留值。
|
|
|
+ * 当前版本的互斥状态由模块内 hotUpdatePromise 管理,不会跨进程形成死锁。
|
|
|
*/
|
|
|
export const HOT_UPDATE_LOCK_KEY = "__APP_WGT_HOT_UPDATE_RUNNING__";
|
|
|
export const HOT_UPDATE_FALLBACK_APP_CHECK_EVENT =
|
|
|
"__APP_WGT_HOT_UPDATE_FALLBACK_APP_CHECK__";
|
|
|
+const HOT_UPDATE_FALLBACK_PENDING_KEY =
|
|
|
+ "__APP_WGT_HOT_UPDATE_FALLBACK_PENDING__";
|
|
|
+
|
|
|
+/**
|
|
|
+ * 页面 onShow、App onShow 和路由切换可能在很短时间内连续触发。
|
|
|
+ * 默认三十秒只向服务端检查一次;显式传入 forceCheck 时可用于用户手动检查。
|
|
|
+ */
|
|
|
+export const HOT_UPDATE_CHECK_INTERVAL = 30 * 1000;
|
|
|
+
|
|
|
+let hotUpdatePromise = null;
|
|
|
+let lastCheckStartedAt = 0;
|
|
|
|
|
|
/**
|
|
|
* 当用户点击“稍后再说”时,我们把当前版本记下来。
|
|
|
*
|
|
|
* 这样做的原因:
|
|
|
- * 1. 热更新检查放在 onLaunch,意味着每次冷启动都会检查一次。
|
|
|
- * 2. 如果用户已经明确拒绝了某个版本,下一次冷启动继续弹同一个版本,体验会比较差。
|
|
|
+ * 1. 热更新会在冷启动、回到前台和页面显示时检查。
|
|
|
+ * 2. 如果用户已经明确拒绝了某个版本,后续页面切换继续弹同一个版本,体验会比较差。
|
|
|
* 3. 只记录“被跳过的目标版本”,新版本来了仍然会重新提示。
|
|
|
*/
|
|
|
const HOT_UPDATE_SKIP_VERSION_KEY = "__APP_WGT_HOT_UPDATE_SKIP_VERSION__";
|
|
|
@@ -39,33 +42,39 @@ const HOT_UPDATE_API_URL =
|
|
|
config.default.apiUrl + config.default.apiUrlSuffix + "/rq/iot-app/newWgt";
|
|
|
|
|
|
/**
|
|
|
- * 读取启动期热更新锁。
|
|
|
+ * 读取当前进程的热更新运行状态。
|
|
|
*
|
|
|
* 对页面组件的意义:
|
|
|
- * 旧的整包升级逻辑挂在页面里,页面 mounted 时如果发现启动热更新还在跑,
|
|
|
+ * 旧的整包升级逻辑挂在页面里,页面 mounted 时如果发现热更新还在跑,
|
|
|
* 就应该先让路,避免同时弹两个升级框。
|
|
|
*/
|
|
|
export const isHotUpdateRunning = () => {
|
|
|
- return uni.getStorageSync(HOT_UPDATE_LOCK_KEY) === "1";
|
|
|
+ return Boolean(hotUpdatePromise);
|
|
|
};
|
|
|
|
|
|
/**
|
|
|
- * 设置/清理热更新锁。
|
|
|
- *
|
|
|
- * 输入:
|
|
|
- * - running: Boolean,true 表示流程开始,false 表示流程结束。
|
|
|
- *
|
|
|
- * 输出:
|
|
|
- * - 无返回值,副作用是修改 storage 中的互斥标记。
|
|
|
+ * 旧版本把运行锁写进了持久化 storage。如果 App 在下载或安装过程中被系统
|
|
|
+ * 强杀,这个值不会进入 finally 清理,后续每次启动都会永久跳过更新。
|
|
|
+ * 新实现使用模块内 Promise 作为进程锁;这里仅清理由旧版本遗留的死锁。
|
|
|
*/
|
|
|
-const setHotUpdateRunning = (running) => {
|
|
|
- if (running) {
|
|
|
- uni.setStorageSync(HOT_UPDATE_LOCK_KEY, "1");
|
|
|
- } else {
|
|
|
+const clearLegacyHotUpdateLock = () => {
|
|
|
+ if (uni.getStorageSync(HOT_UPDATE_LOCK_KEY)) {
|
|
|
uni.removeStorageSync(HOT_UPDATE_LOCK_KEY);
|
|
|
}
|
|
|
};
|
|
|
|
|
|
+/**
|
|
|
+ * 整包回退事件可能早于 upgrade 组件创建。额外保存一次待处理标记,组件挂载后
|
|
|
+ * 仍能消费它,避免事件丢失。
|
|
|
+ */
|
|
|
+export const consumePendingAppUpgradeCheck = () => {
|
|
|
+ const pending = uni.getStorageSync(HOT_UPDATE_FALLBACK_PENDING_KEY) === "1";
|
|
|
+ if (pending) {
|
|
|
+ uni.removeStorageSync(HOT_UPDATE_FALLBACK_PENDING_KEY);
|
|
|
+ }
|
|
|
+ return pending;
|
|
|
+};
|
|
|
+
|
|
|
/**
|
|
|
* 等待 plus 对象可用。
|
|
|
*
|
|
|
@@ -508,9 +517,7 @@ const installWgtPackage = (filePath, appid) => {
|
|
|
* 2. 这一步和旧的页面级整包升级不同,旧逻辑 Android 是直接安装 apk。
|
|
|
* 3. 热更新的目标是“尽快让新前端资源生效”,因此安装完成后立即重启是最直接的方案。
|
|
|
*
|
|
|
- * 注意这里在 restart 前先清掉锁:
|
|
|
- * - 因为 storage 会跨重启保留。
|
|
|
- * - 如果不先清,应用重启后旧组件会误以为“热更新一直在运行中”,造成后续检查失效。
|
|
|
+ * 运行锁现在是进程内 Promise;应用重启会自然释放,不再依赖持久化 storage 清锁。
|
|
|
*/
|
|
|
const restartAppAfterInstall = (targetVersion) => {
|
|
|
return new Promise((resolve) => {
|
|
|
@@ -519,7 +526,6 @@ const restartAppAfterInstall = (targetVersion) => {
|
|
|
content: `资源版本 ${targetVersion} 已安装完成,点击确定后将立即重启应用使更新生效。`,
|
|
|
showCancel: false,
|
|
|
success: () => {
|
|
|
- setHotUpdateRunning(false);
|
|
|
plus.runtime.restart();
|
|
|
resolve();
|
|
|
},
|
|
|
@@ -531,7 +537,6 @@ const restartAppAfterInstall = (targetVersion) => {
|
|
|
* - 到这里说明安装已经完成。
|
|
|
* - 如果不重启,用户还会继续运行旧资源,和“安装成功”的状态不一致。
|
|
|
*/
|
|
|
- setHotUpdateRunning(false);
|
|
|
plus.runtime.restart();
|
|
|
resolve();
|
|
|
},
|
|
|
@@ -556,7 +561,7 @@ const showHotUpdateError = (error, title = "更新失败") => {
|
|
|
};
|
|
|
|
|
|
/**
|
|
|
- * 对外暴露的启动期热更新入口。
|
|
|
+ * 内部热更新执行流程,由公开入口负责并发合并和调用频率控制。
|
|
|
*
|
|
|
* 这是你后续在 App.vue onLaunch 里真正调用的函数。
|
|
|
*
|
|
|
@@ -574,18 +579,10 @@ const showHotUpdateError = (error, title = "更新失败") => {
|
|
|
* - Promise<{ status: string, ... }>
|
|
|
* 主要用于日志或后续扩展,目前 App.vue 不需要依赖它的返回值。
|
|
|
*/
|
|
|
-export const runWgtHotUpdate = async () => {
|
|
|
+const executeWgtHotUpdate = async () => {
|
|
|
// #ifdef APP-PLUS
|
|
|
- if (isHotUpdateRunning()) {
|
|
|
- return {
|
|
|
- status: "locked",
|
|
|
- message: "已有热更新流程在执行,跳过重复检查",
|
|
|
- };
|
|
|
- }
|
|
|
-
|
|
|
let currentStage = "prepare";
|
|
|
let finalResult = null;
|
|
|
- setHotUpdateRunning(true);
|
|
|
|
|
|
try {
|
|
|
currentStage = "plus-ready";
|
|
|
@@ -656,14 +653,8 @@ export const runWgtHotUpdate = async () => {
|
|
|
};
|
|
|
return finalResult;
|
|
|
} finally {
|
|
|
- /**
|
|
|
- * 正常情况下:
|
|
|
- * - 无更新 / 用户取消 / 下载失败 / 安装失败,都会走到这里清锁。
|
|
|
- * - 安装成功后会在 restart 之前先清锁,所以这里再次清理也没有副作用。
|
|
|
- */
|
|
|
- setHotUpdateRunning(false);
|
|
|
-
|
|
|
if (finalResult?.status === "fallback-app-check-version") {
|
|
|
+ uni.setStorageSync(HOT_UPDATE_FALLBACK_PENDING_KEY, "1");
|
|
|
uni.$emit(HOT_UPDATE_FALLBACK_APP_CHECK_EVENT, finalResult);
|
|
|
}
|
|
|
}
|
|
|
@@ -674,3 +665,39 @@ export const runWgtHotUpdate = async () => {
|
|
|
message: "当前不是 APP-PLUS 环境,跳过 .wgt 热更新检查",
|
|
|
};
|
|
|
};
|
|
|
+
|
|
|
+/**
|
|
|
+ * 在任意页面均可调用的热更新入口。
|
|
|
+ *
|
|
|
+ * - 同一时刻只执行一个检查/下载/安装流程。
|
|
|
+ * - 自动触发默认受三十秒检查间隔限制。
|
|
|
+ * - 用户主动点击“检查更新”时传 { forceCheck: true } 可忽略时间间隔。
|
|
|
+ */
|
|
|
+export const runWgtHotUpdate = ({ forceCheck = false } = {}) => {
|
|
|
+ // #ifdef APP-PLUS
|
|
|
+ clearLegacyHotUpdateLock();
|
|
|
+
|
|
|
+ if (hotUpdatePromise) {
|
|
|
+ return hotUpdatePromise;
|
|
|
+ }
|
|
|
+
|
|
|
+ const now = Date.now();
|
|
|
+ if (!forceCheck && now - lastCheckStartedAt < HOT_UPDATE_CHECK_INTERVAL) {
|
|
|
+ return Promise.resolve({
|
|
|
+ status: "throttled",
|
|
|
+ message: "距离上次热更新检查时间较短,跳过重复检查",
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ lastCheckStartedAt = now;
|
|
|
+ hotUpdatePromise = executeWgtHotUpdate().finally(() => {
|
|
|
+ hotUpdatePromise = null;
|
|
|
+ });
|
|
|
+ return hotUpdatePromise;
|
|
|
+ // #endif
|
|
|
+
|
|
|
+ return Promise.resolve({
|
|
|
+ status: "skip-non-app-plus",
|
|
|
+ message: "当前不是 APP-PLUS 环境,跳过 .wgt 热更新检查",
|
|
|
+ });
|
|
|
+};
|