Cordova App 热更新 超详细教程

前言:

Cordova热更新的一些要点:

1、在不用重新安装App的情况下,更新你的代码。可以越过应用商店的审核步骤。 2、涉及的插件依赖发生变化时,无法使用热更新,需要去应用商店下载最新版本安装。 3、热更新不能完全替代你的更新方案,需要结合现有更新方案实施。

正文:

目前Cordova平台我找到的热更新方案有两种

  1. 使用cordova-hot-code-push插件 GitHub仓库地址
  2. 使用 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时。 在此之前可以先捋一下思路,通常热更新的场景有两种:

  1. 静默更新,不弹框提示用户。更新完以后,等待下次App启动应用更新或者直接强制重启App应用更新(不推荐强制重启)。
  2. 弹框提示用户,有新的更新内容,用户可选忽略本次更新(下次进入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,如果有更新就弹出自定义的提示框!

本文部分内容参考自 传送门 如果觉得代码放在微软不安全或者觉得每次翻墙下载更新包太慢,可以自己搭建热更新服务器 参考 传送门

相关推荐
醉の虾11 分钟前
Vue3 使用v-for 渲染列表数据后更新
前端·javascript·vue.js
张小小大智慧20 分钟前
TypeScript 的发展与基本语法
前端·javascript·typescript
hummhumm30 分钟前
第 22 章 - Go语言 测试与基准测试
java·大数据·开发语言·前端·python·golang·log4j
asleep70142 分钟前
第8章利用CSS制作导航菜单
前端·css
hummhumm1 小时前
第 28 章 - Go语言 Web 开发入门
java·开发语言·前端·python·sql·golang·前端框架
幼儿园的小霸王1 小时前
通过socket设置版本更新提示
前端·vue.js·webpack·typescript·前端框架·anti-design-vue
疯狂的沙粒1 小时前
对 TypeScript 中高级类型的理解?应该在哪些方面可以更好的使用!
前端·javascript·typescript
gqkmiss2 小时前
Chrome 浏览器 131 版本开发者工具(DevTools)更新内容
前端·chrome·浏览器·chrome devtools
Summer不秃2 小时前
Flutter之使用mqtt进行连接和信息传输的使用案例
前端·flutter
旭日猎鹰2 小时前
Flutter踩坑记录(二)-- GestureDetector+Expanded点击无效果
前端·javascript·flutter