App升级方式会受到不同的业务情景和产品定位影响,比如互联网的产品,稳定迭代,体验较好的方式都是走 商城下载安装 的方式,比如应用宝、Google Play等。

小公司或者一些业务变更较快的创业公司,多数为传统行业的公司,较多可能采用下载安装包的方式,不走商城,因为商城需要审核,相对比较慢。也有可能是两种方式都结合,稳定迭代的方式走商城,紧急的方式走下载安装包升级或者热更新。

热更新会更方便,只会更新HTML5部分代码,相对于ionic这类APP来说,热更新可能是最好的选择,但是你不能只支持热更新这种升级方式,因为总会有兼容性意外,或者失败的情况,或者是APP插件升级,原生组件升级的情况,这时候就需要全量升级了,因为这部分代码不在HTML5中。

以下是个人在公司里对升级改造的设计流程

Ionic App升级流程实践(全量更新&热更新) - 图1

升级与热更新解决方案

以下介绍说的以安卓为例,IOS如果是热更新其实也是一样的过程

App管理平台

需要一个APP管理平台去维护APP升级版本,控制APP版本、升级时间、升级内容说明、上传apk安装包、是否为内测版本,内测用户指定控制等。这部分就简单的增删改查维护工作

Ionic App升级流程实践(全量更新&热更新) - 图2

Ionic App 实现

热更新

下边代码会包含一些个人脚本,这里先贴一下。

package.json脚本命令

  1. "scripts": {
  2. "clean": "ionic-app-scripts clean",
  3. "build:www": "ionic build --prod",
  4. "build:hcp": "cordova-hcp build",
  5. "add:android": "ionic cordova platform add android",
  6. "build:android": "npm run lint && ionic cordova build android --prod --release",
  7. "build:android:prod": "node scripts/changeConfig.js prod && npm run build:hcp && npm run lint && ionic cordova build android --prod --release",
  8. "build:android:uat": "node scripts/changeConfig.js uat && npm run build:hcp && npm run lint && ionic cordova build android --prod --release",
  9. "build:android:test": "node scripts/changeConfig.js test && npm run build:hcp && npm run lint && ionic cordova build android --prod --release",
  10. "build:android:test30": "node scripts/changeConfig.js test30 && npm run build:hcp && npm run lint && ionic cordova build android --prod --release",
  11. "build:android:dev": "node scripts/changeConfig.js dev && npm run build:hcp && npm run lint && ionic cordova build android --prod --release",
  12. "build:ios": "npm run lint && ionic cordova build ios --prod",
  13. "android": "npm run lint && ionic cordova run android --prod",
  14. "debug:prod": "node scripts/changeConfig.js prod && ionic cordova run android",
  15. "debug:uat": "node scripts/changeConfig.js uat && ionic cordova run android",
  16. "debug:test": "node scripts/changeConfig.js test && ionic cordova run android",
  17. "debug:test30": "node scripts/changeConfig.js test30 && ionic cordova run android",
  18. "debug:dev": "node scripts/changeConfig.js dev && ionic cordova run android",
  19. "ios": "npm run lint && ionic cordova run ios",
  20. "lint": "tslint \"src/**/*.ts\"",
  21. "ionic:build": "npm run lint && ionic-app-scripts build",
  22. "ionic:serve": "npm run lint && ionic-app-scripts serve",
  23. "start": "npm run lint && ionic-app-scripts serve",
  24. "precommit": "npm run lint"
  25. },

以上 node scripts/changeConfig.js dev 的脚本是动态修改对应环境的脚本,为了方便Jenkins构建时,不同环境,自动替换对应环境配置。此处不做介绍

安装热更新依赖插件:

ionic cordova plugin add cordova-hot-code-push-plugin
 npm install --save @ionic-native/hot-code-push

热更新修改参考相关文章

热更新升级App操作步骤:

  • npm run build:www
  • npm run build:hcp
  • 复制 www 文件夹替换 content_url 指定的目录内容 (Jenkins构建操作)

Jenkins自动构建脚本

gitlab配合Jenkins,构建不同环境下热更新代码进行测试。(全量安装包apk也是走Jenkins构建,此部分这里不做说明)

#npm install
# ===hot code update
#npm install --save-dev cordova-hot-code-push-cli #非全局
#ionic cordova plugin add cordova-hot-code-push-plugin
#npm install --save @ionic-native/hot-code-push
# ===
#ionic cordova platform add android
#bash ../Script_for_build/chapp.sh #这个脚本只能再第一次构建执行

#python ../Script_for_build/chang-app-config-Prd.py
#node scripts/changeConfig dev #配置修改
#npm run build:www
#npm run build:hcp

mmdir=`pwd`

case $envname in
    dev)
        echo "envname: $envname"
        echo "goujian dev"
        # python ../../Script_for_build/chang-app-config-Prd.py
        # sudo npm install --save-dev cordova-hot-code-push-cli # 这个只需要执行一次
        # npm run build:www
        node scripts/changeConfig dev
        npm run build:www
        npm run build:hcp
        echo "Completion!!!"
    ;;
    test)
        echo "envname: $envname"
        node scripts/changeConfig test
        npm run build:www
        npm run build:hcp

        echo "Completion!!!"
    ;;
    uat)
        echo "envname: $envname"
        node scripts/changeConfig uat
        npm run build:www
        npm run build:hcp
        echo "Completion!!!"
    ;;
    *)
    exit 1
    ;;
esac

tar zcf www-${envname}.tar.gz www

Ionic App升级流程实践(全量更新&热更新) - 图3

更新代码 HotCodeUpdate.ts

统一在一个service里实现apk全量升级和热更新逻辑,然后通过依赖注入的方式调用。appUpdateController.getVersion 接口即为上边所说的 App版本管理 中获取最新的APP版本信息,更加是否存在新版本来进行下一个逻辑处理。整个代码逻辑是根据流程图实现的。

/**
 * @author: giscafer ,https://github.com/giscafer
 * @date: 2018-10-16 14:35:13
 * @description: APP升级检测 & 热更新逻辑,版本6.1.7开始支持热更新
 */

import { Injectable } from '@angular/core';
import { ErrorCode, HotCodePush } from '@ionic-native/hot-code-push';
import { Storage } from '@ionic/storage';
import { Api } from '../api';
import { HCP_CONTENT_JSON_URL } from '../config';
import { Alert } from './../common/alert';
import { NativeService } from './NativeService';
import blacklist from '../../utils/blacklist';
import { Settings } from '../settings';
import { promiseHandle } from '../../utils/promiseHandle';
import { includes } from 'lodash';

/* tslint:disable */
@Injectable()
export class HotCodeUpdateService {

    /* app包下载安装信息 */
    apkInstallInfo = {
        appUrl: '',
        describe: '',
        update: false,
        versionNumber: ''
    };

    _api: any;

    constructor(
        private api: Api,
        private storage: Storage,
        private nativeService: NativeService,
        private hotCodePush: HotCodePush,
        private alert: Alert,
        public settings: Settings,
    ) {
        this._api = this.api.noLoading();
    }


    /**
     * 检查更新
     * ips有新版app则检查app是否支持热更新,不支持则apk下载安装;
     * 支持热更新会直接走热更新,热更新失败则走apk下载安装。
     * 请阅读《JZT升级更新流程-强制流程变更》思维导图
     */
    checkUpdateApp() {
        let that = this;
        if (!this.nativeService.isMobile()) {
            return;
        }
        // user info
        this.getUserInfo().then(res => {
            let user: any = res || {};
            let text = '';
            // 获取最新版本信息
            this.getLatestVersion().then((versionInfo: any) => {
                if (!versionInfo) return;
                this.storage.remove('hcp_version');
                // update 是否强制升级,true强制升级版本
                const { update, describe, versionNumber, curVersion, beta = false, betaUserPhone = [] } = versionInfo;
                this.apkInstallInfo = versionInfo;
                // 内测版本
                if (beta) {
                    // 内测用户
                    if (!includes(betaUserPhone, user.mobile)) {
                        return;
                    } else {
                        text = '【内测Beta】<br>';
                    }
                }
                if (update) {
                    this.nativeService.detectionUpgrade(`${text}${describe}`, update, versionNumber);
                    return;
                }
                // <=6.1.6 版本不支持热更新
                if (!this.nativeService.compareVersionNum(curVersion, '6.1.6')) {
                    return this.apkInstall(this.apkInstallInfo);
                }
                // 6.1.7+ 版本支持热更新
                this.checkHotCodeUpdate().then(hasUpdate => {
                    if (hasUpdate) {
                        this.alert.showDetermine('检测到有新版本,请更新!<br>' + text + describe, `发现新版本${versionNumber}`, () => {
                            this.fetchUpdateAndInstall();
                        }, false);
                        return;
                    }
                    console.log('热更新无最新版本!');
                    // 标识热更新已经最新版本
                    that.storage.set('hcp_version', 'newhcp').then(() => {
                        // 解决触发登陆页面原本就打开的情况,缓存异步无法更新
                        window['epInstance'].emit('hcp_version_cache', true);
                    });
                }).catch(err => {
                    this.nativeService.showToast(err);
                    this.apkInstall(this.apkInstallInfo);
                    console.log('Error:热更新时,apk版本信息获取失败!');
                });
            }).catch(err => {
                this.nativeService.showToast(err);
            });


        });

    }

    /* 获取热更新可升级版本release唯一标识 */
    readyToInstallWebVersion(): Promise<Object> {
        const timestramp = new Date().getTime();
        return new Promise((resolve, reject) => {
            this.api.get(`${HCP_CONTENT_JSON_URL}?v=${timestramp}`).then(resp => {
                try {
                    const json = resp.json() || {};
                    const { release } = json;
                    return resolve({ release });
                } catch (e) {
                    return reject(e);
                }
            }).catch(e => reject(e));
        });
    }



    /* 检查热更新版本是否存在最新 */
    checkHotCodeUpdate() {

        return new Promise((resolve, reject) => {
            this.hotCodePush.getVersionInfo().then(data => {
                let readyRelease;
                this.readyToInstallWebVersion().then((obj: { release: string, app_version: string }) => {
                    readyRelease = obj.release;
                    console.log(`readyToInstallWebVersion: ${readyRelease}`);
                    if (readyRelease <= data.currentWebVersion) {
                        return resolve(false);
                    }
                    return resolve(true);
                }).catch(e => {
                    console.log(e);
                    return reject(e);
                });
                console.log(`当前应用时间版本: ${data.currentWebVersion}, version name: ${data.appVersion}`);
            });
        })
    }

    /* 更新并安装 */
    fetchUpdateAndInstall() {
        // 获取更新文件
        this.nativeService.showLoading('正在下载更新文件……');

        this.hotCodePush.fetchUpdate().then(() => {
            this.nativeService.hideLoading();
            this.nativeService.showLoading('正在安装更新文件……');
            // 安装更新文件
            setTimeout(() => {
                this.hotCodePush.installUpdate().then(() => {
                    this.nativeService.showToast('更新成功,即将重启App,请耐心等待……', 3000);
                }).catch(err => {
                    // 安装失败
                    this.apkInstall(this.apkInstallInfo, true);
                });
            }, 1000);

        }).catch(error => {
            this.nativeService.hideLoading();
            if (error.code === -2) {
                const dialogMessage = '您的App版本不支持热更新,请升级最新版本';
                //调用升级提示框 点击确认会跳转对应商店升级
                this.hotCodePush.requestApplicationUpdate(dialogMessage).then(() => {
                    this.nativeService.openUrlByBrowser('http://jzt.1ziton.com');
                });
            }
            this.hotCodeUpdateErrorHandler(error.code);
            this.apkInstall(this.apkInstallInfo, true);
            console.log(error);
        });
    }

    /* 错误提示处理 */
    hotCodeUpdateErrorHandler(code: number) {

        switch (code) {
            case ErrorCode.FAILED_TO_DOWNLOAD_APPLICATION_CONFIG:
                this.nativeService.showToast('获取应用程序热更新配置失败', 3000);
                console.log('获取应用程序热更新配置失败!');
                break;
            case ErrorCode.FAILED_TO_DOWNLOAD_CONTENT_MANIFEST:
                this.nativeService.showToast('下载MANIFEST文件失败!', 3000);
                console.log('下载MANIFEST文件失败!');
                break;
            case ErrorCode.FAILED_TO_DOWNLOAD_UPDATE_FILES:
                this.nativeService.showToast('下载更新文件失败!', 3000);
                console.log('下载更新文件失败!');
                break;
            case ErrorCode.UPDATE_IS_INVALID:
                this.nativeService.showToast('无效的更新文件', 3000);
                console.log('无效的更新文件');
                break;
            case ErrorCode.ASSETS_FOLDER_IS_NOT_YET_INSTALLED:
                this.nativeService.showToast('assets文件夹未安装', 3000);
                console.log('assets文件夹未安装');
                break;
            case ErrorCode.FAILED_TO_INSTALL_ASSETS_ON_EXTERNAL_STORAGE:
                this.nativeService.showToast('assets安装失败', 3000);
                console.log('assets安装失败');
                break;
            case ErrorCode.NOTHING_TO_UPDATE:
                this.nativeService.showToast('没有可更新的文件');
                console.log('没有可更新的文件');
                break;
            default:
                this.nativeService.showToast('更新失败!', 3000);
                break;
        }
    }

    /* app apk下载安装 */
    apkInstall({ appUrl, versionNumber, update, describe }, hcpFailure = false) {
        if (hcpFailure) {
            this.nativeService.showToast('热更新失败,即将尝试apk完整包下载安装!');
        }
        this.storage.set('APK_DOWNLOAD', appUrl);
        this.nativeService.detectionUpgrade(describe, update, versionNumber);
    }

    /**
     * 检查版本更新
     * @param showTip 没有新版是否提示
     */
    checkApkUpdates(showTip = false) {

        this.getUserInfo().then(res => {
            let user: any = res || {};
            this._checkApkUpdates(user.mobile, showTip).then(() => { }).catch(err => {
                console.log(JSON.stringify(err));
            })
        });

    }

    async _checkApkUpdates(userMobile, showTip = false) {
        if (!userMobile) {
            this.nativeService.showToast("用户信息获取失败:checkApkUpdates");
            return;
        }

        const [err, versionInfo] = await promiseHandle(this.getLatestVersion());

        if (err) {
            this.nativeService.showToast("app 检查更新失败:checkApkUpdates");
            return err;
        }

        if (!versionInfo) {
            if (showTip) {
                this.nativeService.showToast("当前是最新版本哦!");
            }
            return;
        }

        const { content, update, beta = false, betaUserPhone = [], versionNumber } = versionInfo;

        if (beta) {
            // 内测版本
            if (includes(betaUserPhone, userMobile)) {
                let text = '【内测Beta】<br>' + content;
                this.nativeService.detectionUpgrade(text, update, versionNumber);
                return;
            }
        } else {
            this.nativeService.detectionUpgrade(content, update, versionNumber);
        }

    }

    /* 获取最新版本 */
    getLatestVersion() {
        // TODO: beta内测 betaUserPhone
        return new Promise((resolve, reject) => {
            this.nativeService.getVersionNumber().then(version => {
                const versionNumber = version;
                window['app_version'] = version;
                const endpoint = "appUpdateController.getVersion";
                let pargram = {
                    "versionNumber": versionNumber
                }
                //检查版本号
                this.api.noLoading().call(endpoint, pargram).then(json => {
                    let data = json.result;
                    if (!data) {
                        return resolve(null);
                    }
                    let appUrl = data.fileInfos[0];
                    this.storage.set('APK_DOWNLOAD', appUrl);
                    let versionInfo = {
                        ...blacklist(data, 'fileInfos'),
                        curVersion: version, // curVersion 当前机器安装版本
                        content: data.describe,
                        appUrl: data.fileInfos[0]
                    }
                    return resolve(versionInfo)

                }).catch(errs => {
                    return reject(errs);
                });
            }).catch(err => {
                this.nativeService.showToast(err);
                return reject(err);
            });
        });
    }

    /* 获取用户信息 */
    getUserInfo() {
        return new Promise((resolve, reject) => {
            this.settings.getValue('userInfo').then(userInfoStr => {
                if (!userInfoStr) {
                    this._api.noLoading().call("appUserController.queryUserInfo").then((json) => {
                        this.settings.setValue('userInfo', JSON.stringify(json.result));
                        return resolve(json.result);
                    }).then(err => {
                        this.nativeService.showToast("用户信息获取失败:queryUserInfo");
                        return reject(err);
                    });
                } else {
                    return resolve(JSON.parse(userInfoStr));
                }
            }).catch(err => {
                return reject(err);
            });
        })
    }
}

最后在 app.component.ts 中进行 resume 事件监听,我这里热更新触发时机也是 resume

if (this.nativeService.isAndroid()) {
        //进入,前台展示
        document.addEventListener("resume", () => {
            // tslint:disable-next-line:no-console
            console.log("resume");
            this.updateSrv.checkUpdateApp();
        }, false);
    }

另外,在我的——关于我们——检查更新下可以也可以调用checkApkUpdates()方法。

总结

更新升级如果不走商城,热更新和全量升级难免遇到很多坑,不管是系统版本的兼容性,还是其他兼容性问题,都是试坑解决的过程。

Ionic App升级流程实践(全量更新&热更新) - 图4