本篇文章主要说一下如何利用ReactNativejsbundle机制来实现App的热更新。

前段时间iOS界发生了一件大事,苹果禁止含有热更新或者热修复的APP上架,这里主要是禁止使用runtime的一些特性。但是ReactNative不同,它没有使用到runtime特性,并且好多人已经证实,ReactNative应用依然可以上线。

其实好多应用并不是使用ReactNative直接开发,而是在原生应用的基础上,导入ReactNativeiOS最好使用cocoapods导入,Android使用build.gradle添加依赖包。本篇文章主要包含三个部分:环境搭建、iOS热更新和Android热更新。

环境搭建-macOS

在了解本篇文章之前,你要把ReactNative的环境配好,你可以看这里来配置环境。如果你是macOS系统,这里推荐你一定要装Homebrew,因为HomebrewmacOS下的包管理器,可以安装很多软件。但是,这里并不推荐你使用Homebrew来安装Node,最好使用nvm来安装node,主要是因为nvm是一个强大的node包管理器,在你本机上你可以安装好几个版本的node,并且可以切换任意版本的node。可以使用Homebrew来安装nvm

安装Homebrew

直接在终端执行以下命令即可:

/usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"

因为macOS自带ruby,放在/usr/bin目录下,所以你可以直接使用ruby命令。下面的你可以不实现:(如果你的ruby版本过低,最好别动系统自带的ruby,你可以使用rvm来添加新版本ruby,使用homebrew来安装rvm,重新配置环境变量即可。如果你当前shellbash,则在当前用户目录下修改.bash_profile添加环境变量,如果当前shellzsh,则在当前用户目录下修改.zshrc添加环境变量,我接下来所说的添加环境变量都这种情况,当然你电脑的shell也可能是另外两只情况,但是都一样)

安装nvm

brew install nvm

安装完nvm,必须要配置环境变量,需要在根据你当前的shell在相应的文件中添加如下代码:

export NVM_DIR="$HOME/.nvm"
. "/usr/local/opt/nvm/nvm.sh"

然后可以使用如下命令查看是否配好环境:

nvm --version

安装node

通过nvm ls-remote可以列出所有版本的node,你可以根据你的所需按钮指定版本。

nvm install v6.10.0

安装完node以后通过nvm ls查看当前已经按钮的有哪些版本。node中自带npm,所有你以后可以直接使用npm来安装其他依赖包。

安装react-native-cli

这是ReactNative命令,使用它可以构建ReactNative应用。

npm install -g react-native-cli

-g表示安装到全局模块。

接下来还推荐你安装WatchmanFlow,但是你安装这写不影响开发,安装了更好。它们都可以通过homebrew来安装。

关于IDE,这里推荐Visual Studio CodeWebStorm,至于选哪个看你。


iOS

ReactNative提供了热更新的功能,但是它并没有热更新的能力,我们需要借助其他平台来实现,这里推荐使用微软的CodePush,它专门用来给ReactNativeCordova提供热更新管理。它是一个中央管理库。

创建工程

首先创建一个iOS工程,这里创建一个示例工程名为:CodePushDemo,并添加cocoapods支持。为了方便管理,我们在工程的根目录下创建一个名为ReactNative的文件夹,这里面放置所有关于ReactNative的文件。打开终端,并进入到ReactNative文件夹中。使用下列命令来初始化依赖:

npm init
npm install --save react react-native

至此,你文件夹下会有如下文件:



打开package.json,如下:

{
"name": "CodePushDemo",
"version": "1.0.0",
"private": true,
"scripts": {
"start": "node node_modules/react-native/local-cli/cli.js start",
"test": "jest"
},
"author": "GYD",
"dependencies": {
"react": "^16.0.0-alpha.6",
"react-native": "^0.43.1"
},
"devDependencies": {
"babel-jest": "19.0.0",
"babel-preset-react-native": "1.9.1",
"jest": "19.0.2",
"react-test-renderer": "16.0.0-alpha.6"
},
"jest": {
"preset": "react-native"
}
}

至此,ReactReactNative包我们已经下好了,下面需要装一下code-push-cli,这是CodePush为我们提供的管理工具。

npm install -g code-push-cli

如此,code-push-cli已经装好,使用code-push -v可以查看当前版本。

接下来还需要导入CodePush包。依然使用终端,进入到我们刚刚创建的ReactNative文件夹中:

npm install react-native-code-push

然后你再次打开package.json,你会发现在dependencies字段下多了"react-native-code-push": "^2.0.1-beta"这一句,说明我们的包已经添加好了。

至此,我们说有的ReactNative依赖已经添加好。

添加pod依赖

我们这里使用cocoapods来添加ReactNative的依赖,打开并编辑Podfile文件:

platform :ios, ‘8.0’
target 'CodePushDemo' do
# 导入ReactNative 注意,这里的path路径一定要写对 因为我们的node_modules放在了ReactNative文件夹下。
pod 'React', :path => './ReactNative/node_modules/react-native', :subspecs => [
'Core',
'RCTText',
'RCTNetwork',
'RCTWebSocket', # 这个模块是用于调试功能的
# 在这里继续添加你所需要的模块
]
# 如果你的RN版本 >= 0.42.0,请加入下面这行
pod "Yoga", :path => "./ReactNative/node_modules/react-native/ReactCommon/yoga"
#CodePush 热更新
pod 'CodePush', :path => './ReactNative/node_modules/react-native-code-push'
target 'CodePushDemoTests' do
inherit! :search_paths
end
target 'CodePushDemoUITests' do
inherit! :search_paths
end
end

然后执行pod install,接下来打开工程,看是否编译通过。

在CodePush上创建应用

注册账号

code-push register

你注册完成以后会给你一个key,你需要将这个key输入终端。

向CodePush服务器注册app
为了让CodePush服务器知道你的app,我们需要向它注册app: 在终端输入code-push app add 即可完成注册。



注册完成之后会返回一套deployment key,该key在后面步骤中会用到。

注意,因为CodePush不会区分iOSAndroid,所以我们需要注册两个APP

code-push 关于app的相关命令:

  • code-push app add 在账号里面添加一个新的app
  • code-push app remove 在账号里移除一个app
  • code-push app rename 重命名一个存在app
  • code-push app list 列出账号下面的所有app
  • code-push app transfer 把app的所有权转移到另外一个账号

使用CodePush

首先在Info.plist文件中添加键值对:

<key>CodePushDeploymentKey</key>
<string>zjS1l098BMmScNFNrOl7ZmsAi3VCNylurZJ6M</string>

其中这个key是你在注册app的时候CodePush给你的,我们这里使用Production key。你也可以通过code-push deployment ls <APP_NAME> -k来查看deployment key

当然,在我们测试的时候,要把工程更改为Release模式。

然后创建index.ios.js,如下:

'use strict';
import React , { Component } from 'react';
import {
AppRegistry,
StyleSheet,
Text,
View
} from 'react-native';
//导入热更新
import codePush from "react-native-code-push";
export default class CodePushDemo extends Component {
render() {
return (
<View style={styles.container}>
<Text style={styles.highScoresTitle}>
测试
</Text>
</View>
);
}
componentDidMount(){
//检查是否有信版本
codePush.sync();
}
}
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: '#FFFFFF',
},
highScoresTitle: {
fontSize: 20,
textAlign: 'center',
margin: 10,
}
});
// 整体js模块的名称
AppRegistry.registerComponent('CodePushDemo', () => CodePushDemo);

首先要导入热更新模块import codePush from "react-native-code-push";,而且我们一般再componentDidMount方法中调用sync放,后台请求更新。

我们在原生里面需要这么使用:

#import "RNViewController.h"
#import <React/RCTRootView.h>
#import <React/RCTBundleURLProvider.h>
#import <CodePush/CodePush.h>
@interface RNViewController ()
@end
@implementation RNViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.view.backgroundColor = [UIColor whiteColor];
self.title = @"RN界面";
NSURL *jsCodeLocation;
jsCodeLocation = [CodePush bundleURL];
RCTRootView *view = [[RCTRootView alloc] initWithBundleURL:jsCodeLocation moduleName:@"CodePushDemo" initialProperties:nil launchOptions:nil];
view.frame = self.view.bounds;
[self.view addSubview:view];
}
@end

我们知道,ReactNative是以bundle的形式加载界面的,所以,我们需要向CodePush提交我们的新Bundle

js打包成Bundle的命令是:

react-native bundle --platform 平台 --entry-file 启动文件 --bundle-output 打包js输出文件 --assets-dest 资源输出目录 --dev 是否调试。
react-native bundle --platform ios --entry-file index.ios.js --bundle-output ./bundle/main.jsbundle --dev false

最终打好的包如下:



注意,这里有一个坑,在你首次打好包以后,或者你重新上传appappstore的时候,你需要先把这个main.jsbundle导入到工程中,但是以后更新bundle的时候就不用再次导了,总之,需要保证你的工程中有一个main.jsbundle

接下来,需要把我们新打好的bundle上传到CodePush

code-push release <应用名称> <Bundles所在目录> <对应的应用版本> --deploymentName: 更新环境 --description: 更新描述 --mandatory: 是否强制更新
code-push release CodePushDemo ./bundle/main.jsbundle 1.0.0 --deploymentName Production --description "1.0" --mandatory true

注意:

  • CodePush默认是staging环境,这里我们切换到Production环境
  • 如果有mandatoryCodePush会根据mandatorytruefalse来控制应用是否强制更新。默认情况下mandatoryfalse即不强制更新。
  • 对应的应用版本(targetBinaryVersion)是指当前app的版本,也就是说此次更新的bundle对应的是app的那个版本。不要将其理解为这次js更新的版本。
  • 如果我们要对某一个应用版本进行多次更新,只需要上传与上次不同的bundle即可

在终端输入 code-push deployment history <appName> Production 可以看到Production版本更新的时间、描述等等属性。

应用启动之后,从CodePush服务器查询更新,并下载到本地,下载好之后跟新界面。

更多部署命令:

  • code-push deployment rm 删除部署

接下来我们再来看看codePush.sync()方法,它可以传如下几种参数:

  • deploymentKey (String): 部署key,指定你要查询更新的部署秘钥,默认情况下该值来自于Info.plist(iOS)和MianActivity.java(Android)文件,你可以通过设置该属性来动态查询不同部署key下的更新。
  • installMode (codePush.InstallMode): 安装模式,用在向CodePush推送更新时没有设置强制更新(mandatory为true)的情况下,默认codePush.InstallMode.ON_NEXT_RESTART即下一次启动的时候安装。
  • mandatoryInstallMode (codePush.InstallMode):强制更新,默认codePush.InstallMode.IMMEDIATE。
  • minimumBackgroundDuration (Number):该属性用于指定app处于后台多少秒才进行重启已完成更新。默认为0。该属性只在installMode为InstallMode.ON_NEXT_RESUME情况下有效。

至此,iOS热更新已经说完。


Android

创建工程

首先,我们依然需要创建package.json,并添加依赖包。最终的工程目录为:



接下来配置Android工程,添加依赖。这里我们先添加ReactNative依赖,这里面的坑还是不少的。

添加ReactNative依赖

在你的app中 build.gradle 文件中添加 React Native 依赖:

dependencies {
...
compile 'com.facebook.react:react-native:0.43.1'
}
android {
...
configurations.all {
resolutionStrategy.force 'com.google.code.findbugs:jsr305:1.3.9'
}
}

react-native后面的版本是你当前package.json内的react-native版本。在这里我们还添加了configurations.all,这里是为了兼容appcompat库。

然后在项目的build.gradle文件中为ReactNative添加一个maven依赖的入口,必须写在"allprojects"代码块中:

allprojects {
repositories {
jcenter()
maven {
// All of React Native (JS, Android binaries) is installed from npm
url "$rootDir/../ReactNative/node_modules/react-native/android"
}
}
}

添加完依赖后,新建一个RNActivity。在MainActivity中添加一个按钮,按钮点击打开RNActivity,在RNActivity里面的添加ReactNative界面。由于需要一些权限问题,所以一定要在AndroidManifest.xml文件中添加如下权限:

<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
<uses-permission android:name="android.permission.SYSTEM_OVERLAY_WINDOW"/>

因为SYSTEM_OVERLAY_WINDOW是运行时权限,所以我们要在按钮点击的时候去添加权限。MainActivity如下:

public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Button button = (Button) findViewById(R.id.button);
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if (Build.VERSION.SDK_INT >= 23) {
if (!Settings.canDrawOverlays(MainActivity.this)) {
//开启权限
Intent intent = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION);
startActivity(intent);
}else {
openRNActivity();
}
}else {
openRNActivity();
}
}
});
}
private void openRNActivity() {
Intent intent = new Intent(MainActivity.this,RNActivity.class);
startActivity(intent);
}
}

再来看RNActivity,因为我们是将ReactNative作为一个子界面放在布局中的,所以我们直接在RNActivity的布局文件中添加ReactRootView布局,布局如下:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.guiyongdong.codepushdemoandroid.RNActivity">
<com.facebook.react.ReactRootView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/rn_layout"/>
</LinearLayout>

然后看RNActivity.java

public class RNActivity extends AppCompatActivity implements DefaultHardwareBackBtnHandler {
private ReactRootView mReactRootView;
private ReactInstanceManager mReactInstanceManager;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_rn);
mReactRootView = (ReactRootView) findViewById(R.id.rn_layout);
mReactInstanceManager = ReactInstanceManager.builder()
.setCurrentActivity(this)
.setApplication(getApplication())
.setBundleAssetName("index.android.bundle")
.setJSMainModuleName("index.android")
.addPackage(new MainReactPackage())
.setUseDeveloperSupport(BuildConfig.DEBUG)
.setInitialLifecycleState(LifecycleState.RESUMED)
.build();
mReactRootView.startReactApplication(mReactInstanceManager,"CodePushDemoAndroid",null);
}
@Override
public void invokeDefaultOnBackPressed() {
super.onBackPressed();
}
@Override
protected void onPause() {
super.onPause();
if (mReactInstanceManager != null) {
mReactInstanceManager.onHostPause(this);
}
}
@Override
protected void onResume() {
super.onResume();
if (mReactInstanceManager != null) {
mReactInstanceManager.onHostResume(this, this);
}
}
@Override
protected void onDestroy() {
super.onDestroy();
if (mReactInstanceManager != null) {
mReactInstanceManager.onHostDestroy();
}
}
@Override
public void onBackPressed() {
if (mReactInstanceManager != null) {
mReactInstanceManager.onBackPressed();
} else {
super.onBackPressed();
}
}
}

ReactRootView就是我们的ReactNative界面,ReactInstanceManager主要是去加载bundle的,注意,这里的名字一定要和js中注册的名字一样。我们再来看index.android.js文件:

'use strict';
import React , { Component } from 'react';
import {
AppRegistry,
StyleSheet,
Text,
View,
Image
} from 'react-native';
export default class CodePushDemoAndroid extends Component {
render() {
return (
<View style={styles.container}>
<Text style={styles.highScoresTitle}>
我是ReactNative界面
</Text>
</View>
);
}
}
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: '#FFFFFF',
},
highScoresTitle: {
fontSize: 20,
textAlign: 'center',
margin: 10,
}
});
// 整体js模块的名称
AppRegistry.registerComponent('CodePushDemoAndroid', () => CodePushDemoAndroid);

最终运行结果如下:



至此,我们Android原生项目导入ReactNative已经完成,是不是感觉比iOS坑太多了。
下面我们再来看如何导入CodePush

添加CodePush依赖

在使用CodePush之前,我们依然要在CodePush平台上添加一个APP,获取key。这些过程和iOS一样,这里就不再说了。

appbuild.gradle文件里面添如下代码:

apply from: "../../ReactNative/node_modules/react-native-code-push/android/codepush.gradle"
dependencies {
...
compile project(':react-native-code-push')
}

注意,这里的路径一定要根据实际情况来写。比如我的node_modules是放在了和工程目录同一目录下的ReactNative文件夹下。

然后在工程下的settings.gradle中添加如下代码:

include ':react-native-code-push'
project(':react-native-code-push').projectDir = new File(rootProject.projectDir, '../ReactNative/node_modules/react-native-code-push/android/app')

这里是将react-native-code-push模块导入到工程中,作为一个子模块。

注意,你Android工程的buildToolsVersion版本可能比react-native-code-push的版本高,所以你需要修改react-native-code-push的版本。

至此,CodePush的环境算是配好了。接下来看如何使用。

在这里,你只需要修改RNActivity.javaonCreate方法就行:

public class RNActivity extends AppCompatActivity implements DefaultHardwareBackBtnHandler {
private ReactRootView mReactRootView;
private ReactInstanceManager mReactInstanceManager;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_rn);
// 这里的key要替换成你自己的key
CodePush codepush = new CodePush("_lVphT2Ox1tyCFRIhzVrBoTcGwKbNylurZJ6M",getApplication(),BuildConfig.DEBUG);
// 本地bundle的路径
String path = CodePush.getJSBundleFile();
mReactRootView = (ReactRootView) findViewById(R.id.rn_layout);
mReactInstanceManager = ReactInstanceManager.builder()
.setCurrentActivity(this)
.setApplication(getApplication())
.setJSBundleFile(path)
// .setBundleAssetName("index.android.bundle")
// .setJSMainModuleName("index.android")
.addPackage(new MainReactPackage())
.addPackage(codepush)
.setUseDeveloperSupport(BuildConfig.DEBUG)
.setInitialLifecycleState(LifecycleState.RESUMED)
.build();
mReactRootView.startReactApplication(mReactInstanceManager,"CodePushDemoAndroid",null);
}
...
}

可以看出来,之前我们是使用setBundleAssetName("index.android.bundle")setJSMainModuleName("index.android")方法去加载本地bundle,现在我们使用setJSBundleFile(path)并且还添加了addPackage(codepush)。好了,现在这个界面就可以随时更新了。

总结

其实,ReactNative为什么能热更新?主要是因为我们使用js将代码写好,然后使用ReactNativejs代码打包成jsbundle,而真正去执行代码的就是这个jsbundle,只要你的app支持ReactNative环境,就可以去加载jsbundle,而这个jsbundle其实是放在本地的。所以,你完全可以自己搭一个服务器,去判断远端是否有新bundle,如果有,那么下载下来,替换本地的bundle,等到下次运行的时候就可以更新的。当然,你也可以在下载完以后才去加载这个bundle。我们使用CodePush,只是因为它版本的控制等做的比较好。