最近做了一个 App 更新的需求,在 web 端我们只需要在站点内重新上传打包好的文件就行,但是如果混合开发了 App 端,我们更新了一些静态资源后,那就需要使用 热更新或者整包更新了。

文章目录

1.热更新和整包更新

热更新:无需重新安装App,只需要下载最新的 wgt 包即可更新,在未修改 SDK、原生插件 时,可以使用该方式更新。

  • wgt:所有的前端资源文件压缩包。

整包更新:相当于迭代一个版本吧,需要重新安装App。


2.版本名称和版本号

manifest.json 中,versionName 代表版本名称,versionCode 代表版本号,版本名称用户是可见的,例如:v1.0.0。

我使用的方案是:版本名称发生改变代表整包更新,versionCode发生改变代表热更新。
当整包更新后,将 versionCode 置 1,因为整包更新的时候已经是最新的资源了,不需要以前热更新的数据了。


3.代码实现

以下片段是:获取版本信息接口返回的信息,data 下分别是热更新和整包更新的配置,其中 isForce 代表是否强制更新。

"data": {
    "appRefreshConfig": {
        "versionName": "1.0.0",
        "downloadUrl": "https://www.xx/update/xx.apk",
        "versionDesc": "测试更新.",
        "isForce": 0,
        "type": "appRefreshConfig"
    },
    "hotRefreshConfig": {
        "versionCode": "2",
        "downloadUrl": "https://www.xx/update/2.wgt",
        "versionDesc": "修复了xxx的问题.",
        "isForce": 1,
        "type": "hotRefreshConfig"
    }
}

接下来可以创建一个 store,在里面去编写具体的逻辑代码,下面是我封装好的 store 代码,可以直接使用。

// 获取配置版本的接口
import { getServerVersionInfoApi } from "@/api/utils.js";

export default {
    namespaced: true,
    state: {
        // 本地信息
        localVersionInfo: { versionCode: "", versionName: "" },
        // 服务器信息
        serverVersionInfo: {
            // 热更新配置项
            hotRefreshConfig: { versionCode: "", versionDesc: "", isForce: undefined, downloadUrl: "" },
            // 整包更新配置项
            appRefreshConfig: { versionName: "", versionDesc: "", isForce: undefined, downloadUrl: "" },
        },
        // 页面提示内容
        pageShowInfo: {
            type: "", // hotRefreshConfig | appRefreshConfig
            downloadTempPath: "", // 下载资源的临时路径
            isNeed: false, // 是否需要更新(是否跳转到更新页面的关键字段)
        },
        // 安装按钮配置
        installShowInfo: {
            text: "开始下载",
        },
    },
    actions: {
        /**
         * 获取本地应用版本信息。
         */
        async getLocalVersionInfo({ commit }) {
            return new Promise((resolve, reject) => {
                plus.runtime.getProperty("这里写你的AppID", (widgetInfo) => {
                    commit("setLocalVersionInfo", { versionCode: widgetInfo.versionCode, versionName: widgetInfo.version });
                    resolve({ versionCode: widgetInfo.versionCode, versionName: widgetInfo.version });
                });
            });
        },
        /**
         * 获取服务器版本信息。
         * @returns {Promise<{
         *  hotRefreshConfig: { versionCode: string, versionDesc: string, isForce: Number, downloadUrl: string },
         *  appRefreshConfig: { versionName: string, versionDesc: string, isForce: Number, downloadUrl: string }
         * }>} hotRefreshConfig: 热更新配置,appRefreshConfig: app更新配置
         */
        async getServerVersionInfo({ commit }) {
            try {
                const getServerVersionInfoApiRes = await getServerVersionInfoApi();

                commit("setServerVersionInfo", getServerVersionInfoApiRes.data);
                return Promise.resolve(getServerVersionInfoApiRes.data);
            } catch (e) {
                uni.showToast({ title: "获取版本信息失败", icon: "none" });
                return Promise.reject(getServerVersionInfoApiRes.message);
            }
        },
        /**
         * 对比 (本地版本信息 和 服务器版本信息), 并进行更新提醒。
         *  - 先对比整包版本名称(versionName), 如果本地版本小于服务器版本, 则直接更新主包.
         *  - 否则 对比版本号(versionCode), 如果本地版本小于服务器版本, 则热更新.
         * @param {Object} options 选项对象,包含是否显示最新版本提示的设置。
         * @param {boolean} [options.latestVerIsTip] 是否在本地版本为最新时显示提示信息,默认为 false。
         */
        async comparison({ commit, dispatch }, { latestVerIsTip = false }) {
            // 本地信息
            const localVersionInfo = await dispatch("getLocalVersionInfo");
            const serverVersionInfo = await dispatch("getServerVersionInfo");

            // 对比整包
            if (localVersionInfo.versionName < serverVersionInfo.appRefreshConfig.versionName) {
                return commit("setPageShowInfoType", "appRefreshConfig");
            }

            // 如果整包不需要更新, 则对比热更新包
            if (localVersionInfo.versionName >= serverVersionInfo.appRefreshConfig.versionName) {
                // 对比热更新包
                if (localVersionInfo.versionCode < serverVersionInfo.hotRefreshConfig.versionCode) {
                    return commit("setPageShowInfoType", "hotRefreshConfig");
                }
            }

            // 提示无需更新
            if (latestVerIsTip) {
                uni.showToast({ title: "您当前为最新版本!", icon: "none" });
            }
        },

        // 点击安装按钮的事件
        installClickHandle({ state, commit, dispath }) {
            // 如果在下载中点击, 无效
            if (state.installShowInfo.text.includes("下载中", "%")) return;

            // 如果下载完成后点击确认按钮, 弹出安装界面
            if (state.installShowInfo.text.includes("下载完成")) {
                return plus.runtime.install(state.pageShowInfo.downloadTempPath, { force: true });
            }

            commit("setInstallShowInfoKey", { key: "text", val: "下载中" });
            // 获取当前更新的类型
            const type = state.pageShowInfo.type;

            // 创建下载对象
            const downloadContext = plus.downloader.createDownload(state.serverVersionInfo[type].downloadUrl, {}, (download, status) => {
                if (status == 200) {
                    commit("setPageShowInfoKey", { key: "downloadTempPath", val: download.filename });
                    commit("setInstallShowInfoKey", { key: "text", val: "下载完成" });

                    // 进行安装
                    plus.runtime.install(download.filename, { force: true }, (widgetInfo) => {
                        // 退出应用
                        if (state.pageShowInfo.type === "hotRefreshConfig") {
                            uni.showModal({
                                title: "更新完成",
                                content: "为了加载成功, 请点击确定后重新打开App!",
                                showCancel: false,
                                success: () => {
                                    // plus.runtime.restart(); -> 使用 restart() 可以重启应用 | 但是有可能卡在加载页.
                                    plus.runtime.quit(); // 退出应用, 让用户重新打开.
                                },
                            });
                        }
                    });
                } else {
                    uni.showToast({ title: "下载失败", icon: "none" });
                    commit("setInstallShowInfoKey", { key: "text", val: "重新下载" });
                }
            });

            // 添加下载监听器
            downloadContext.addEventListener("statechanged", (download, status) => {
                // 处理除数不能是0的问题.
                if (download.totalSize === 0 || download.downloadedSize === 0) {
                    return commit("setInstallShowInfoKey", { key: "text", val: "0%" });
                }

                commit("setInstallShowInfoKey", {
                    key: "text",
                    val: `${parseFloat((download.downloadedSize / download.totalSize) * 100).toFixed(1)}%`,
                });

                if (download.state == 4 && status == 200) commit("setInstallShowInfoKey", { key: "text", val: "下载完成" });
            });

            // 开始下载
            downloadContext.start();
        },
    },
    mutations: {
        // 设置 installShowInfo[key]
        setInstallShowInfoKey(state, { key, val }) {
            state.installShowInfo[key] = val;
        },

        // 设置 pageShowInfo.type
        setPageShowInfoType(state, val) {
            state.pageShowInfo["type"] = val;
            state.pageShowInfo["isNeed"] = true;
        },
        // 设置 pageShowinfo[key]
        setPageShowInfoKey(state, { key, val }) {
            state.pageShowInfo[key] = val;
        },
        // 设置 serverVersionInfo
        setServerVersionInfo(state, val) {
            state.serverVersionInfo = val;
        },
        // 设置的 localVersionInfo
        setLocalVersionInfo(state, val) {
            state.localVersionInfo = val;
        },
    },
};

然后在进入页面时,需要去 App.vue 中,触发对比方法。

<script>
export default {
    onLaunch: function () {
        // #ifdef APP-PLUS
        this.$store.dispatch("detectionUpdate/comparison", {}); // 检测更新
        // #endif
    },
    watch: {
        // 如果 isNeed 为 true, 则说明有新版本, 直接跳转到更新页面.
        "$store.state.detectionUpdate.pageShowInfo.isNeed": (n, o) => {
            if (n) uni.navigateTo({ url: "/pages/update/index" });
        },
    },
};
</script>

然后创建一个更新页面:/pages/update/index,先来配置 pages.json,主要是设置一下样式。

{
    "path": "pages/update/index",
    "style": {
        "navigationBarTitleText": "",
        "navigationStyle": "custom",
        "app-plus": {
            "bounce": "none",
            "animationType":"none",
            "background": "transparent"
        }
    }
}

然后在 update/index.vue 中去编写逻辑代码,这里的 u-modal 组件是 uview 组件库中的,然后就大功告成了!

<template>
    <div class="updatePageContainer">
        <u-modal
            :show="true"
            :title="getUpdateInfo.title"
            :content="getUpdateInfo.desc"
            :confirmText="getBtnText"
            @confirm="updateClickHandle"
            :showCancelButton="!isForce"
            cancelText="暂不安装"
            @cancel="updateCancelHandle"
        >
        </u-modal>
    </div>
</template>

<script>
export default {
    name: "Update",
    // 如果强制更新, 就禁用返回按键
    onBackPress(options) {
        if (this.isForce) {
            if (options.from == "backbutton") return true;
        }
    },
    methods: {
        // 取消更新
        updateCancelHandle() {
            uni.navigateBack();
        },
        // 点击更新按钮
        async updateClickHandle() {
            this.$store.dispatch("detectionUpdate/installClickHandle");
        },
    },
    computed: {
        // 当前是否强制更新: { true:强制, false:不强制 }
        isForce() {
            const type = this.$store.state.detectionUpdate.pageShowInfo.type;
            return this.$store.state.detectionUpdate.serverVersionInfo[type].isForce === 1;
        },
        // 获取当前更新信息
        getUpdateInfo() {
            const type = this.$store.state.detectionUpdate.pageShowInfo.type;
            const updateInfo = this.$store.state.detectionUpdate.serverVersionInfo[type];

            return {
                title: `发现新版本~${type === "hotRefreshConfig" ? "(免安装)" : ""}`,
                desc: updateInfo.versionDesc,
            };
        },
        // 获取按钮的文字
        getBtnText() {
            return this.$store.state.detectionUpdate.installShowInfo.text;
        },
    },
};
</script>

<style lang="scss">
page {
    background: rgba(0, 0, 0, 0.5);
}
</style>