本篇文章主要说一下如何利用ReactNative的jsbundle机制来实现App的热更新。
前段时间iOS界发生了一件大事,苹果禁止含有热更新或者热修复的APP上架,这里主要是禁止使用runtime的一些特性。但是ReactNative不同,它没有使用到runtime特性,并且好多人已经证实,ReactNative应用依然可以上线。
其实好多应用并不是使用ReactNative直接开发,而是在原生应用的基础上,导入ReactNative。iOS最好使用cocoapods导入,Android使用build.gradle添加依赖包。本篇文章主要包含三个部分:环境搭建、iOS热更新和Android热更新。
环境搭建-macOS 在了解本篇文章之前,你要把ReactNative的环境配好,你可以看这里 来配置环境。如果你是macOS系统,这里推荐你一定要装Homebrew,因为Homebrew是macOS下的包管理器,可以安装很多软件。但是,这里并不推荐你使用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,重新配置环境变量即可。如果你当前shell是bash,则在当前用户目录下修改.bash_profile添加环境变量,如果当前shell是zsh,则在当前用户目录下修改.zshrc添加环境变量,我接下来所说的添加环境变量都这种情况,当然你电脑的shell也可能是另外两只情况,但是都一样)
安装nvm
安装完nvm,必须要配置环境变量,需要在根据你当前的shell在相应的文件中添加如下代码:
export NVM_DIR="$HOME/.nvm"
. "/usr/local/opt/nvm/nvm.sh"
然后可以使用如下命令查看是否配好环境:
安装node
通过nvm ls-remote可以列出所有版本的node,你可以根据你的所需按钮指定版本。
安装完node以后通过nvm ls查看当前已经按钮的有哪些版本。node中自带npm,所有你以后可以直接使用npm来安装其他依赖包。
安装react-native-cli
这是ReactNative命令,使用它可以构建ReactNative应用。
npm install -g react-native-cli
-g表示安装到全局模块。
接下来还推荐你安装Watchman和Flow,但是你安装这写不影响开发,安装了更好。它们都可以通过homebrew来安装。
关于IDE,这里推荐Visual Studio Code和WebStorm,至于选哪个看你。
iOS ReactNative提供了热更新的功能,但是它并没有热更新的能力,我们需要借助其他平台来实现,这里推荐使用微软的CodePush,它专门用来给ReactNative和Cordova提供热更新管理。它是一个中央管理库。
创建工程 首先创建一个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"
}
}
至此,React和ReactNative包我们已经下好了,下面需要装一下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上创建应用 注册账号
你注册完成以后会给你一个key,你需要将这个key输入终端。
向CodePush服务器注册app 为了让CodePush服务器知道你的app,我们需要向它注册app: 在终端输入code-push app add 即可完成注册。
注册完成之后会返回一套deployment key,该key在后面步骤中会用到。
注意,因为CodePush不会区分iOS和Android,所以我们需要注册两个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 ,
}
});
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
最终打好的包如下:
注意,这里有一个坑,在你首次打好包以后,或者你重新上传app到appstore的时候,你需要先把这个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环境
如果有mandatory则CodePush会根据mandatory是true或false来控制应用是否强制更新。默认情况下mandatory为false即不强制更新。
对应的应用版本(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 ,
}
});
AppRegistry.registerComponent('CodePushDemoAndroid' , () => CodePushDemoAndroid);
最终运行结果如下:
至此,我们Android原生项目导入ReactNative已经完成,是不是感觉比iOS坑太多了。 下面我们再来看如何导入CodePush。
添加CodePush依赖 在使用CodePush之前,我们依然要在CodePush平台上添加一个APP,获取key。这些过程和iOS一样,这里就不再说了。
在 app的build.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.java中onCreate方法就行:
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);
CodePush codepush = new CodePush("_lVphT2Ox1tyCFRIhzVrBoTcGwKbNylurZJ6M" ,getApplication(),BuildConfig.DEBUG);
String path = CodePush.getJSBundleFile();
mReactRootView = (ReactRootView) findViewById(R.id.rn_layout);
mReactInstanceManager = ReactInstanceManager.builder()
.setCurrentActivity(this )
.setApplication(getApplication())
.setJSBundleFile(path)
.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将代码写好,然后使用ReactNative将js代码打包成jsbundle,而真正去执行代码的就是这个jsbundle,只要你的app支持ReactNative环境,就可以去加载jsbundle,而这个jsbundle其实是放在本地的。所以,你完全可以自己搭一个服务器,去判断远端是否有新bundle,如果有,那么下载下来,替换本地的bundle,等到下次运行的时候就可以更新的。当然,你也可以在下载完以后才去加载这个bundle。我们使用CodePush,只是因为它版本的控制等做的比较好。