App升级方式会受到不同的业务情景和产品定位影响,比如互联网的产品,稳定迭代,体验较好的方式都是走 商城下载安装
的方式,比如应用宝、Google Play等。
小公司或者一些业务变更较快的创业公司,多数为传统行业的公司,较多可能采用下载安装包的方式,不走商城,因为商城需要审核,相对比较慢。也有可能是两种方式都结合,稳定迭代的方式走商城,紧急的方式走下载安装包升级或者热更新。
热更新会更方便,只会更新HTML5部分代码,相对于ionic这类APP来说,热更新可能是最好的选择,但是你不能只支持热更新这种升级方式,因为总会有兼容性意外,或者失败的情况,或者是APP插件升级,原生组件升级的情况,这时候就需要全量升级了,因为这部分代码不在HTML5中。
以下是个人在公司里对升级改造的设计流程
升级与热更新解决方案
以下介绍说的以安卓为例,IOS如果是热更新其实也是一样的过程
App管理平台
需要一个APP管理平台去维护APP升级版本,控制APP版本、升级时间、升级内容说明、上传apk安装包、是否为内测版本,内测用户指定控制等。这部分就简单的增删改查维护工作
Ionic App 实现
热更新
下边代码会包含一些个人脚本,这里先贴一下。
package.json脚本命令
"scripts": {
"clean": "ionic-app-scripts clean",
"build:www": "ionic build --prod",
"build:hcp": "cordova-hcp build",
"add:android": "ionic cordova platform add android",
"build:android": "npm run lint && ionic cordova build android --prod --release",
"build:android:prod": "node scripts/changeConfig.js prod && npm run build:hcp && npm run lint && ionic cordova build android --prod --release",
"build:android:uat": "node scripts/changeConfig.js uat && npm run build:hcp && npm run lint && ionic cordova build android --prod --release",
"build:android:test": "node scripts/changeConfig.js test && npm run build:hcp && npm run lint && ionic cordova build android --prod --release",
"build:android:test30": "node scripts/changeConfig.js test30 && npm run build:hcp && npm run lint && ionic cordova build android --prod --release",
"build:android:dev": "node scripts/changeConfig.js dev && npm run build:hcp && npm run lint && ionic cordova build android --prod --release",
"build:ios": "npm run lint && ionic cordova build ios --prod",
"android": "npm run lint && ionic cordova run android --prod",
"debug:prod": "node scripts/changeConfig.js prod && ionic cordova run android",
"debug:uat": "node scripts/changeConfig.js uat && ionic cordova run android",
"debug:test": "node scripts/changeConfig.js test && ionic cordova run android",
"debug:test30": "node scripts/changeConfig.js test30 && ionic cordova run android",
"debug:dev": "node scripts/changeConfig.js dev && ionic cordova run android",
"ios": "npm run lint && ionic cordova run ios",
"lint": "tslint \"src/**/*.ts\"",
"ionic:build": "npm run lint && ionic-app-scripts build",
"ionic:serve": "npm run lint && ionic-app-scripts serve",
"start": "npm run lint && ionic-app-scripts serve",
"precommit": "npm run lint"
},
以上 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
更新代码 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()
方法。
总结
更新升级如果不走商城,热更新和全量升级难免遇到很多坑,不管是系统版本的兼容性,还是其他兼容性问题,都是试坑解决的过程。