终于要集成下rn的热更新了,这边挑选了微软的CodePush服务https://microsoft.github.io/code-push/

首先列一下大体步骤:

  • 本地安装CodePush cli
  • 注册CodePush账号
  • 把要接入的项目加入到你的CodePush中
  • 项目中安装CodePush sdk
  • 加入集成的代码
  • 发布更新版本来测试

上面的1 2 3步骤下面各种教程都有,就只粗略写一下,重点写具体项目集成的

1、2 安装注册

本地安装CodePush cli:

1
npm install -g code-push-cli

注册账号:

1
code-push register

如果直接在官网注册了,输入这个会弹出来token,复制到命令行就ok了

3 把要接入的项目加入到你的CodePush中

要把项目的安卓和ios都分别添加进去

1
2
3
4
5
//ios添加
code-push app add iOSApp ios react-native
//android添加
code-push app add androidApp android react-native

每个app添加的时候,都会输出它的deployment key的表格,Staging和Production,复制下来备用。可以测试用Staging,线上用Production,用哪个都行,后面集成的时候对应好可以。

添加完可以用下面命令查看你添加了哪些app(官网上登录了也可以看到):

1
code-push app list

4 在项目中安装CodePush sdk

首先安装和link,和别的插件步骤一样

1
2
3
4
//安装
npm install react-native-code-push --save
//link
react-native link react-native-code-push

link的时候,会提示让你输入deploymentKey,这个key就是上面添加app后输出的那个表格,选你要的环境的key就好(比如用Staging的话,安卓、ios都输入Staging的key)

link好了之后去看下两个地方:

  • ios的info.plist中CodePushDeploymentKey有没有成功加进去,没有就自己手加下

  • android里android/app/src/main/java/com/…(你app的名字)/MainApplication.java这文件

    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
    //...(各种引入太长了省略)
    //新添加的
    import com.microsoft.codepush.react.CodePush;
    //...(各种引入太长了省略)
    public class MainApplication extends NavigationApplication {
    //新添加的这个getJSBundleFile重写
    @Override
    public String getJSBundleFile(){
    return CodePush.getJSBundleFile();
    }
    @Override
    public boolean isDebug() {
    return BuildConfig.DEBUG;
    }
    protected List<ReactPackage> getPackages() {
    return Arrays.<ReactPackage>asList(
    new RNCameraPackage(),
    new SplashScreenReactPackage(),
    new RNVersionNumberPackage(),
    //新添加的
    new CodePush("h42yJwZPRwunc2KwitUiHZxvNHrda7eba4ac-fdd0-4f23-8f78-1b82ff7d5664",getApplicationContext(),BuildConfig.DEBUG)
    );
    }
    @Override
    public List<ReactPackage> createAdditionalReactPackages() {
    return getPackages();
    }
    @Override
    public String getJSMainModuleName() {
    return "index";
    }
    }

5 集成

(这块我也不是很确定我这样的思路是不是正确的,没有去找教程之类的参考,所以如果有问题或者有更好的解决方式,麻烦指教下~)

首先在App的外面用codepush包起来,配置为手动更新。checkFrequency可以设置更新的方式,如果是自动的话就会自己检测然后下载,静默的,我这边的需求是要提示页面的所以走手动。

1
2
3
4
5
6
7
import codePush from "react-native-code-push";
import App from "./App";
let codePushOptions = { checkFrequency: codePush.CheckFrequency.MANUAL };
const AppContainer = codePush(codePushOptions)(App);
export default AppContainer;

首先我项目中用了react-native-navigation,所以为了拎出来做这块就把热更新的承载页面也作为一个root route。在注册玩所有route后,先检查一下有没有更新版本,有就先render到热更新承载页,再进入具体的更新相关的操作。

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
//...为了篇幅 省略各种import
appRoot = 'loading';
let { store } = configStore();
registerTabs(store, Provider);
handleUpdate();
async function handleUpdate() {
CodePush.notifyAppReady();
const updateMessage = await CodePush.checkForUpdate() || {};
if (updateMessage && !lodash.isEmpty(updateMessage)) {
this.appRoot = 'hot';
renderType();
store.subscribe(onStoreUpdateHot)
} else {
checkToken();
}
}
function checkToken() {
let token = null;
if (store.getState()['auth'] && store.getState()['auth']['loginResult'] && store.getState()['auth']['loginResult']['token']) {
token = store.getState()['auth']['loginResult']['token'];
}
this.appRoot = token ? 'tabs' : 'login';
renderType();
store.subscribe(onStoreUpdate)
}
//监听hotCheck变化 变true表示无需更新 重新流入token流程
function onStoreUpdateHot() {
if (store.getState()['hot'] && store.getState()['hot']['hotCheck']) {
if (this.appRoot === 'hot') {
checkToken();
}
}
}
//监听token变化切换root
function onStoreUpdate() {
let token = null;
if (store.getState()['auth'] && store.getState()['auth']['loginResult'] && store.getState()['auth']['loginResult']['token']) {
token = store.getState()['auth']['loginResult']['token'];
}
const _appRoot = token ? 'tabs' : 'login';
if (this.appRoot !== _appRoot) {
this.appRoot = _appRoot;
renderType();
}
}
function renderType() {
switch (this.appRoot) {
case "tabs":
Navigation.startTabBasedApp({
tabs: [...]
});
break;
case 'login':
Navigation.startSingleScreenApp({
screen: {
screen: 'login',
...
}
});
break;
case 'hot':
Navigation.startSingleScreenApp({
screen: {
screen: 'hot',
...
}
});
break;
default:
console.log("Error")
}
}

检测到有更新包就进入hot这个root,这块儿就处理我们更新相关的逻辑,比如显示下载的进度,成功与否的提示之类的。checkForUpdate和sync这两个api可以详细看文档的解释,checkForUpdate是一个promise,返回了新包的相关参数,sync是触发更新的操作、可以监听整个流程。

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
handleUpdate = async () => {
const updateMessage = await CodePush.checkForUpdate() || {};
await CodePush.sync(
// 配置更新相关
{
// 安装模式 'IMMEDIATE' 立刻安装
installMode: CodePush.InstallMode.IMMEDIATE,
// 强制更新模式
mandatoryInstallMode: CodePush.InstallMode.IMMEDIATE,
updateDialog: {
mandatoryUpdateMessage: '强制更新内容: ' + updateMessage.description,
mandatoryContinueButtonLabel: '强制更新/确认',
optionalIgnoreButtonLabel: '取消',
optionalInstallButtonLabel: '安装',
optionalUpdateMessage: '本次更新内容: ' + updateMessage.description,
title: '发现新版本'
}
},
// 更新的状态监听
//0 已是最新 1 安装完成等待生效 2 忽略更新 3 未知错误 4 已经在下载了 5 查询更新 6 弹出了更新确认界面 7 下载中 8下载完成
(status) => {
console.log(status)
switch (status) {
case 0:
console.log('已经是最新版本');
this.props.setHot(true);
break;
case 1 :
console.log('更新完成, 再启动APP更新即生效');
break;
case 3:
console.log('出错了,未知错误');
break;
case 4:
console.log('已经在下载');
this.setState({updating: true})
break;
case 7:
this.setState({updating: true})
break;
case 8:
this.setState({updating: false})
break;
default:
break;
}
},
// 监听下载过程
({ receivedBytes, totalBytes }) => {
this.setState({
receivedBytes: (receivedBytes / 1024).toFixed(2),
totalBytes: (totalBytes / 1024).toFixed(2)
})
},
);
};

6 发布更新

可以使用release-react这个简化命令发布更新代替使用release,但是所有的参数也是可以配置的,可以仔细读一下文档https://github.com/Microsoft/code-push/tree/master/cli#releasing-updates-react-native 对每个参数都有解释

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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>]

我项目里最后发布的命令如下是这样的,由于我使用的Production key,所以这边deploymentName也换成Production,默认是Staging,targetBinaryVersion不传的话默认会去android/gradle.properties下读,你的文件里没有的话就在命令里自己传进去。

1
2
3
4
//android
code-push release-react androidApp android --deploymentName Production --targetBinaryVersion 1.0.1
//ios
code-push release-react iosApp ios --deploymentName Production --targetBinaryVersion 1.0.1

CodePush.checkForUpdate()会检测对应当前版本的热更新包,比如你本地是1.0.3的包,传的是1.0.2的热更新包,.3是不会检测到这个更新的。更新包的时候可以打各种方式的版本,1.x.x之类的,可以细看文档描述。

发布后可以看下所有发布了包历史:

1
code-push deployment history androidApp/iosApp Production

会显示有多少个client安装了更新包

清除更新包:

1
code-push deployment clear androidApp/iosApp Production

碰到的问题

测试的时候发现,下载好更新包后马上更新的内容是最新的,但是重启app的时候就自动rollback了,但是sync时却返回state为0,已经是最新的包了。后来在issue里找到类似的问题https://github.com/microsoft/react-native-code-push/issues/1564 。在最外面一层加上CodePush.notifyAppReady(),通知一下。

文档里是这样写notifyAppReady这个api的:
Notifies the CodePush runtime that an installed update is considered successful. If you are manually checking for and installing updates (i.e. not using the sync method to handle it all for you), then this method MUST be called; otherwise CodePush will treat the update as failed and rollback to the previous version when the app next restarts.

虽然我们这边是使用的sync,按他的说法应该sync会自动做好这件事情,但是显然没有到达预期,所以手动加一下,重新测试发现的确解决了这个问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//我这边加在了每次进入app的check之前 (全部代码见上面5中)
handleUpdate();
async function handleUpdate() {
//这儿
CodePush.notifyAppReady();
const updateMessage = await CodePush.checkForUpdate() || {};
if (updateMessage && !lodash.isEmpty(updateMessage)) {
this.appRoot = 'hot';
renderType();
store.subscribe(onStoreUpdateHot)
} else {
checkToken();
}
}

参考

官网:https://microsoft.github.io/code-push/

CodePush client sdk文档:https://docs.microsoft.com/en-us/appcenter/distribution/codepush/react-native

各种别人的教程、经验分享:

https://blog.csdn.net/w178191520/article/details/86361785

http://techblog.sishuxuefu.com/atricle.html?5beaa7e59f5454007039e01c

https://www.jianshu.com/p/6a5e00d22723

https://github.com/microsoft/react-native-code-push/issues/1564