React-Native — 私服热更新的集成与使用

一、热更新的介绍

很多开发技术中,都会有热更新的说法:

热更新、热启动中的热一般是指不停机/不停APP,或者说不重启。

  • 服务器中的热更新:不需要关闭服务器,直接重新部署项目就行。冷的自然就是关闭服务后再操作。
  • 移动端的热启动、冷启动,这里热就表示APP/服务正在运行中的状态。
  • 客户端中的热更新,稍微扩展了一下,表示不需要重新安装新版本的APP,用户下载安装APP之后,打开App时可以即时更新。

1.1 苹果对热更新的政策

苹果允许使用热更新Apple's developer agreement, 但是规定不能弹框提示用户更新,影响用户体验。 Google Play也允许热更新,但必须弹框告知用户更新。在中国的android市场发布时,都必须关闭更新弹框,否则会在审核应用时以“请上传最新版本的二进制应用包”驳回应用。

如何看待苹果禁止 JSPatch 等 iOS APP 热更新方案?

苹果禁止的是“基于反射的热更新“,而不是 “基于沙盒接口的热更新”。而大部分的应用框架(如 React-Native)和游戏引擎(比如 Unity ,Cocos2d-x,白鹭引擎等)都属于后者,所以不在被警告范围内。

苹果为什么要禁止 JSPatch 等热更新技术?

JSPatch 的原理是,开发者编写 JavaScript 代码,利用苹果内置的 JavaScriptCore.Framework 执行,以实现热更新功能。这一点看似也符合标准,但是在技术上,存在着重大安全隐患,参考 JSPatch 的业务逻辑:

require('UIView')
var view = UIView.alloc().init()
view.setBackgroundColor(require('UIColor').grayColor())
view.setAlpha(0.5)

简单理解,JSPatch可以理解为所有的 Objective-C 的 API 进行了映射,允许开发者在 JS 端调用任意原生代码。这显然是极其危险的。假设这段代码是通过热更新技术下载执行的,如果在中间存在黑客,把这段代码动态替换掉,比如修改为获取用户通讯录并上传到黑客的服务器,就会造成重大的安全问题。

为什么游戏热更新技术可以被理解为是安全的

与 JSPatch 不同的是,游戏热更新技术主要的实现方式是把动态脚本下载之后,让动态脚本调用游戏引擎提供的接口实现缺陷修复。与 JSPatch不同的是,动态脚本并不能任意调用全部原生代码,而是只能根据游戏引擎提供的接口调用相关功能。<font color='red'>本身能够调用的功能是确定、有限的,而不是不确定、任意的系统API。</font>

在这个过程中,游戏引擎的原生端作为一个安全沙箱,提供了一个安全的保护层,只要游戏引擎不要对外提供获取通讯录的接口,黑客就无法通过替换动态脚本的方式获取用户的隐私资料。进而可以被认为是安全的,自然不在苹果的禁止范围内。

1.2 客户端热更新的方案

目前针对react native 热更新的方案比较成熟的选择有 React Native 中文网的 Pushy、微软的 CodePush 和用来搭建私服的 code-push-server

二、CodePush

2.1 介绍

CodePush 是微软的一项云服务,使 Cordova 和 React Native 开发人员能够将移动应用程序的更新直接部署到他们用户的设备上。它充当中央存储库,开发人员可以向其发布某些更新(例如,JS、HTML、CSS 和图像更改),并且应用程序可以从中查询更新(使用提供的客户端 SDK)。使得你在处理bug、添加小功能时,不需要重新构建二进制文件,或者通过任何公共应用商店重新发布。让你拥有一个与你的最终用户更确定和直接的互动模型。

2020年,CodePush is moving to App Center。Visual Studio App Center 将 CodePush 的强大功能与云托管构建、自动化 UI 测试、崩溃报告、分析和推送服务相结合。

客户端的命令行工具,也从 code-push-cli 更换成了 appcenter-cli 。前者的最终版本为3.0.0,之后不再提供支持。

  • Visual Studio App Center 命令行界面 (CLI) 是从命令行运行 App Center 服务的统一工具。 我们的目标是为我们的开发人员提供一个简洁而强大的工具,让他们可以使用 App Center 服务并轻松编写他们想要执行的一系列命令的脚本。 您可以在 App Center 中登录并查看或配置您有权访问的所有应用程序。

CodePush的优点:除了满足基本更新功能外,还有统计,hash计算容错和补丁更新功能。微软的项目,大公司技术有保障,而且开源。近几年微软在拥抱开源方面,让大家也是刮目相看。

2.2 code-push-server

默认code-push 使用的服务器地址为微软的服务器,但考虑到代码安全、微软在中国的速度等,我们需要使用 code-push-server 搭建自己的 服务器。

code-push-server支持以下存储模式:

  • 本地:storage bundle file in local machine
  • 七牛: storage bundle file in qiniu
  • s3(亚马逊简易存储服务): storage bundle file in aws
  • oss(阿里云对象存储 Objec Storage Service): storage bundle file in aliyun
  • 腾讯云: storage bundle file in tencentcloud

三、React-Native集成热更新

3.1 大致流程与所需工具

流程图:

由于我是在开发一个实验性项目,所以工程化不完善,借用的网友公司的热更新大致流程,如有不妥,麻烦评论一下,我删除~

环境

  • React-Native:'0.64.2'

工具:

  • react-native-cli:react-native命令行工具,安装后可以在终端使用 react-native 命令。用于RN项目的初始化、本地调试、bundle及资源文件打包。本机中非全局安装,npx调用。
  • code-push-server 微软云服务在中国太慢,可以用它搭建自己的服务端。
  • code-push-cli :连接微软云端,管理发布更新版本的命令行工具,安装后可以在终端使用 code-push 命令
  • react-native-code-push 集成到react-native项目

3.2 code-push-server 搭建私服

code-push-server 是个服务器上的工具,可以让我们搭建自己的 CodePush 服务,有两种集成方式:

  • docker集成(推荐)
  • 手动操作

HOW TO INSTALL code-push-server 文档很清楚,不是重点,略

注意因为code-push-server 是个人维护的,已经好久没更新,看 issue 有人说不支持 code-push-cli 3.0版本,要使用 2.1.9 版本,react-native-code-push 倒是没限制,直接用的当前最新的 7.0.1 版本(2021.08.26日)。

3.3 开发工作流

3.3.1 分支管理

每个热更新版本都需要在一个新的分支上开发,同时此分支也是版本开发完成后发布更新的分支。

分支名可以遵循如下规则,如:release/20190926_1.8.1.2_newActivity

不过如果不想这么麻烦,直接以版本号命名也可以。单独维护一个 README.md 来记录版本迭代信息。

3.3.2 变量替换

在业务完成后,开发者需要打包App交由测试人员测试。热更发布通常需要开发人员提供三种包:

  • QA环境的测试包
  • 线上环境的测试包
  • 线上环境的生产包

所以在每次打包之前,需要执行脚本,根据参数来替换代码中的Key值,如执行npm run build --dev,会将CodePush的key和host指向qa环境。

3.3.3 打包静态资源

执行 react-native bundle 命令可以将js代码打包成jsbundle文件,也可将静态文件如图片打包到文件夹中。

react-native bundle --platform ios 
--entry-file index.js  # 从index.js为入口
--bundle-output ./bundles/ios/main.jsbundle # 将打包的jsbundle输出到 ./bundles/ios/main.jsbundle 文件
--assets-dest ./bundles/ios # 将静态文件输出到 ./bundles/ios 目录下 
--verbose 
--dev false  # 打包环境为生产环境。--dev默认是true。如果为false,则禁用警告并缩小包。

注意:./bundles/ios 文件夹可以随意指定更改,但要提前创建好目录,否则会报错。

这里打包输出的jsbundle最终会上传到code push服务端用于App端对比更新。

在开发端打包静态资源主要是为了节省发布更新的时间,当然总时间是不变的,(优化了发布系统的体验而已)

3.3.4 推送代码

开发者将代码推送到代码服务器。

3.4 热更新的发布和管理

3.4.1 直接使用code-push-cli

code-push-cli 完成应用的创建、应用更新的版本。相当于是一个CLI形式的管理后台。 npm文档

npm install code-push-cli@2.1.9 -g 
常用code-push命令
# 注册账号
code-push register

# 登陆 在弹出的网页中登录,默认账号:admin, 默认密码:123456,然后获取token,将token复制到控制台中登录即可。
code-push login <url:host>
# 显示登陆的token
code-push access-key ls
# 注销
code-push logout

# 添加项目 创建项目时,默认会生成两套部署环境:Staging(分阶段)、Production
code-push app add <appName> <os> <platform>

code-push app add CodePushDemoIos ios react-native
code-push app add CodePushDemoAndroid android react-native

# 重命名应用
code-push app rename <appName> <newAppName>
# 列出账号下的所有项目
code-push app list
# 删除项目
code-push app remove <appName>

# 部署一个环境
code-push deployment add <appName> <deploymentName>
# 重命名部署
code-push deployment rename <appName> <deploymentName> <newDeploymentName>
# 列出应用的部署
code-push deployment ls <appName>  # 加上 -k 参数,将各个部署的 key 显示出来
# 删除部署
code-push deployment rm <appName> <deploymentName>
# 查看特定应用程序部署的50个最新版本的历史记录
code-push deployment history <appName> <deploymentName>
# 无法删除单个版本,可以使用以下命令清除与部署关联的整个版本历史记录. 运行此命令后,客户端设备将不再接收已清除的更新。此命令是不可逆的,因此不应在生产部署中使用。
code-push deployment clear <appName> <deploymentName>
1. 创建应用
# 添加项目 创建项目时,默认会生成两套部署环境:Staging(分阶段)、Production
code-push app add <appName> <os> <platform>

code-push app add CodePushDemoIos ios react-native
code-push app add CodePushDemoAndroid android react-native
2. 发布新更新 release
code-push release <appName> 
<updateContents>  # 指定应用更新的资源和代码的位置就是打包后的jsbundle位置。 如 `/opt/www/bundle/ios`
<targetBinaryVersion>
[--deploymentName <deploymentName>]
[--description <description>]
[--disabled <disabled>]  # 指定了最终用户是否可以下载更新。 如果未指定,更新将不会被禁用
[--rollout <rolloutPercentage>]  # 指定可以更新的用户百分百,取值在1-100。默认为100
[--mandatory]   # 是否强制更新 强制更新参数有一个`动态转换`的过程,假如用户现在安装了v1版本,服务端更新了v2版本是强制更新,
                # 过后又上传了不是强制更新的v3,这是用户下载v3,v3就会变成强制更新(因为v2是强制更新的),这就是强制更新的动
                # 态转换.

targetBinaryVersion: 目标二进制的版本号,它的可选值规则如图

如果元数据文件中的二进制版本缺少补丁版本,例如 2.0,它将被视为补丁版本为 0,即 2.0 -> 2.0.0。 对于等于纯整数的二进制版本也是如此,在这种情况下,1 将被视为 1.0.0。

3. 发布新更新 release-react

此命令用于一键发布,其实是将react-native bundle命令和code-push release命令结合起来使用。

code-push release-react <appName> <platform>
[--bundleName <bundleName>]
[--deploymentName <deploymentName>]
[--description <description>]
[--development <development>]
[--disabled <disabled>]
[--entryFile <entryFile>]
[--gradleFile <gradleFile>]
[--mandatory]
[--noDuplicateReleaseError]
[--outputDir <outputDir>]
[--plistFile <plistFile>]
[--plistFilePrefix <plistFilePrefix>]
[--sourcemapOutput <sourcemapOutput>]
[--targetBinaryVersion <targetBinaryVersion>]
[--rollout <rolloutPercentage>]
[--privateKeyPath <pathToPrivateKey>]
[--config <config>]
4. 补丁更新(patch)

在发布更新之后,如果想要修改此次更新的参数可以使用patch命令(给更新打补丁),如:你想增加更新的首次展示百分比。

code-push patch MyApp Product --label v10 --rollout 100
code-push patch <appName> <deploymentName>
[--label <releaseLabel>]
[--mandatory <isMandatory>]
[--description <description>]
[--rollout <rolloutPercentage>]
[--disabled <isDisabled>]
[--targetBinaryVersion <targetBinaryVersion>]
  • label:指定的部署环境里更新哪个发布版本(如:v10)
5. 促进更新(promote)

有一个场景, 当我们在线上的Staging环境下测试完毕后,我们可以执行promote命令将之推进到Product环境,而不是重新执行release命令,然后重新设置参数。我们只需执行promote命令进行一个拷贝即可。

code-push promote <appName> <sourceDeploymentName> <destDeploymentName>
[--description <description>]
[--disabled <disabled>]
[--mandatory]
[--rollout <rolloutPercentage>]
[--targetBinaryVersion <targetBinaryVersion]

使用promote命令的优势

  • 速度更快,不需要重新装配资源
  • 可靠性高,不会出错,因为这只是一个推进的过程
6. 回滚更新(rollback)

当某个版本出现重大问题时,需要将版本回滚到老的正常版本去,可以使用rollback命令

code-push rollback <appName> <deploymentName> [--targetRelease/-r <label>]
code-push rollback MyApp Production --targetRelease v10

targetRelease参数指定需要回滚的版本,默认为上个版本。

3.4.2 搭建GUI管理后台

微软的 CodePush 提供了 CodePush Management SDK(Node.js) 。其是一个JavaScript库,用于以编程方式管理CodePush帐户(例如创建应用程序、发布更新版本),该库允许编写基于Node.js的构建和/或部署脚本,而无需使用CLI。

1. 搭建服务端

基于CodePush Management SDK搭建一个node的Http服务,为热更新发布后台管理系统提供服务。使用示例:

npm i code-push -S
var CodePush = require("code-push");
var codePush = new CodePush("YOU_ACCESS_KEY", null, "YOUR_CODE_PUSH_HOST");

/**
 * 获取历史部署
 * @param {string} appName
 * @param {string} deploymentName
 */
async function getDeploymentHistory(appName, deploymentName) {
    let rs = await codePush.getDeploymentHistory(appName, deploymentName);
    console.log(rs);
}

getDeploymentHistory("YOUR_APP_NAME", "Production")

通过上方代码就可以直接使用此服务了,这里说以下很坑的点。

在官方文档中YOU_ACCESS_KEY的值是通过code-push access-key add "YOU_ACCESS_KEY"来的,但通过实验发现此key无效。并输出错误401 Unauthorized

解决:执行cat ~/.code-push.config,使用输出的accessKey作为YOU_ACCESS_KEY

继续执行还是输出了错误:
The session or access key being used is invalid; please run “code-push login” again. If you are on an older version of the CLI, you may need to run “code-push logout” first to clear the session cache.

这个问题我在github上查了很久都没有答案,最后翻看源码终于发现了问题所在,CodePush构造函数的第三个参数接收的是你的codepush服务所在的地址,国内的环境想要使用微软的code-push云服务也会有很多问题。所以都在自己的服务器上搭建,所以会遇到此问题,而国外的程序员一般来说都是使用微软提供的云服务所以没有碰到相关问题,所以在使用时给第三个参数传入自己code push地址即可。

new CodePush("test", null, "http://127.0.0.1:3000")

具体这个基于node的服务如何搭建取决于你们公司的实际情况而定。

此发布热更系统基本已经包含了所有常用的热更新功能,包括了最常用的releasepatchpromoterollback命令。

2. 版本号设计

在热更系统中维护一个版本号,开发者希望这个版本号能够反映出对应的二进制包的版本如2.2.0,同时亦能对应到热更的版本号。

在code push的服务端执行code-push deployment ls appName能查看到如下部署信息

显然这个V9版本号不能满足目前的需求,需要自己设计一套版本号规则,同时和这个V9对应起来。

最终的版本号 = 二进制版本号 + 热更新版本号,如这版热更是针对1.8.1版本的二进制包发布的第三个热更版本,则最终版本号为1.8.1.3

在App内部通过维护此版本号帮助快速定位版本问题version = 1.8.1.3,同时会在个人中心展示此版本号,同时在接口中带上此版本号。

3. 版本号对应

上面设计了一个新的版本号来代替 code push 提供的 V9,但是最终还是需要为这两个版本号建立对应关系,才能保证系统的正常运行,比如需要回滚某个有严重 bug1.9.0.5 版本到 1.9.0.4,最终需要执行

code-push rollback MyApp Production --targetRelease v34

这里需要建立1.9.0.4v34版本的一一对应。

新建version_control数据表存储此关系。

每次发布新版的热更新时,运营人员只需要选泽热更的二机制的版本即可1.8.0,后续的最终版本号由系统按照热更版本自动加一的规则自动生成。

4. 查看发布历史

运营人员通过版本号等条件可以查看发布历史信息,版本历史相关数据如下:

{ 
    description: '',
    isDisabled: false,
    isMandatory: false,
    rollout: 100,
    appVersion: '1.8.0',
    packageHash: '0e616848b4ac4f77617fe51d2c7271dfdde1cad2fa478b2d2b75c6f9d274ae02',
    blobUrl: 'http://192.168.1.1:3000/download/fj/Fji6Hx1buh-sd7f6o9x2BtCLv4MT',
    size: 1656590,
    manifestBlobUrl: 'http://192.168.1.1:3000/download/fs/FswRb5CD9YCbMjzdyQ3EQYI7QUC7',
    diffPackageMap: null,
    releaseMethod: 'Upload',
    uploadTime: 1567362630000,
    originalLabel: '',
    originalDeployment: '',
    label: 'v5'
}

3.5 客户端检查更新

3.5.1 集成 react-native-code-push

官方文档。与所有其他 React Native 插件一样,iOS 和 Android 的集成体验不同,因此请根据您的目标平台执行以下设置步骤。(Android略)

npm install --save react-native-code-push@latest  #安装 react-native-code-push 至 RN 项目

iOS设置文档

  • 在0.6之前,React Native库需要使用 rnpm 进行Link。不支持 rnpm 的还需要手动集成。

  • 0.60之后是采用 CocoaPods 管理的相关依赖。

1. pod install

运行cd ios && pod install && cd ..以安装所有必需的CocoaPods依赖项。

2. 修改 URLForBridge

修改 AppDelegate.m 中的 sourceURLForBridge 方法:

// 打开 AppDelegate.m 文件,并为CodePush标头添加导入语句:
#import <CodePush/CodePush.h>

- (NSURL *)sourceURLForBridge:(RCTBridge *)bridge
{
  #if DEBUG
    return [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@"index" fallbackResource:nil];
  #else
    // 为生产版本设置 bridge 的源URL
    // -- return [[NSBundle mainBundle] URLForResource:@"main" withExtension:@"jsbundle"];
    return [CodePush bundleURL];
  #endif
}

此更改将您的应用配置为始终加载应用的 JS 包的最新版本。在第一次启动时,这将对应于使用应用程序编译的文件。但是,在通过 CodePush 推送更新后,这将返回最近安装的更新的位置。

注意:

  • bundleURL 方法假定您的应用程序的 JS 包被命名为 main.jsbundle。如果您已将应用程序配置为使用不同的文件名,只需调用 bundleURLForResource: 方法(假设您使用的是 .jsbundle 扩展名)或 bundleURLForResource:withExtension: 方法,以覆盖该默认行为。
  • 通常,您只想使用 CodePush 来解析发布版本中的 JS 包位置,因此,我们建议使用 DEBUG 预处理器宏在使用打包服务器和 CodePush 之间动态切换,具体取决于您是否调试与否。这将使确保您在生产中获得所需的正确行为变得更加简单,同时仍然能够在调试时使用 Chrome 开发工具、实时重新加载等。
3. 设置部署环境的密钥

CodePush 运行时,会根据指定的密钥,针对对应的部署环境查询更新,

方法一:在 info.plist 中固定写死

在 APP 的 Info.plist 文件中添加一个名为 CodePushDeploymentKey 的新条目,其值是针对此应用程序配置的部署环境对应的key。

可以通过 code-push deployment ls <appName> -k 来查看应用每个部署环境的 key,(该 -k 标志是必需的,因为默认情况下不会显示键),然后复制相对应的 Deployment Key 即可。

请注意,使用部署的名称(如 Staging)将不起作用。 该“友好名称”仅用于 CLI 中经过身份验证的管理使用,而不用于你应用程序中的公共使用。

  • 如果需要动态使用不同的部署,还可以使用 Code-Push options 在JS代码中覆盖部署密钥

方法二:多部署测试

为了有效利用与 CodePush 应用程序一起创建的 StagingProduction 部署,请在实际将你的应用程序对 CodePush 的使用移入生产环境之前,进行多部署测试的配置。

简单来说,在 Info.plist 中添加名称为 CodePushDeploymentKey 的字段,将值设置为各个部署环境的 key。详细步骤,看文档

方法三:动态部署分配

如果您希望能够执行 A/B 测试,或配置某些用户提前访问到新版本的应用程序(灰度测试),那么能够在运行时将特定用户动态放置到特定部署中被证明是非常有用的。

为了实现这种工作流,您需要做的就是在调用 codePush 方法时指定您希望当前用户同步的部署 key。 指定后,此 key 将覆盖应用程序的 Info.plist (iOS) 或 MainActivity.java (Android) 文件中提供的“默认” key。 这允许您生成用于 staging 或 production 的构建,也能够根据需要动态“重定向”。

// 假设“userProfile”是这个组件收到的一个 prop, 其中包括当前用户应使用的部署密钥。
codePush({deploymentKey: userProfile.CODEPUSH_KEY})(App)
codePush.sync({ deploymentKey: userProfile.CODEPUSH_KEY });
4. 修改服务器地址

步骤同多部署测试,然后在 Info.plist 中添加名称为 CodePushServerURL 的字段,将值设置为各个环境的code-push服务器的地址(IP:host)。

5. 代码签名

文档:从 CLI 2.1.0 版开始,您可以在发布期间对包进行自签名,并在安装更新之前验证其签名。 有关代码签名的更多信息,请参阅相关的代码推送文档部分。

为了配置用于捆绑验证的公钥,您需要在 Info.plist 中添加名称为 CodePushPublicKey 的字段和公钥内容的字符串值。

6. 调试/故障排除

sync 方法包括许多开箱即用的诊断日志记录,因此如果您在使用它时遇到问题,最好首先尝试检查应用程序的输出日志。 这将告诉您应用程序是否配置正确(例如插件能否找到您的部署密钥?),如果应用程序能够访问服务器,是否发现可用更新,是否成功下载/安装更新, 等等。我们希望继续改进日志记录,使其尽可能直观/全面,因此如果您发现它令人困惑或遗漏任何内容,请告诉我们。

查看这些日志的最简单方法是添加标志 --debug。 这将输出一个被过滤为仅 CodePush 消息的日志流。 这使得识别问题变得容易,而无需使用特定于平台的工具,或涉足潜在的大量日志。(code debug ios只支持模拟器,code debug android不限)

此外,还可以启动 Chrome DevTools 控制台、Xcode 控制台 (iOS)、OS X 控制台 (iOS) 和/或 ADB logcat (Android),并查找以 [CodePush] 为前缀的消息。

3.5.2 功能介绍

任何涉及到原生代码的更改都不能通过 CodePush 分发,必须通过商店进行更新。

请注意,如果您同时针对两个平台,建议为每个平台创建单独的 CodePush 应用程序。

1. 差异更新

Releasing Updates:CodePush 客户端支持差异更新,因此即使每次更新时都发布了 JS bundle 和 assets ,最终用户实际上只会下载他们需要的文件。 该服务会自动处理此问题,优化最终用户的下载。

2. 回滚功能

CodePush在实现发布敏捷性的同时,同时也实现了强大的回滚功能。

  • 服务器端回滚:允许您在发现错误版本后阻止其他用户安装。

  • 客户端回滚:为了确保您的最终用户始终拥有您的应用程序的正常运行版本,该插件会维护一个先前更新的副本,以便在您不小心推送包含崩溃的更新时,它可以自动回滚。这样,也保证不会在服务器端回滚之前,会导致用户会被阻塞。

3.5.3 API — 检查更新

react-native-code-push 由两部分组成:

  • JavaScript 模块,可以 import/require,并允许应用程序在运行时与服务交互(例如检查更新,检查有关当前运行的应用程序更新的元数据)。官方文档
  • 原生 API(Objective-C 和 Java),它允许 React Native 应用程序主机使用正确的 JS 包位置引导(bootstrap启动)自身。

code-push的最简单的检查更新如下:

codePush(options: CodePushOptions)(rootComponent: React.Component): React.Component;
// 普通方式
import CodePush from "react-native-code-push";
class App extends React.Component {}
export default CodePush(App);

// ES7 装饰器的方式加载
@CodePush
class App extends React.Component {}
export default App;

使用CodePush高阶函数包裹根组件, 这样会在每次启动App时检查,下载,安装App。 使用高阶组件可以实现App自动更新。

CodePush也可以接受一个检查更新相关的配置对象CodePushOptions,使用如下:

import CodePush from "react-native-code-push";
class App extends React.Component {}
export default CodePush(CodePushOptions)(App);

3.5.4 API — CodePushOptions对象

CodePushOptions配置对象有如下属性:

1. deploymentKey

指定要查询更新的部署密钥。一般来说 code-push 会从 info.plist 或者 MainActivity.java 文件中获取,但是我们可以使用此属性覆盖文件中的key值。

2. checkFrequency

指定检查更新的时间,可取值如下:

/*(默认值) */
codePush.CheckFrequency.ON_APP_START  // 当app完全初始化时(或者更具体地说,当根组件被挂载时)。可以理解为应用进程启动时
codePush.CheckFrequency.ON_APP_RESUME // 当应用程序重新进入前台(包含ON_APP_START的场景)
codePush.CheckFrequency.MANUAL // 禁用自动检查更新,仅在调用sync方法时检查
3. installMode、mandatoryInstallMode

两者取值都是 CodePush.InstallMode ,表示应用程序应该何时安装更新

// 以下说的重启restart the app,都是说的是刷新APP组件,不是整个应用程序进程重启。
    // 无论当前是在任何页面,更新后还是在当前页面,不过当返回时就到了根页面(App组件重新挂载嘛)。
    // 如果就是在根页面,会看到闪的一下刷新效果。
enum InstallMode {
    // 安装更新并立即重启 app。此模式通常使用在提示用户更新时,因为用户在点击更新后往往希望马上看到更新,也常用于强制更新。
    IMMEDIATE, 
  
    // 安装更新,但不重启 app 。当程序下次启动时会自然更新。
    ON_NEXT_RESTART,  

      // 安装更新,但不重启 app,当程序从后台恢复后自然更新(也就是常用的resume事件)
      // 当应用程序在后台超过minimumBackgroundDuration秒后恢复到前台,其实会相当于重启 codePush.restartApp 方法
    ON_NEXT_RESUME,

    // 应用程序需要在后台 minimumBackgroundDuration 秒后才开始安装更新, minimumBackgroundDuration 默认为0;
    ON_NEXT_SUSPEND
}
  • installMode 指定可选更新(没有标记为强制性)的安装模式。默认值:codePush.InstallMode.ON_NEXT_RESTART

  • mandatoryInstallMode:指定被标记为强制更新的安装模式。默认为:codePush.InstallMode.IMMEDIATE

4. minimumBackgroundDuration

指定在重新启动应用程序之前应用程序需要处于后台的最小秒数。 此属性仅适用于使用 InstallMode.ON_NEXT_RESUMEInstallMode.ON_NEXT_SUSPEND 安装的更新,并且有助于更快地将更新呈现在最终用户面前,而不会太突兀。 默认为“0”,它具有在恢复后立即应用更新的效果。

5. updateDialog
null // 默认值,不展示对话框
任一真值 // 启用具有默认字符串的对话框
UpdateDialog // 传入 UpdateDialog类型的对象 启用对话框以及覆盖一个或多个默认字符串。
    // 可以设置强制更新、可选更新时的描述文案、标题、按钮文字

根据地区和平台不同,各大应用市场对更新确认框有不同限制,目前只有google play需要更新确认提示, app store和中国大陆应用市场不允许弹更新确认框。

一般如果需要做弹框提醒更新,往往会自定义弹框样式,不会使用原本的弹框, 在启动 app 时调用 codePush.checkForUpdate() 方法,在有更新时提醒更新。

6. rollbackRetryOptions

回滚重试机制允许应用程序尝试重新安装先前回滚的更新。

null // 默认值,具有禁用重试机制的效果
任一真值 // 启用具有默认设置的重试机制
RollbackRetryOptions // 传入 RollbackRetryOptions 类型对象,启用回滚重试以及覆盖一个或多个默认值。

3.5.5 API — codePush.常用方法

除了使用高阶组件的方式检查安装更新,我们也可以使用调用方法的方式检查更新, CodePush既是个方法,也是个namespace,其中定义了一些检查更新相关的方法。一下都是使用 CodePush. 形式调用的。

1. sync()
/*
 * codePush.sync方法是检测更新、下载更新、安装更新为一体方法,它接收三个参数。调用该方法即可自动更新
 * @param option 为配置对象,和 CodePushOptions 一致, 只是没有 checkFrequency 指定检查时间方法,因为在调用sync方法后
                 马上就会去检查更新。
 * @param statusDidChange 为更新过程状态改变的回调函数, 
 * @param downloadDidProgress 为从code-push服务器下载更新时定时调用的回调函数,通常可以用于向用户展示进度。 
 */
codePush.sync(option, statusDidChange, downloadDidProgress)
statusDidChange ((syncStatus: Number) => void) 

statusDidChange回调会返回app的安装更新情况, 每个阶段都会触发,syncStatus一共有如下情况:

// 应用程序与配置的部署完全一致
codePush.SyncStatus.UP_TO_DATE 

// 已安装可用更新,将在此函数返回后立即运行,或者在下次应用程序恢复/重新启动时运行,具体取决于installMode的值
codePush.SyncStatus.UPDATE_INSTALLED 

// 应用程序有一个可选的更新,最终用户选择忽略。(仅在updateDialog使用时适用)
codePush.SyncStatus.UPDATE_IGNORED

// 同步操作遇到未知错误
codePush.SyncStatus.UNKNOWN_ERROR

// 正在查询code-push服务器以进行更新
codePush.SyncStatus.CHECKING_FOR_UPDATE

// 有可用更新,并向最终用户显示确认对话框(仅在updateDialog使用时适用)
codePush.SyncStatus.AWAITING_USER_ACTION

// 正在从服务器下载可用更新
codePush.SyncStatus.DOWNLOADING_PACKAGE

// 已下载更新,即将安装
codePush.SyncStatus.INSTALLING_UPDATE
// 用法如下:
codePush.sync({...}, this.codePushStatusDidChange )

codePushStatusDidChange = syncStatus => {
    switch(syncStatus) {
        case CodePush.SyncStatus.CHECKING_FOR_UPDATE:
        consloe.log("Checking for update.");
    }
    ...
}
downloadDidProgress((progress: DownloadProgress) => void)

下载更新过程中定时调用此回调函数, DownloadProgress参数是返回的进度,其中包含了两个属性:

  • totalBytes: 此次更新的从字节数
  • receivedBytes: 当前已经接收的字节数
codePush.sync({...}, this.codePushStatusDidChange, this.codePushDownloadDidProgress)
codePushDownloadDidProgress = progress => {
    console.log(progress)
}

在使用高阶函数包裹根组件的方式中,也会有这两个回调, 只不过是以生命周期函数出现的, 用发是在App根组件中添加两个生命周期方法, 用法如下。

import CodePush from "react-native-code-push";

class App extends React.Component {
    codePushStatusDidChange(state){
            //...
    }
    codePushDownloadDidProgress(progress){
            //...
    }
}
export default CodePush(App);

一般来说,我们使用高阶函数或者sync方法配合一些配置已经可以完成检查更新的大部份需求, 但有时我们需要手动去控制整个过程(检查更新, 下载更新, 安装更新), 这时我们可能会用到下面的一些高级方法。

2. disallowRestart()

由于安装了更新, 在下次启动时安装的更新会被应用。 在这期间(安装了更新但还未重启),调用codePush.disallowRestart()可以禁止通过程序重启App。

什么时候会用到此方法呢?当您的应用程序中的某个组件(例如有一个载入过程)需要确保在其生命周期内不会发生最终用户中断时非常有用。

适用于当installMode的值为IMMEDIATE,或ON_NEXT_RESUME,或者手动调用codePush.restart()方法时。也可以理解为codePush.disallowRestart()方法阻止codePush.restart()的调用。

在调用codePush.disallowRestart()方法后,仍然可以获取和安装更新, 但必须等待allowRestart方法被调用后才会重启。

3. allowRestart()

允许因安装更新而发生程序化重启。如果之前调用了disallowRestart方法,导致有需要重启的更新 未重启(被挂起),那么调用 allowRestart 方法将立即重启程序。

如果在 allowRestart() 之前:

  1. 这期间没有更新,所以无需重启
  2. installMode 为 ON_NEXT_RESTART (下次启动更新), 所以无需重启
  3. installMode 为 ON_NEXT_RESUME,但程序一直在前台,所以无需重启
  4. 这期间没有调用过 restartApp() 方法
class App extends Component {
    componentWillMount(){
        // 组件活动状态不允许重启
        codePush.disallowRestart();
    }

    componentWillUnmount(){
        // 组件卸载时可以运行重启更新了
        codePush.allowRestart();
    }
    //... 
}
4. checkForUpdate()
/*
 * 用于查询code-push服务器是否有可用更新,
 * @param deploymentKey 可用于覆盖配置文件中的key
 * @param handleBinaryVersionMismatchCallback 第二个为查询的回调函数。
 */
codePush.checkForUpdate(deploymentKey, handleBinaryVersionMismatchCallback)

handleBinaryVersionMismatchCallback 返回一个promise表示查询结果, 有两种情况:

  • null 表示无更新 可能是如下几种情况造成的:
    • 服务器上该部署还没有任何版本
    • 配置部署的二进制版本和当前用户版本不一致(二进制版本更新需重新上传应用商店)
    • 已经是最新版本
    • 部署中的版本被标记为禁用
    • 部署中的最新版本是活动部署状态,当前用户不在百分百范围内(也就是灰度发布)
  • 可用的更新实例RemotePackage (远端包的实例)。这个实例中包含了一些包的基础信息和下载信息, 另外提供了一个下载方法,用于我们调用此方法下载更新。具体如下:
    • appVersion: 二进制包的版本号
    • deploymentKey: 秘钥
    • packageSize: 包的大小
    • downloadUrl: 包的地址
    • download(downCallBack ? function) : Promise, 下载的回调。
      • 将远端的包下载到本地后,可以拿到LocalPackage本地包的实例;
      • 本地包实例包含了和LocalPackage包相似的属性方法, 另外提供了一个install方法用于安装更新。
    • 其他的属性不说了…
codePush.checkForUpdate().then(update => {
    if (!update) {
        console.log("上面那五种失败情况之一");
    } else {
        console.log("有可用更新");
            // 下载远端的包到本地。
                update.download(this.downCallBack);
    }
});
5. notifyAppReady()

调用此方法通知codePush服务器新的安装已经成功,此方法用在手动下载更新时,如果没有调用此方法通知,那么在下一次启动app时,code-push服务器会认为上一次安装失败了,然后会回滚更新。 在使用sync方法或者高阶函数时不需要调用此方法。

6. getUpdateMetadata()
/**
 * 检索已安装更新的元数据 (比如 description, isMandatory, appVersion, deploymentKey等).
 * @param updateState 默认是 UpdateState.RUNNING ,表示获取用户当前正在运行的更新版本的信息
 */
function getUpdateMetadata(updateState?: UpdateState) : Promise<LocalPackage|null>;
7. restartApp()

立即重启应用程序, 但有可能被阻止。

3.6 未完待续

开发者端热更新发布端热更新服务端App端分析了Code Push的热更流程,以及每个环节应该做什么事情,这其中涉及到的点主要有:

  • 开发环境搭建和发布前准备
  • 热更新版本号的设计和对应关系
  • 依赖于CodePush Management SDK的发布系统设计
  • App端采用的更新模式选则

还差什么?

  • 监控和报警系统(大面积更新失败等严重问题)

参考链接

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,456评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,370评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,337评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,583评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,596评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,572评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,936评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,595评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,850评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,601评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,685评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,371评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,951评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,934评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,167评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,636评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,411评论 2 342