本篇文章主要说一下如何利用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
,只是因为它版本的控制等做的比较好。