前言:
Cordova热更新的一些要点:
1、在不用重新安装App的情况下,更新你的代码。可以越过应用商店的审核步骤。 2、涉及的插件依赖发生变化时,无法使用热更新,需要去应用商店下载最新版本安装。 3、热更新不能完全替代你的更新方案,需要结合现有更新方案实施。
正文:
目前Cordova平台我找到的热更新方案有两种
- 使用cordova-hot-code-push插件 GitHub仓库地址
- 使用 cordova-plugin-code-push GitHub仓库地址
第一个插件,已经官宣不再维护了。第二个插件是微软官方提供的。果断选择第二个。 关于第一个插件的使用方式可以参考这篇文章 传送门
先放上我本地的开发环境:
环境准备
先全局安装 code-push-cli
javascript
npm install -g code-push-cli
后来在实际使用中 提示code-push-cli后续不再支持了,推荐统一使用 appcenter-cli。我去Github仓库看了cli的帮助文档,写的比较简单。需要对应着 code push cli的帮助文档使用。所以本文仍采用 code-push-cli
javascript
npm install -g appcenter-cli
使用code-push-cli 登录
执行 code-push login 命令会打开浏览器窗口 登录code-push服务端
我用的github帐号登录,登录成功会返回一个权限token
复制token粘帖到命令行中,回车登录成功(如果粘贴一直失败,可以尝试点鼠标右键,我一直遇到这种情况)
使用code-push-cli在服务端创建应用
使用命令code-push app add 创建应用
javascript
code-push app add test_ios ios cordova
code-push app add test_android android cordova
执行上述命令会默认为每个应用生成两种部署类型("Production"和"Staging"),我们通过这两种类型分别代表生产环境和开发环境。要注意记下你生成的这些Key值,它用来连接客户端和服务端。
如果忘记了,也可以执行以下命令查看
javascript
code-push deployment list <ownerName>/<appName> --displayKeys
其实也可以在Web端查看管理,但是需要引入相关sdk 传送门
在项目中集成热更新
1、安装插件
javascript
ionic cordova plugin add cordova-plugin-code-push
npm install @ionic-native/code-push
注:官方文档中有提到必须安装白名单插件 cordova plugin list ,一般在cordova 添加platform时就默认安装过了这个插件,最好还是检查plugins文件夹确认一下。
2、配置
在config.xml文件中添加如下配置允许与CodePush服务器通信
javascript
// 其实在添加platform时,这句已经自动帮你加上了
<access origin="*" />
或者
javascript
<access origin="https://codepush.azurewebsites.net" />
<access origin="https://codepush.blob.core.windows.net" />
<access origin="https://codepushupdates.azureedge.net" />
然后在config.xml中加入如下配置,这里的value也就是上一步添加项目时生成的DeploymentKey
javascript
<platform name="android">
<preference name="CodePushDeploymentKey" value="YOUR-ANDROID-DEPLOYMENT-KEY" />
</platform>
<platform name="ios">
<preference name="CodePushDeploymentKey" value="YOUR-IOS-DEPLOYMENT-KEY" />
</platform>
也可以选择不配置,在代码中动态控制,如下: 在项目中新建一个config文件,然后在代码中根据环境传入不同的key。(CodePush插件的一些方法允许传入key,并帮你改写config文件中配置,比如sync 和 checkForUpdate),具体的可以参考后面正式代码
javascript
export const config = {
/**
* 是否是debug环境
*/
isDebug: true,
/**
* 热更新部署时用于链接项目的key
*/
codePushDeploymentKey: {
android: {
Production: '你的android Production key ',
Staging: '你的android Staging key'
},
ios: {
Production: '你的ios Production key',
Staging: '你的ios Staging key'
}
}
};
3、加入热更新代码
代码可以加在任何你想触发热更新检查的地方,比如启动App时。 在此之前可以先捋一下思路,通常热更新的场景有两种:
- 静默更新,不弹框提示用户。更新完以后,等待下次App启动应用更新或者直接强制重启App应用更新(不推荐强制重启)。
- 弹框提示用户,有新的更新内容,用户可选忽略本次更新(下次进入App时重新提示),当用户选择确认更新时,显示loading下载进度条,并执行下载更新。更新完毕后可以弹框提示让用户选择是否立刻重启。若不重启则下次启动App时自动应用更新。
ios不允许热更新,所以我们不能弹框,一般走静默更新,而谷歌安卓规定需要弹框提示。
后台静默更新
javascript
import { Component } from '@angular/core';
import { Platform } from '@ionic/angular';
import { SplashScreen } from '@ionic-native/splash-screen/ngx';
import { StatusBar } from '@ionic-native/status-bar/ngx';
import { CodePush } from '@ionic-native/code-push/ngx';
import { config } from './app.config';
@Component({
selector: 'app-root',
templateUrl: 'app.component.html',
styleUrls: ['app.component.scss']
})
export class AppComponent {
constructor(
private platform: Platform,
private splashScreen: SplashScreen,
private statusBar: StatusBar,
private codePush: CodePush
) {
this.initializeApp();
}
initializeApp() {
this.platform.ready().then(() => {
this.statusBar.styleDefault();
this.splashScreen.hide();
// 可以在这里比对数据库判断是否有大的版本变更,如果有就走更新整个安装包的逻辑,如果没有就走热更新
if(this.checkAppVersion()) {
// do something
} else {
this.loadCodePush();
}
});
}
// 执行热更新逻辑
loadCodePush() {
let deploymentKey = '';
if (this.platform.is('ios')) {
deploymentKey = config.isDebug ? config.codePushDeploymentKey.ios.Staging : config.codePushDeploymentKey.ios.Production;
} else if (this.platform.is('android')) {
deploymentKey = config.isDebug ? config.codePushDeploymentKey.android.Staging : config.codePushDeploymentKey.android.Production;
}
// 下载进度回调
const downloadProgress = (progress) => {
console.log('下载进度:', progress);
console.log(`Downloaded ${progress.receivedBytes} of ${progress.totalBytes}`);
};
// 开始同步
// 除非需要自定义UI和/或行为,否则建议大多数开发人员在将CodePush集成到他们的应用程序时使用这种方法。
//(推荐悄悄的在后台安装然后等待下一次重启时应用更新)
this.codePush.sync({deploymentKey}, downloadProgress).subscribe(syncStatus => {
console.log('同步状态:', syncStatus);
});
}
}
弹框提示更新
javascript
import { Component } from '@angular/core';
import { Platform } from '@ionic/angular';
import { SplashScreen } from '@ionic-native/splash-screen/ngx';
import { StatusBar } from '@ionic-native/status-bar/ngx';
import { CodePush, InstallMode } from '@ionic-native/code-push/ngx';
import { config } from './app.config';
@Component({
selector: 'app-root',
templateUrl: 'app.component.html',
styleUrls: ['app.component.scss']
})
export class AppComponent {
constructor(
private platform: Platform,
private splashScreen: SplashScreen,
private statusBar: StatusBar,
private codePush: CodePush
) {
this.initializeApp();
}
initializeApp() {
this.platform.ready().then(() => {
this.statusBar.styleDefault();
this.splashScreen.hide();
// 可以在这里比对数据库判断是否有大的版本变更,如果有就走更新整个安装包的逻辑,如果没有就走热更新
if(this.checkAppVersion()) {
// do something
} else {
this.loadCodePush();
}
});
}
// 加载热更新
loadCodePush() {
let deploymentKey = '';
if (this.platform.is('ios')) {
deploymentKey = config.isDebug ? config.codePushDeploymentKey.ios.Staging : config.codePushDeploymentKey.ios.Production;
} else if (this.platform.is('android')) {
deploymentKey = config.isDebug ? config.codePushDeploymentKey.android.Staging : config.codePushDeploymentKey.android.Production;
}
// 弹框配置
const dialogOption = {
optionalUpdateMessage: '是否马上更新',
updateTitle: '发现新版本',
optionalInstallButtonLabel: '确定',
optionalIgnoreButtonLabel: '忽略',
};
// 获取下载进度的回调函数
const downloadProgress = (progress) => {
console.log('下载进度:', progress);
console.log(`Downloaded ${progress.receivedBytes} of ${progress.totalBytes}`);
};
// 自动执行检查更新,有更新则在后台下载和安装它。如果本次更新设置了强制更新,会立马重启App
this.codePush.sync({
updateDialog: dialogOption,
deploymentKey,
}, downloadProgress).subscribe(syncStatus => {
console.log('同步状态:', syncStatus);
});
}
}
向热更新服务器推送更新
进入项目的根目录下,使用 code-push release-cordova 发布应用
javascript
code-push release-cordova test_ios ios --description "ios code push"
code-push release-cordova test_android android --description "android code push"
使用命令 code-push deployment list 查看发布状态
javascript
code-push deployment list test_android
其他常用命令
javascript
//给app在热更新服务器上创建应用
code-push app add <appName> <os> <platform>
//删除应用
code-push app rm <appName>
//查看热更新服务器上有哪些应用
code-push app list
//发布应用
code-push release-cordova <appName> <platform> [options]
Options参数:
--deploymentName, -d ..指定部署的类型.默认"Staging",可以选择"Production"或其他 自定义类型
--description, --des ..添加描述
--mandatory, -m .......指定此版本是否为强制更新版本
例1:发布更新
code-push release-cordova ionic2_tabs_android android --des ""
例2:部署"Production"状态的更新,即生产环境的热更新部署使用这句命令
code-push release-cordova ionic2_tabs_android android -d "Production" --des ""
注意:一般生产环境的app是压缩过的,所以在发布正式环境热更新之前,先执行"ionic build --prod"压缩代码
例3:部署ios应用的更新
code-push release-cordova ionic2_tabs_ios ios --des ""
例4:添加-m参数强制更新,code-push插件从服务端下载完代码,会立即自动重启app
code-push release-cordova ionic2_tabs_android android -m --des ""
//查看部署状态
code-push deployment list <appName>
例1:
code-push deployment list ionic2_tabs_android
例2:查看部署状态及key值,忘记key就这样找
code-push deployment list ionic2_tabs_android -k
//清空部署记录
code-push deployment clear <appName> <deploymentName>
如:清空Staging状态的部署记录
code-push deployment clear ionic2_tabs_android Staging
//添加部署状态,默认只有"Staging"和"Production"两中状态
code-push deployment add <appName> [deploymentName]
//删除自定义的部署状态
code-push deployment rm <appName> <deploymentName>
Api解读 官方文档 跟官方文档的调用方式不一样是因为,在项目内引入了ionic-native/code-push ,它对官方的方法使用promise做了封装。但是方法名都分别对应着官方文档的方法名。
javascript
// 检查是否有更新 返回null则代表未检测到更新
const checkForUpdateRes = await this.codePush.checkForUpdate(deploymentKey);
console.log('检查是否有更新:', checkForUpdateRes);
// 获取当前更新包的相关信息 如 描述信息、安装时间、大小
const getCurrentPackageRes = await this.codePush.getCurrentPackage();
console.log('获取当前安装包的相关信息:', getCurrentPackageRes);
// 为已下载并安装,但尚未通过重新启动应用的更新(如果存在)检索相关数据。
const getPendingPackageRes = await this.codePush.getPendingPackage();
console.log('获取已下载完成更新包的相关信息:', getPendingPackageRes);
// 通知本次热更新安装成功。如果你手动检查并安装更新(即不使用sync方法来处理所有更新),
// 那么这个方法必须被调用;否则,CodePush会将更新视为失败,并在应用程序下一次重启时回滚到之前的版本。
this.codePush.notifyApplicationReady();
// 重启App
this.codePush.restartApplication();
// 自动执行检查更新,有更新则在后台下载和安装它。如果本次更新设置了强制更新,会立马重启App
// 除非需要自定义UI和或行为,否则建议大多数开发人员在将CodePush集成到他们的应用程序时使用这种方法。
// 调用此方法时,插件内部会自动帮你执行上面几种方法
this.codePush.sync(syncOptions?: SyncOptions, downloadProgress?: SuccessCallback<DownloadProgress>).subscribe(syncStatus => {
console.log('同步状态:', syncStatus);
});
关于sync 方法中相关参数说明:
javascript
this.codePush.sync(syncOptions?: SyncOptions, downloadProgress?: SuccessCallback<DownloadProgress>)
.subscribe(syncStatus => {
console.log('同步状态:', syncStatus);
});
SyncOptions:{
/**
* 用于指定用于安装操作的安装模式。默认为InstallMode.ON_NEXT_RESTART(即下次重启时应用)
*/
installMode?: InstallMode;
/**
* 如果installMode === ON_NEXT_RESUME,则表示当应用程序恢复更新安装之前,应用程序在后台运行所需的最短时间(以秒为单位)。
*/
minimumBackgroundDuration?: number;
/**
* 用于指定强制更新时用于安装操作的安装模式(即像热更新服务器推送更新包时的配置)。
* 这是可选的,默认为InstallMode.IMMEDIATE。(即立刻重启)
*/
mandatoryInstallMode?: InstallMode;
/**
* 如果设置该值,将忽略前一个被回滚的更新。默认值为true。
*/
ignoreFailedUpdates?: boolean;
/**
* 用于在同步过程中启用、禁用或自定义用户交互。
* 如果设置为true,用户将收到弹框提示是否确认更新(如果设置了强制推送,则不会显示取消按钮)
* 如果传递 UpdateDialogOptions 则对弹框进行自定义操作
*/
updateDialog?: boolean | UpdateDialogOptions;
/**
* 在检查更新时重写config.xml中设置的key。
*/
deploymentKey?: string;
}
enum InstallMode {
/**
* 立即重启并应用更新
*/
IMMEDIATE = 0,
/**
* 下一次重启时应用更新
*/
ON_NEXT_RESTART = 1,
/**
* 当应用程序切入后台,然后重新进入时应用更新
*/
ON_NEXT_RESUME = 2
}
enum SyncStatus {
/**
* 当前是最新的
*/
UP_TO_DATE,
/**
* 更新是可用的,它已经下载、解压缩并复制到部署文件夹。
* 在用SyncStatus调用的回调完成之后。UPDATE_INSTALLED,应用程序将用更新后的代码和资源重新加载。
*/
UPDATE_INSTALLED,
/**
* 更新是可用的,但用户选择忽略此次更新(仅适用于使用updateDialog时)
*/
UPDATE_IGNORED,
/**
* 同步操作期间发生错误。这可能是在与热更新服务器通信、下载或解压缩更新时出现的错误。
* 控制台日志应该包含关于所发生事件的更多信息。本次操作中没有应用任何更新。
*/
ERROR,
/**
* 另一个同步已经在运行,因此此同步尝试已中止。
*/
IN_PROGRESS,
/**
* 中间状态-正在查询热更新服务器以进行更新。。
*/
CHECKING_FOR_UPDATE,
/**
* 中间状态 - 更新可用,并向用户显示最终确认对话框。(这仅适用于updateDialog使用时)
*/
AWAITING_USER_ACTION,
/**
* 中间状态 - 正在从热更新服务器下载一个可用的更新。
*/
DOWNLOADING_PACKAGE,
/**
* 中间状态 - 下载了一个可用的更新并即将安装。
*/
INSTALLING_UPDATE
}
其他问题
走热更新还是整体包更新
在前言部分热更新的注意点中也提到了,当项目依赖的插件发生变化时,比如新增了一个cordova插件,或者新import了插件中的一个类型,重新打包推送到热更新服务器后,热更新就无法正常运行了。推送直接就会报Conflict错误,解决方法往后看。 这时就必须提示用户有版本更新,然后去应用商店下载,或者采用后台下载的方式。
所以在代码中我们一般要根据版本号做出判断,是走热更新还是包更新。 但是我实际踩坑下来,当更改了插件内容,并推送了一个更新版本的包0.0.3上去,如下图(我手机装的是0.0.2版本),在调试了官方提供用于检测版本的Api checkForUpdate后,发现并不能检测到更新,直接返回null。 只是在插件代码执行过程中打印了一句 :更新是可用的,但它针对的是比您当前运行的更新的二进制版本。 就算是我想通过判断字符串这种蠢办法,我也拿不到这个log啊!猜测这个方法应该是通过version为条件进行查找的。 所以对于怎么判断有大的版本变更,还是要通过比对数据库的方式,每次发布新版本后,往数据库写入一条数据,然后app启动时请求该表,比对当前版本和数据库的版本大小,如果有新版本,则执行整体更新。针对App更新方案后续我再单独写一篇文章。
后续:今天去扒了扒codepush的源码,看到了这里: 这个 remotePackageOrUpdateNotification 对象里是有version版本的,所以可以魔改一下代码,或者给codepush的原型对象上加一个方法,把这个version传递出去,这样就可以省去比对数据库版本时,多的那一次请求了。
关于苹果 App Store禁止热更新的说明:
虽然苹果的开发者协议完全允许执行JavaScript和资产的空中更新(这是实现CodePush的原因),但应用显示更新提示是违反他们的政策的。 因此,建议ios应用商店发布的应用在调用sync时不启用updateDialog选项,而谷歌Play和内部发布的应用(如Enterprise, Fabric, HockeyApp)可以选择启用 或者自己定制弹框。
涉及到插件变化,推送后报错:
如果有插件发生变化,亲测哪怕是多 import 了插件内的某个类型(猜测是因为tree shaking,加入了新的import后,依赖包发生了变化),发布内容就会报如下错误。code push此时认为这是两个版本了,无法推送代码。 解决方案:修改版本号(新的版本号必须大于当前),并重新发布。 注意:虽然向热更新服务器成功推送了更新,但是由于版本号发生变化,热更新并不能检测到,需要提示用户去应用商店下载最新的App并重新安装,或者走后台下载,然后弹框提示的方式。参考 传送门。 这里还有个问题,如果你在应用商店上传了新的app,同时向热更新服务器推送了新版本的包,在用户从应用商店装了新的App后,向热更新服务器请求检测是否有新版本,还是会检测到,哪怕你已经从商店装了最新的。 因为每次向热更新服务器推送新版本后,热更新服务器就会生成差异包,此时用新的版本号查询,就会查询到差异包存在,还是会执行一遍替换。 所以这里建议向热更新服务器推迟一个版本,等待下一次需要热更新时再往热更新服务器去推送。
插件自带的更新提示弹框实在太丑了:
不仅丑,还不支持带格式的文本显示,就是一个单纯的字符串!建议自己定制一套样式,然后先调用检测更新的那个api,如果有更新就弹出自定义的提示框!
本文部分内容参考自 传送门 如果觉得代码放在微软不安全或者觉得每次翻墙下载更新包太慢,可以自己搭建热更新服务器 参考 传送门