Sfoglia il codice sorgente

热更新调整不是wgt文件走整包更新流程

Zimo 2 settimane fa
parent
commit
fa40e77742
3 ha cambiato i file con 383 aggiunte e 313 eliminazioni
  1. 69 39
      components/upgrade.vue
  2. 272 270
      pages/user/index.vue
  3. 42 4
      utils/hot-update.js

+ 69 - 39
components/upgrade.vue

@@ -49,12 +49,15 @@
   </uni-popup>
 </template>
 
-<script>
-import { isHotUpdateRunning } from "@/utils/hot-update.js";
-import { getAppVersion } from "@/api/app.js";
-import { checkVersion, downloadApp, installApp } from "@/utils/upgrade.js";
-// 引用全局变量$t
-import { getCurrentInstance } from "vue";
+<script>
+import {
+  isHotUpdateRunning,
+  HOT_UPDATE_FALLBACK_APP_CHECK_EVENT,
+} from "@/utils/hot-update.js";
+import { getAppVersion } from "@/api/app.js";
+import { checkVersion, downloadApp, installApp } from "@/utils/upgrade.js";
+// 引用全局变量$t
+import { getCurrentInstance } from "vue";
 
 export default {
   data() {
@@ -65,13 +68,15 @@ export default {
       downloadUrl: "", //APP下载链接
       isDownloadFinish: false, //是否下载完成
       hasProgress: false, //是否能显示进度条
-      currentPercent: 0, //当前下载百分比
-      isStartDownload: false, //是否开始下载
-      fileName: "", //下载后app本地路径名称
-
-      t: null, // 全局翻译函数
-    };
-  },
+      currentPercent: 0, //当前下载百分比
+      isStartDownload: false, //是否开始下载
+      fileName: "", //下载后app本地路径名称
+      hasTriggeredStartupAppCheck: false,
+      pendingHotUpdateFallbackAppCheck: false,
+
+      t: null, // 全局翻译函数
+    };
+  },
   computed: {
     //设置进度条样式,实时更新进度位置
     setProStyle() {
@@ -94,10 +99,14 @@ export default {
     if (options.from == "backbutton") {
       return true;
     }
-  },
-  mounted() {
-    //检测版本
-    // #ifdef APP
+  },
+  mounted() {
+    const { appContext } = getCurrentInstance();
+    const t = appContext.config.globalProperties.$t;
+    this.t = t;
+
+    //检测版本
+    // #ifdef APP
     /**
      * 这里保留原来的页面级整包更新检查,但增加一个非常轻量的互斥判断。
      *
@@ -114,28 +123,49 @@ export default {
      * - 用户可能连续看到两个升级提示,甚至两个流程同时下载,体验会非常差
      */
 
-    if (!isHotUpdateRunning()) {
-      this.appCheckVersion();
-    } else {
-      console.log(
-        "skip page upgrade check because startup hot update is running"
-      );
-    }
-    // #endif
-    const { appContext } = getCurrentInstance();
-    const t = appContext.config.globalProperties.$t;
-    this.t = t;
-  },
-  created() {
-    uni.$on("upgrade-app", this.bindEmit);
-  },
-  beforeDestroy() {
-    uni.$off("upgrade-app", this.bindEmit);
-  },
-  methods: {
-    async appCheckVersion() {
-      const { data: remote } = await getAppVersion();
-      const up = await checkVersion({
+    if (this.pendingHotUpdateFallbackAppCheck) {
+      this.triggerStartupAppCheckOnce();
+    } else if (!isHotUpdateRunning()) {
+      this.triggerStartupAppCheckOnce();
+    } else {
+      console.log(
+        "skip page upgrade check because startup hot update is running"
+      );
+    }
+    // #endif
+  },
+  created() {
+    uni.$on("upgrade-app", this.bindEmit);
+    uni.$on(
+      HOT_UPDATE_FALLBACK_APP_CHECK_EVENT,
+      this.handleHotUpdateFallbackAppCheck
+    );
+  },
+  beforeDestroy() {
+    uni.$off("upgrade-app", this.bindEmit);
+    uni.$off(
+      HOT_UPDATE_FALLBACK_APP_CHECK_EVENT,
+      this.handleHotUpdateFallbackAppCheck
+    );
+  },
+  methods: {
+    triggerStartupAppCheckOnce() {
+      if (this.hasTriggeredStartupAppCheck) return;
+      this.hasTriggeredStartupAppCheck = true;
+      this.appCheckVersion();
+    },
+    handleHotUpdateFallbackAppCheck() {
+      if (!this.t) {
+        this.pendingHotUpdateFallbackAppCheck = true;
+        return;
+      }
+
+      this.pendingHotUpdateFallbackAppCheck = false;
+      this.triggerStartupAppCheckOnce();
+    },
+    async appCheckVersion() {
+      const { data: remote } = await getAppVersion();
+      const up = await checkVersion({
         name: remote.appVersion, //最新版本名称
         code: remote.appVersion, //最新版本号
         content: "", //更新内容

+ 272 - 270
pages/user/index.vue

@@ -1,277 +1,279 @@
 <template>
-	<view class=" page  mine-container" :style="{height: `${windowHeight}px`}">
-		<view class="page-back"></view>
-		<view class="navgator justify-center align-center">
-			<view class="nav-title">
-				{{ $t('app.appName') }}
-			</view>
-		</view>
-		<view class="content-section">
-			<!--顶部个人信息栏-->
-			<view class="user-info flex-row justify-start align-center">
-				<image class="avatar" :src="userInfo?.avatar ? userInfo?.avatar : '/static/common/avata.gif'"></image>
-				<view class="info flex-col align-center justify-start">
-					<view class="text flex-row"> <span
-							class="span">{{$t('user.username')}}:</span>{{ userInfo?.nickname }} </view>
-					<view class="text flex-row"> <span class="span">{{$t('user.phone')}}:</span>{{ userInfo?.mobile }}
-					</view>
-
-				</view>
-			</view>
-			<!-- 菜单 -->
-			<view class="menu-list">
-				<view class="card-cell flex-row align-center justify-between" @click="navigateToChange">
-					<image src="/static/user/anquanzhongxin.svg" mode="aspectFill"></image>
-					<view class="cell-con flex-row align-center justify-between">
-						<view class="cell-text flex-row align-center justify-start">
-							<view class="title">
-								{{ $t('user.securityCenter') }}
-							</view>
-							<view class="subtitle">
-								{{ $t('user.modifyPhoneAndPassword') }}
-							</view>
-						</view>
-						<uni-icons type="right" :color="'#CACCCF'" size="15" />
-					</view>
-				</view>
-
-				<view class="card-cell flex-row align-center justify-between">
-					<image src="/static/user/guanyuwomen.svg" mode="aspectFill"></image>
-					<view class="cell-con flex-row align-center justify-between">
-						<view class="cell-text flex-row align-center justify-start">
-							<view class="title">
-								{{ $t('user.aboutUs') }}
-							</view>
-							<view class="subtitle">
-								{{ $t('user.currentVersion') + ' ' + version }}
-							</view>
-						</view>
-						<uni-icons type="right" :color="'#CACCCF'" size="15" />
-					</view>
-				</view>
-
-				<view class="card-cell flex-row align-center justify-between" @click="logout">
-					<image src="/static/user/tuichu.svg" mode="aspectFill"></image>
-					<view class="cell-con flex-row align-center justify-between">
-						<view class="cell-text flex-row align-center justify-start">
-							<view class="title">
-								{{ $t('user.logout') }}
-							</view>
-						</view>
-						<uni-icons type="right" :color="'#CACCCF'" size="15" />
-					</view>
-				</view>
-			</view>
-
-
-		</view>
-	</view>
+  <view class="page mine-container" :style="{ height: `${windowHeight}px` }">
+    <view class="page-back"></view>
+    <view class="navgator justify-center align-center">
+      <view class="nav-title">
+        {{ $t("app.appName") }}
+      </view>
+    </view>
+    <view class="content-section">
+      <!--顶部个人信息栏-->
+      <view class="user-info flex-row justify-start align-center">
+        <image
+          class="avatar"
+          :src="
+            userInfo?.avatar ? userInfo?.avatar : '/static/common/avata.gif'
+          "></image>
+        <view class="info flex-col align-center justify-start">
+          <view class="text flex-row">
+            <span class="span">{{ $t("user.username") }}:</span
+            >{{ userInfo?.nickname }}
+          </view>
+          <view class="text flex-row">
+            <span class="span">{{ $t("user.phone") }}:</span
+            >{{ userInfo?.mobile }}
+          </view>
+        </view>
+      </view>
+      <!-- 菜单 -->
+      <view class="menu-list">
+        <view
+          class="card-cell flex-row align-center justify-between"
+          @click="navigateToChange">
+          <image
+            src="/static/user/anquanzhongxin.svg"
+            mode="aspectFill"></image>
+          <view class="cell-con flex-row align-center justify-between">
+            <view class="cell-text flex-row align-center justify-start">
+              <view class="title">
+                {{ $t("user.securityCenter") }}
+              </view>
+              <view class="subtitle">
+                {{ $t("user.modifyPhoneAndPassword") }}
+              </view>
+            </view>
+            <uni-icons type="right" :color="'#CACCCF'" size="15" />
+          </view>
+        </view>
+
+        <view class="card-cell flex-row align-center justify-between">
+          <image src="/static/user/guanyuwomen.svg" mode="aspectFill"></image>
+          <view class="cell-con flex-row align-center justify-between">
+            <view class="cell-text flex-row align-center justify-start">
+              <view class="title">
+                {{ $t("user.aboutUs") }}
+              </view>
+              <view class="subtitle">
+                {{ $t("user.currentVersion") + " " + version }}
+              </view>
+            </view>
+            <uni-icons type="right" :color="'#CACCCF'" size="15" />
+          </view>
+        </view>
+
+        <view
+          class="card-cell flex-row align-center justify-between"
+          @click="logout">
+          <image src="/static/user/tuichu.svg" mode="aspectFill"></image>
+          <view class="cell-con flex-row align-center justify-between">
+            <view class="cell-text flex-row align-center justify-start">
+              <view class="title">
+                {{ $t("user.logout") }}
+              </view>
+            </view>
+            <uni-icons type="right" :color="'#CACCCF'" size="15" />
+          </view>
+        </view>
+      </view>
+    </view>
+  </view>
 </template>
 
 <script setup>
-	import {
-		onMounted,
-		ref
-	} from 'vue';
-	import {
-		useI18n
-	} from 'vue-i18n'
-	import {
-		getLoginUserInfo
-	} from "@/api/login";
-	const {
-		t
-	} = useI18n({
-		useScope: 'global'
-	})
-
-	import $store from '@/store';
-	// const userStore = $store('user');
-	//
-	// const userInfo = ref(JSON.parse(getUserInfo()))
-	// userInfo.value = JSON.parse(userInfo.value).user
-	// console.log('useer---', userInfo.value)
-	// const avatar = userInfo.value.avatar
-	// const name = userInfo.value.nickname
-	// const phone = userInfo.value.phone || ''
-	const windowHeight = ref(0)
-	uni.getSystemInfo({
-		success: function(res) {
-			windowHeight.value = res.windowHeight
-		}
-	})
-
-	const navigateToChange = () => {
-		uni.navigateTo({
-			url: '/pages/user/change?info=' + JSON.stringify(userInfo.value)
-		})
-	}
-
-	const userStore = $store('user')
-	const logout = () => {
-		uni.showModal({
-			title: t('api.message'),
-			content: `${t('login.logoutConfirm')}?`,
-			success: async function(res) {
-				if (res.confirm) {
-					await userStore.LogOut();
-					await uni.reLaunch({
-						url: '/pages/user/login'
-					})
-				} else if (res.cancel) {
-					console.log('用户点击取消');
-				}
-			}
-		})
-	}
-
-	const userInfo = ref({})
-	const version = ref('')
-	onMounted(async () => {
-		version.value = uni.getAppBaseInfo().appVersion
-		userInfo.value = (await getLoginUserInfo()).data
-
-		uni.$once('updateUserInfo', async () => {
-			userInfo.value = (await getLoginUserInfo()).data
-		})
-	})
+import { getCurrentWgtInfo } from "@/utils/hot-update";
+import { onMounted, ref } from "vue";
+import { useI18n } from "vue-i18n";
+import { getLoginUserInfo } from "@/api/login";
+const { t } = useI18n({
+  useScope: "global",
+});
+
+import $store from "@/store";
+// const userStore = $store('user');
+//
+// const userInfo = ref(JSON.parse(getUserInfo()))
+// userInfo.value = JSON.parse(userInfo.value).user
+// console.log('useer---', userInfo.value)
+// const avatar = userInfo.value.avatar
+// const name = userInfo.value.nickname
+// const phone = userInfo.value.phone || ''
+const windowHeight = ref(0);
+uni.getSystemInfo({
+  success: function (res) {
+    windowHeight.value = res.windowHeight;
+  },
+});
+
+const navigateToChange = () => {
+  uni.navigateTo({
+    url: "/pages/user/change?info=" + JSON.stringify(userInfo.value),
+  });
+};
+
+const userStore = $store("user");
+const logout = () => {
+  uni.showModal({
+    title: t("api.message"),
+    content: `${t("login.logoutConfirm")}?`,
+    success: async function (res) {
+      if (res.confirm) {
+        await userStore.LogOut();
+        await uni.reLaunch({
+          url: "/pages/user/login",
+        });
+      } else if (res.cancel) {
+        console.log("用户点击取消");
+      }
+    },
+  });
+};
+
+const userInfo = ref({});
+const version = ref("");
+onMounted(async () => {
+  const wgtInfo = await getCurrentWgtInfo();
+  version.value = wgtInfo?.version || "";
+  userInfo.value = (await getLoginUserInfo()).data;
+
+  uni.$once("updateUserInfo", async () => {
+    userInfo.value = (await getLoginUserInfo()).data;
+  });
+});
 </script>
 
 <style lang="scss" scoped>
-	.page {
-		padding: 10px !important;
-	}
-
-	.mine-container {
-		width: 100%;
-		height: 100%;
-		position: relative;
-		box-sizing: border-box;
-		overflow: hidden;
-	}
-
-	.page-back {
-		background-image: url("/static/common/1.png");
-		background-repeat: no-repeat;
-		background-size: 100% 100%;
-		position: fixed;
-		top: 0;
-		left: 0;
-		width: 100%;
-		height: 350px;
-		z-index: 0;
-	}
-
-	.navgator {
-		position: fixed;
-		top: 1.5625rem;
-		left: 0;
-		width: 100%;
-		height: 55px;
-		padding: 15px 0;
-
-		z-index: 2;
-		// background-image: url('/static/common/nav-back.png');
-	}
-
-	.nav-title {
-		font-family: PingFang-SC, PingFang-SC;
-		font-weight: bold;
-		font-size: 16px;
-		color: #FFFFFF;
-		line-height: 22px;
-		text-align: right;
-		font-style: normal;
-	}
-
-	.content-section {
-		position: relative;
-		margin-top: 70px;
-		width: 100%;
-		height: calc(100% - 55px - 20px);
-		overflow-y: auto;
-	}
-
-	.user-info {
-		width: 100%;
-		height: 80px;
-		padding: 14px 20px;
-		box-sizing: border-box;
-		background: #FFFFFF;
-		border-radius: 6px;
-		font-size: 14px;
-		color: #333333;
-
-		.avatar {
-			width: 52px;
-			height: 52px;
-			border-radius: 50%;
-			margin-right: 18px;
-		}
-
-		.info {
-			width: calc(100% - 52px - 18px);
-		}
-
-	}
-
-	.text {
-		height: 19px;
-		width: 100%;
-
-		.span {
-			display: block;
-			min-width: 70px;
-		}
-	}
-
-	.menu-list {
-		height: 274px;
-		background: #FFFFFF;
-		border-radius: 6px;
-		margin-top: 10px;
-		padding: 20px;
-		box-sizing: border-box;
-	}
-
-	.card-cell {
-		width: 100%;
-		height: 50px;
-
-		image {
-			width: 32px;
-			height: 32px;
-		}
-	}
-
-	.cell-con {
-		margin-left: 20px;
-		margin-right: 10px;
-		width: calc(100% - 32px - 20px - 10px);
-		height: 100%;
-		border-bottom: 0.5px solid #CACCCF;
-	}
-
-	.cell-text {
-		font-weight: 500;
-
-
-		.title {
-			font-size: 14px;
-			color: #333333;
-			line-height: 20px;
-		}
-
-		.subtitle {
-			font-size: 12px;
-			color: #999999;
-			line-height: 17px;
-			margin-left: 10px;
-		}
-
-		.icon {
-			width: 6px;
-			height: 10px;
-		}
-	}
-</style>
+.page {
+  padding: 10px !important;
+}
+
+.mine-container {
+  width: 100%;
+  height: 100%;
+  position: relative;
+  box-sizing: border-box;
+  overflow: hidden;
+}
+
+.page-back {
+  background-image: url("/static/common/1.png");
+  background-repeat: no-repeat;
+  background-size: 100% 100%;
+  position: fixed;
+  top: 0;
+  left: 0;
+  width: 100%;
+  height: 350px;
+  z-index: 0;
+}
+
+.navgator {
+  position: fixed;
+  top: 1.5625rem;
+  left: 0;
+  width: 100%;
+  height: 55px;
+  padding: 15px 0;
+
+  z-index: 2;
+  // background-image: url('/static/common/nav-back.png');
+}
+
+.nav-title {
+  font-family: PingFang-SC, PingFang-SC;
+  font-weight: bold;
+  font-size: 16px;
+  color: #ffffff;
+  line-height: 22px;
+  text-align: right;
+  font-style: normal;
+}
+
+.content-section {
+  position: relative;
+  margin-top: 70px;
+  width: 100%;
+  height: calc(100% - 55px - 20px);
+  overflow-y: auto;
+}
+
+.user-info {
+  width: 100%;
+  height: 80px;
+  padding: 14px 20px;
+  box-sizing: border-box;
+  background: #ffffff;
+  border-radius: 6px;
+  font-size: 14px;
+  color: #333333;
+
+  .avatar {
+    width: 52px;
+    height: 52px;
+    border-radius: 50%;
+    margin-right: 18px;
+  }
+
+  .info {
+    width: calc(100% - 52px - 18px);
+  }
+}
+
+.text {
+  height: 19px;
+  width: 100%;
+
+  .span {
+    display: block;
+    min-width: 70px;
+  }
+}
+
+.menu-list {
+  height: 274px;
+  background: #ffffff;
+  border-radius: 6px;
+  margin-top: 10px;
+  padding: 20px;
+  box-sizing: border-box;
+}
+
+.card-cell {
+  width: 100%;
+  height: 50px;
+
+  image {
+    width: 32px;
+    height: 32px;
+  }
+}
+
+.cell-con {
+  margin-left: 20px;
+  margin-right: 10px;
+  width: calc(100% - 32px - 20px - 10px);
+  height: 100%;
+  border-bottom: 0.5px solid #cacccf;
+}
+
+.cell-text {
+  font-weight: 500;
+
+  .title {
+    font-size: 14px;
+    color: #333333;
+    line-height: 20px;
+  }
+
+  .subtitle {
+    font-size: 12px;
+    color: #999999;
+    line-height: 17px;
+    margin-left: 10px;
+  }
+
+  .icon {
+    width: 6px;
+    height: 10px;
+  }
+}
+</style>

+ 42 - 4
utils/hot-update.js

@@ -13,6 +13,8 @@ import config from "@/utils/config";
  * 对当前这个 uni-app 项目来说,这样的复杂度已经足够,而且侵入性最低。
  */
 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__";
 
 /**
  * 当用户点击“稍后再说”时,我们把当前版本记下来。
@@ -165,6 +167,23 @@ const compareVersion = (a = "", b = "") => {
   return 0;
 };
 
+/**
+ * 判断服务端返回的地址是否真的是 .wgt 文件。
+ *
+ * 这里额外去掉 query/hash,是为了兼容这类地址:
+ * - https://example.com/app/update.wgt?sign=xxx
+ * - https://example.com/app/update.wgt#download
+ */
+const isWgtPackageUrl = (url = "") => {
+  const normalizedUrl = String(url)
+    .trim()
+    .split("#")[0]
+    .split("?")[0]
+    .toLowerCase();
+
+  return normalizedUrl.endsWith(".wgt");
+};
+
 /**
  * 请求服务端检查是否存在新的 .wgt 资源包。
  *
@@ -292,6 +311,16 @@ const normalizeUpdateResult = (response, currentVersion) => {
   //   };
   // }
 
+  if (data.wgtUrl && !isWgtPackageUrl(data.wgtUrl)) {
+    return {
+      shouldUpdate: false,
+      reason: "fallback-app-check-version",
+      message:
+        "服务端返回的 wgtUrl 不是 .wgt 文件地址,回退到原有整包升级检查流程",
+      payload: data,
+    };
+  }
+
   if (!data.version || !data.wgtUrl) {
     return {
       shouldUpdate: false,
@@ -554,6 +583,7 @@ export const runWgtHotUpdate = async () => {
   }
 
   let currentStage = "prepare";
+  let finalResult = null;
   setHotUpdateRunning(true);
 
   try {
@@ -568,20 +598,22 @@ export const runWgtHotUpdate = async () => {
     const updateResult = normalizeUpdateResult(response, wgtInfo.version);
 
     if (!updateResult.shouldUpdate) {
-      return {
+      finalResult = {
         status: updateResult.reason,
         message: updateResult.message,
         data: updateResult.payload || null,
       };
+      return finalResult;
     }
 
     currentStage = "confirm";
     const confirmed = await confirmHotUpdate(updateResult.payload);
     if (!confirmed) {
-      return {
+      finalResult = {
         status: "cancelled",
         message: "用户取消本次热更新",
       };
+      return finalResult;
     }
 
     currentStage = "download";
@@ -593,10 +625,11 @@ export const runWgtHotUpdate = async () => {
     currentStage = "restart";
     await restartAppAfterInstall(updateResult.payload.version);
 
-    return {
+    finalResult = {
       status: "restarted",
       message: "热更新安装完成,应用已触发重启",
     };
+    return finalResult;
   } catch (error) {
     console.error("[hot-update] 执行失败:", currentStage, error);
 
@@ -615,11 +648,12 @@ export const runWgtHotUpdate = async () => {
        */
     }
 
-    return {
+    finalResult = {
       status: "error",
       stage: currentStage,
       error,
     };
+    return finalResult;
   } finally {
     /**
      * 正常情况下:
@@ -627,6 +661,10 @@ export const runWgtHotUpdate = async () => {
      * - 安装成功后会在 restart 之前先清锁,所以这里再次清理也没有副作用。
      */
     setHotUpdateRunning(false);
+
+    if (finalResult?.status === "fallback-app-check-version") {
+      uni.$emit(HOT_UPDATE_FALLBACK_APP_CHECK_EVENT, finalResult);
+    }
   }
   // #endif