React Native热更新部署-CodePush(适配iOS)

CodePush简介

CodePush是微软提供的一套用于热更新 React Native 和 Cordova 应用的服务。CodePush 是提供给 React Native 和 Cordova 开发者直接部署移动应用更新给用户设备的云服务。CodePush 作为一个中央仓库,开发者可以推送更新 (JS, HTML, CSS and images),应用可以从客户端 SDK 里面查询更新。CodePush 可以让应用有更多的可确定性,也可以让你直接接触用户群。在修复一些小问题和添加新特性的时候,不需要经过二进制打包,可以直接推送代码进行实时更新。

官方文档:http://microsoft.github.io/code-push/docs/

安装CodePush客户端与注册账号

  • 安装客户端
1
npm install -g code-push-cli
  • 查看版本
1
code-push -v
  • 注册账号
1
code-push register

授权登陆并得到 access key

  • 登陆
1
code-push login
  • 查看当前登录用户
1
code-push whoami
  • 注销
1
code-push logout
  • 列出当前登陆会话
1
code-push session ls
  • 删除登录会话
1
code-push session rm <machineName>

访问密钥

  • 假如您不想通过 Github 或者 Microsoft 第三方授权获取 access key,那么可以通过命令方式生成,默认有效期为60天
1
code-push access-key add "VSTS Integration"
  • 通过 access key 登录
1
code-push login --accessKey <accessKey>
  • 设置用户名及有效期
1
code-push access-key patch <accessKeyName> --name "new name" --ttl 10d

CodePush服务器APP管理

  • 添加APP
1
code-push app add <appName>
  • 分平台添加APP
1
2
code-push app add MyApp-Android
code-push app add MyApp-iOS
  • 重命名APP
1
code-push app rename <appName> <newAppName>
  • 删除APP
1
code-push app rm <appName>
  • 查看当前账号下APP列表
1
code-push app ls

部署管理

一个APP名字默认对应两个部署环境:生产环境Production、模拟或演示环境Staging

  • 添加部署环境
1
code-push deployment add <appName> <deploymentName>
  • 删除及重命名部署环境
1
2
code-push deployment rm <appName> <deploymentName>
code-push deployment rename <appName> <deploymentName> <newDeploymentName>
  • 查看指定APP的部署环境
1
code-push deployment ls <appName> [--displayKeys|-k]

-k 表示显示 Deployment Key

iOS 集成CodePush、打包、发布更新

参考:https://github.com/Microsoft/react-native-code-push

安装组件

1
npm install --save react-native-code-push@latest

链接组件

1
2
// React Native v0.27+
react-native link react-native-code-push
1
2
// React Native v0.27-
rnpm link react-native-code-push

过程需要输入部署环境 key

编译一下,是否成功?查看info.plist是否包含CodePushDeploymentKey

手动添加依赖方法详见:https://github.com/Microsoft/react-native-code-push#plugin-installation-ios—manual

使用CodePush更新策略

使用 CodePush 包裹根组件

1
2
3
4
5
6
7
//ES6
import codePush from "react-native-code-push";

class MyApp extends Component {
}

MyApp = codePush(MyApp);
1
2
3
4
5
6
//ES7
import codePush from "react-native-code-push";

@codePush
class MyApp extends Component {
}
  • 使用更新选项
1
2
3
4
5
6
let codePushOptions = { checkFrequency: codePush.CheckFrequency.ON_APP_RESUME };

class MyApp extends Component {
}

MyApp = codePush(CodePushOptions)(MyApp);
  • 手动更新选项
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
let codePushOptions = { checkFrequency: codePush.CheckFrequency.MANUAL };

class MyApp extends Component {
onButtonPress() {
codePush.sync({
updateDialog: true,
installMode: codePush.installMode.IMMEDIATE
});
}

render() {
<View>
<TouchableOpacity onPress={this.onButtonPress}>
<Text>Check for updates</Text>
</TouchableOpacity>
</View>
}
}

MyApp = codePush(CodePushOptions)(MyApp);

Redux Saga for CodePush:react-native-code-push-saga

发布更新

1
2
3
4
code-push release-react <appName> <platform>

code-push release-react MyApp ios
code-push release-react MyApp-Android android

自动打包js和资源文件成bundle并且上传发布到CodePush的服务器。

也可以自己打包:

react-native bundle –platform 平台 –entry-file 启动文件 –bundle-output 打包js输出文件 –assets-dest 资源输出目录 –dev 是否调试

1
react-native bundle --platform android --entry-file index.android.js --bundle-output ./bundles/index.android.bundle --dev false

然后手动上传:

code-push release <应用名称> <Bundles所在目录> <对应的应用版本> –deploymentName 更新环境 –description 更新描述 –mandatory 是否强制更新

1
code-push release CodePushDemo ./bundles/index.android.bundle 1.2.0 --deploymentName Staging --description "支持文章缓存" --mandatory true

多环境部署测试的步骤

客户端的回滚机制可以导致服务器端的回滚:code-push rollback

  • 发布更新到Staging环境:code-push release-react <appName> <platform>
  • 运行Staging版APP,同步更新,验证其是否正常工作
  • 从演示环境推进到生产环境:code-push promote
  • 运行Production版APP,同步更新,验证其是否正常工作

我们注意到第2步与第4步,需要生成两种APP,其实可以整合,但是却是分平台的,这里以iOS为例。

配置 Debug 和 Release 模式分别对应 Staging 和 Production 部署环境

使用 XCode 配置项目

  • 在项目配置项中选择 Info 选项

  • 找到 Configurations 部分,点击 + 按钮,选择 Duplicate “Release” Configuration,并将新配置项命名为 Staging

image

  • 在项目配置项中选择 Build Settings 选项

  • 找到工具栏区域,点击 + 按钮,选择 Add User-Defined Setting

image

  • 将新的配置命名为 CODEPUSH_KEY 并展开,为 Staging 配置指定 Staging部署环境 key,为 Release配置指定 Production部署环境 key

image

  • 打开项目的 Info.plist,更新 CodePushDeploymentKey值为 $(CODEPUSH_KEY)

image

配置之后,Staging 编译配置用到的是 Staging环境的key,Release 编译配置用到的是 Production环境的Key

CodePush 插件的使用场景

CodePush 插件是由两部分组成:

  • 1、js模块
  • 2、原生API

CodePush 使用场景

下面列出CodePush一些常用的用法,你可以选择一个或者组合使用:

Silent sync on app start

1
2
3
4
// Fully silent update which keeps the app in sync with the server, without ever interrupting the end user
class MyApp extends Component {
}
MyApp = codePush(MyApp);

Silent sync everytime the app resumes

1
2
3
4
5
6
7
// Sync for updates everytime the app resumes.
class MyApp extends Component {
}
MyApp = codePush({
checkFrequency: codePush.CheckFrequency.ON_APP_RESUME,
installMode: codePush.InstallMode.ON_NEXT_RESUME
})(MyApp);

Interactive

1
2
3
4
5
6
7
// Active update, which lets the end user know about each update, and displays it to them immediately after downloading it
class MyApp extends Component {
}
MyApp = codePush({
updateDialog: true,
installMode: codePush.InstallMode.IMMEDIATE
})(MyApp);

Log/display progress

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// Make use of the event hooks to keep track of the different stages of the sync process.
class MyApp extends Component {
codePushStatusDidChange(status) {
switch(status) {
case codePush.SyncStatus.CHECKING_FOR_UPDATE:
console.log("Checking for updates.");
break;
case codePush.SyncStatus.DOWNLOADING_PACKAGE:
console.log("Downloading package.");
break;
case codePush.SyncStatus.INSTALLING_UPDATE:
console.log("Installing update.");
break;
case codePush.SyncStatus.UP_TO_DATE:
console.log("Installing update.");
break;
case codePush.SyncStatus.UPDATE_INSTALLED:
console.log("Update installed.");
break;
}
}

codePushDownloadDidProgress(progress) {
console.log(progress.receivedBytes + " of " + progress.totalBytes + " received.");
}
}
MyApp = codePush(MyApp);

NOTE: While Apple’s developer agreement fully allows performing over-the-air updates of JavaScript and assets (which is what enables CodePush!), it is against their policy for an app to display an update prompt. Because of this, we recommend that App Store-distributed apps don’t enable the updateDialog option when calling sync, whereas Google Play and internally distributed apps (e.g. Enterprise, Fabric, HockeyApp)

CodePush 属性及回调方法

CodePushOptions 选项对象

CodePushOptions 选项对象可以自定义,其参数包括:

(1)checkFrequency (codePush.CheckFrequency)

检查频率,其值包括:

  • codePush.CheckFrequency.ON_APP_START (0)(默认)
  • codePush.CheckFrequency.ON_APP_RESUME (1)
  • codePush.CheckFrequency.MANUAL (2)

(2)deploymentKey (String)

部署环境 key

(3)installMode (codePush.InstallMode)

可选的安装模式,其值包括:

  • codePush.InstallMode.IMMEDIATE (0)(默认)
  • codePush.InstallMode.ON_NEXT_RESTART (1)
  • codePush.InstallMode.ON_NEXT_RESUME (2)

(4)mandatoryInstallMode (codePush.InstallMode)

强制性的安装模式,其值包括:

  • codePush.InstallMode.IMMEDIATE (0)(默认)
  • codePush.InstallMode.ON_NEXT_RESTART (1)
  • codePush.InstallMode.ON_NEXT_RESUME (2)

(5)minimumBackgroundDuration (Number)

只在 InstallMode.ON_NEXT_RESUME 安装模式下有效,设置更新生效的时间间隔,单位为秒

(6)updateDialog (UpdateDialogOptions)

更新弹窗,包含选项:

  • appendReleaseDescription (Boolean) - false
  • descriptionPrefix (String) - “ Description: “
  • mandatoryContinueButtonLabel (String) - “Continue”
  • mandatoryUpdateMessage (String) - “An update is available that must be installed.”
  • optionalIgnoreButtonLabel (String) - “Ignore”
  • optionalInstallButtonLabel (String) - “Install”
  • optionalUpdateMessage (String) - “An update is available. Would you like to install it?”
  • title (String) - “Update available”

codePushStatusDidChange

codePushStatusDidChange (event hook)

status code 产生变化的回调,SyncStatus 所有值:

  • codePush.SyncStatus.CHECKING_FOR_UPDATE (0)
  • codePush.SyncStatus.AWAITING_USER_ACTION (1)
  • codePush.SyncStatus.DOWNLOADING_PACKAGE (2)
  • codePush.SyncStatus.INSTALLING_UPDATE (3)
  • codePush.SyncStatus.UP_TO_DATE (4)
  • codePush.SyncStatus.UPDATE_IGNORED (5)
  • codePush.SyncStatus.UPDATE_INSTALLED (6)
  • codePush.SyncStatus.SYNC_IN_PROGRESS (7)
  • codePush.SyncStatus.UNKNOWN_ERROR (-1)

codePushDownloadDidProgress

codePushDownloadDidProgress (event hook)

更新下载过程回调,包含参数:

  • totalBytes (Number)
  • receivedBytes (Number)

实验调试

效果:程序员视角、终端用户视角

报错:The uploaded package is identical to the contents of the specified deployment’s current release 表示:已经存在与这次上传完全一样的bundle(对应一个版本有两个bundle的md5完全一样),那么CodePush会拒绝此次更新。

index.ios.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
/**
* Sample React Native App
* https://github.com/facebook/react-native
* @flow
*/

import React, { Component } from 'react';
import {
AppRegistry,
StyleSheet,
Alert,
Text,
Image,
View
} from 'react-native';

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

class CodePushDemo extends Component {
codePushStatusDidChange(status) {
switch (status) {
case codePush.SyncStatus.CHECKING_FOR_UPDATE:
console.log("Checking for updates.");
break;
case codePush.SyncStatus.DOWNLOADING_PACKAGE:
console.log("Downloading package.");
break;
case codePush.SyncStatus.INSTALLING_UPDATE:
console.log("Installing update.");
break;
case codePush.SyncStatus.UP_TO_DATE:
console.log("Installing update.");
break;
case codePush.SyncStatus.UPDATE_INSTALLED:
console.log("Update installed.");
break;
}
}

codePushDownloadDidProgress(progress) {
console.log(progress.receivedBytes + " of " + progress.totalBytes + " received.");
}

componentDidMount() {
console.log('组件加载后执行');
//访问慢,不稳定
codePush.checkForUpdate().then((update) => {
if (!update) {
Alert.alert("提示", "已是最新版本", [ {
text: "Ok", onPress: () => {
console.log("最新版本确定");
}
}]);
} else {
codePush.sync({ updateDialog: true, installMode: codePush.InstallMode.IMMEDIATE });
}
});
}

render() {
return (
<View style={styles.container}>
<Text style={styles.welcome}>
Welcome to React Native!
</Text>
<Text style={styles.instructions}>
To get started, edit index.android.js
</Text>
<Text style={styles.instructions}>
Double tap R on your keyboard to reload,{'\n'}
Shake or press menu button for dev menu
</Text>
<Image
style={styles.img}
resizeMode="contain"
source={require('./rnapp.png')}
/>
<Text style={styles.author}>Powered by RNAPP.CC</Text>
</View>
);
}
}

CodePushDemo = codePush(CodePushDemo);

const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: '#F5FCFF',
},
welcome: {
fontSize: 20,
textAlign: 'center',
margin: 10,
},
instructions: {
textAlign: 'center',
color: '#333333',
marginBottom: 5,
},
img: {
height: 60,
width: 332,
marginTop: 20,
},
author: {
justifyContent: 'flex-end',
alignItems: 'center',
marginBottom: 10,
marginTop: 50,
textAlign: 'center',
fontSize: 16,
color: '#3BC1FF',
},
});

AppRegistry.registerComponent('CodePushDemo', () => CodePushDemo);

Powered by AppBlog.CN     浙ICP备14037229号

Copyright © 2012 - 2020 APP开发技术博客 All Rights Reserved.

访客数 : | 访问量 :