React Native 有赞初探
对于移动客户端开发一般需要同时考虑 iOS
和 Android
两个平台,而这两个平台需要两种完全不同的编程语言来开发,React Native
的出现就可以在很大程度上满足一次开发双平台适用,本文重点介绍 iOS
和 Android
两个平台下如何配置 React Native
环境,原生和 React Native
的通信以及 React Native
热部署,而 JavaScript
编写的 UI 基本上可以双平台共用,UI 部分在 React Native
官网也有详细的介绍,本文就不再做重点介绍。
以下讨论的所有问题都是基于 0.33.0 版本,因为
React Native
现在版本更新很快,临近的版本之间也有很大差异,所以其他版本的就不再单独指出其差异。
1. 接入原生工程
1.1 Android 平台
1.1.1 接入前的配置
先在工程目录下新建一个名为 package.json
文件,文件基本内容为:
{
"name": "react-native-module",
"version": "0.0.1",
"private": true,
"scripts": {
"start": "node node_modules/react-native/local-cli/cli.js start"
},
"dependencies": {
"react": "^15.3.1",
"react-native": "^0.33.0",
},
"main": "index.android.js",
"devDependencies": {},
"author": "",
"license": "ISC",
"description": "Your app name",
"repository": {
"type": "git",
"url": "git@gitlab.qima-inc.com:app/your_app.git"
}
}
在原生工程的根目录下,依次执行下面的命令:
$ npm init
$ npm install --save react react-native
$ curl -o .flowconfig https://raw.githubusercontent.com/facebook/react-native/master/.flowconfig
在 app 目录下的 build.gradle
文件中添加 React Native
的依赖
dependencies {
...
compile "com.facebook.react:react-native:+"
}
然后在工程目录下的 build.gradle
文件添加 maven
目录:
allprojects {
repositories {
mavenLocal() // 这行配置绝对不能少,并且要在第一行,不然下载的版本就会是0.30以下
jcenter()
maven {
url "$projectDir/../node_modules/react-native/android"
}
}
}
这里的 url
路径,将取决于你放置 node_modules
的位置(你可以根据需要选择放置在其他地方)
1.1.2 常见的错误和异常
到此需要完成的配置基本上算结束了,但是这过程中有可能会出现一些异常,下面列举几种常见的问题。
1. 问:构建工程失败,错误信息:
SDK location not found. Define location with sdk.dir in the local.properties file or with an ANDROID_HOME environment variable.
答:这个问题应该不用我说怎么解决了吧,如果真不会的话,请 Google
2. 问:
android/app/src/main/assets/index.android.bundle: No such file or Warning: directory或者手机上提示The development server returned response error code: 500
答:在 android/app/src/main/ 下建立 assets 文件夹,然后执行下面一行命令:
"http://localhost:8081/index.android.bundle?platform=android" -o "android/app/src/main/assets/index.android.bundle"
3. 问:
java.lang.IllegalAccessError: Method 'void android.support.v4.net.ConnectivityManagerCompat.()' is inaccessible to class 'com.facebook.react.modules.netinfo.NetInfoModule' (declaration of 'com.facebook.react.modules.netinfo.NetInfoModule' appears in /data/app/package.name-2/base.apk)
或者 Could not find com.facebook.react:react-native:0.xx.x
答:记得在工程目录下的 build.gradle 文件中添加如下配置
mavenLocal()
jcenter()
maven {
url "$projectDir/../node_modules/react-native/android"
}
4. 问:
Can't find variable: __fbBatchedBridge in android release
答:在工程根目录下输入命令:
react-native bundle --platform android --dev true --entry-file index.android.js --bundle-output app/src/main/assets/index.android.bundle --assets-dest app/src/main/res/
1.1.3 接入 React Native
(1) Application
实现 ReactApplication
接口
public class YourApplication extends BaseApplication implements ReactApplication{
private final ReactNativeHost mReactNativeHost = new ReactNativeHost(this) {
@Override
protected boolean getUseDeveloperSupport() {
return BuildConfig.DEBUG;
}
@Override
protected List<ReactPackage> getPackages() {
return Arrays.<ReactPackage>asList(
new MainReactPackage()
);
}
};
@Override
public ReactNativeHost getReactNativeHost() {
return mReactNativeHost;
}
}
(2) 创建 Activity
并继承 ReactActivity
public class GiftReactActivity extends ReactActivity {
/**
* Returns the name of the main component registered from JavaScript.
* This is used to schedule rendering of the component.
*/
@Override
protected String getMainComponentName() {
return "ReactNativeApp";
}
}
getMainComponentName
方法返回的字符串很重要,它决定了需要显示加载的 js 文件,如何对应,接下来在介绍 js 文件的时候还会提到。
(3) 在工程目录下建立 index.android.js
文件
我们以经典的 Hello world 开始,代码如下:
import React, { Component } from 'react';
import { AppRegistry, Text } from 'react-native';
class HelloWorldApp extends Component {
render() {
return (
<Text style={styles.bigblue}>Hello world!</Text>
<Text style={styles.bigblue, styles.red}>Hello world!</Text>
);
}
}
const styles = StyleSheet.create({
bigblue: {
color: 'blue',
fontWeight: 'bold',
fontSize: 30,
},
red: {
color: 'red',
},
});
AppRegistry.registerComponent('ReactNativeApp', () => HelloWorldApp);
此处的
ReactNativeApp
必须和步骤 (2) 中的提到的字符串保持一致,否则Activity
无法找到对应的 js 文件
这段代码比较简单,只有一个 Text
控件,和 Android
原生控件 TextView
类似,显示内容直接添加到 Text 标签中间即可。更改 Text
属性是通过 style
方式实现的,在 style
中定义各种类 css
属性,比如上面代码中就设置了文字的颜色、大小和加粗,调用方式为 style={styles.bigblue}
。
(4) 清单文件添加声明
到 Android
清单文件 AndroidManifest.xml
添加以下内容(省略了无关部分):
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="com.qima.kdt">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
<application ... >
...
<activity android:name=".business.react.GiftReactActivity"/>
<activity android:name="com.facebook.react.devsupport.DevSettingsActivity" />
</application>
</manifest>
其中 GiftReactActivity
为承载 React Native
页面的业务 Activity
, DevSettingsActivity
则是以下这个 Dev Settings
的界面:
在真机环境下,打开 app 到 React Native
页面,摇动手机会出现如下页面:
数字 1 菜单表示重新加载 js 文件,这个功能在开发过程中非常实用,如果只是更改了 js 文件,那么只需要点击 Reload 菜单即可立马看到更改后的效果。
点击数字 2 菜单即可进入上文提到的 DevSettingActivity
界面。
(5) 到 package.json
的位置,打开命令行,运行 packager server:
npm start 或者 react-native start
至此就可以在手机上看到 React Native
页面了。
(6) debug
模式需要电脑端时刻运行 package server
,方便调试,如果想要打 release
包,可以运行如下命令,这样 React Native
页面加载就不需要电脑,直接从本地加载显示。但是 release
包如果需要更改原生或者 js 文件,都需要重新安装 apk 文件。
react-native bundle --platform android --dev true --entry-file index.android.js --bundle-output app/src/main/assets/index.android.bundle --assets-dest app/src/main/res/
1.2 iOS 平台
1.2.1 接入前的配置
iOS 接入 React Native 相对要简单一些,需要借助 Cocoapods
,因此必须首先安装 Cocoapods
。
$ sudo gem install cocoapods
(1) 添加 Package
依赖
在项目根目录下建立 package.json
,添加相关的 package
依赖,示例如下:
{
"name": "Koudaitong",
"version": "0.0.1",
"private": true,
"scripts": {
"start": "node node_modules/react-native/local-cli/cli.js start"
},
"dependencies": {
"react": "15.3.1",
"react-native": "0.33.0",
"react-native-swipeout": "^2.0.12"
}
}
(2) Packages 安装
通过 Node 包管理器来安装 React Native
,Node
模块将被安装到根目录的 node_modules/
目录下。
$ npm install
(3) 安装 React Native Framework
在 Podfile
末尾添加如下配置:
pod 'React', :path => '../node_modules/react-native', :subspecs => [
'Core',
'RCTText',
'RCTImage',
'RCTNetwork',
'RCTWebSocket', # needed for debugging
# Add any other subspecs you want to use in your project
]
(4) 安装 Pod
$ pod install
1.2.2 代码集成
(1) 创建 index.ios.js
文件
在根目录下创建 index.ios.js
文件,我们还以经典的 Hello World 为例,代码如下:
import React, { Component } from 'react';
import { AppRegistry, Text } from 'react-native';
class HelloWorldApp extends Component {
render() {
return (
<View>
<Text style={styles.bigblue}>Hello world!</Text>
<Text style={styles.bigblue, styles.red}>Hello world!</Text>
</View>
);
}
}
const styles = StyleSheet.create({
bigblue: {
color: 'blue',
fontWeight: 'bold',
fontSize: 30,
},
red: {
color: 'red',
},
});
AppRegistry.registerComponent('ReactNativeApp', () => HelloWorldApp);
(2) Objective-C
代码接入
我们需要用 React Native packager
来创建 index.ios.bundle
,并通过 React Native server
来托管,因此需要借助 RCTRootView
来判断 index.ios.bundle
来源。
首先需要在要接入 React Native
页面的 ViewController
中导入 RCTRootView.h
,然后在 viewDidLoad
中添加如下代码:
NSURL *jsCodeLocation =
[NSURL URLWithString:@"http://172.17.8.76:8081/index.ios.bundle?platform=ios"];
RCTRootView *rootView = [[RCTRootView alloc] initWithBundleURL:jsCodeLocation
moduleName:@"navigation"
initialProperties:@{@"name": @"gift"}
launchOptions:nil];
[rootView setFrame:CGRectMake(0, 0, CGRectGetWidth(self.view.frame), CGRectGetHeight(self.view.frame))];
rootView.backgroundColor = UIColorFromRGB(tableBg);
self.view = rootView;
(3) 到 package.json
的位置,打开命令行,运行 packager server
:
npm start 或者 react-native start
至此就可以在模拟器上看到 React Native
页面了。
2. React Native
与原生通信
有时候 App 需要访问平台 API
,但 React Native
可能还没有相应的模块包装;或者你需要复用一些 Java
代码,而不是用 JavaScript
重新实现一遍;又或者你需要实现某些高性能的、多线程的代码,譬如图片处理、数据库、或者各种高级扩展等等。
不管是在 React Native
层面封装 Java 原生控件,还是实现两种编程语言的信息互通,都有助于 React Native
实现一些复杂的功能,并且还可以达到原生的体验效果。
2.1 Android
平台
2.1.1 React Native
主动发信息
Android
平台下 React Native
发信息模型如下:
接下来以 AlertDialog
为例说明如何在 React Native
层面实现原生的 Dialog
效果,首先需要创建一个原生模块。一个原生模块是一个继承了 ReactContextBaseJavaModule
的 Java 类,它可以实现一些 JavaScript
所需的功能。最终调用的效果如下:
var options = {
'title': '',
'message': '确认删除吗?',
'positive': '确定',
'negative': '取消'
}
var positiveCallback = () => {
var segmentSelect = that.state.segmentSelect
var select = segmentSelect? 0:1
console.log(id);
GiftRequest.deleteGift(segmentSelect, id,
() => {
ToastAndroid.show('删除成功', ToastAndroid.SHORT)
this._getData(select)
},
() => {
ToastAndroid.show('删除失败', ToastAndroid.SHORT)
}
)
}
intentModule.showDialog(options, positiveCallback)
针对上述代码需要几点说明:
options
包含了对话框的所有文案信息,当然这些 key 值都是提前约定好的,下文会提及Java
原生如何保证这些约定。positiveCallback
是Dialog
确定按钮点击事件的回调方法,本例中点击确定按钮执行删除操作。intentModule.showDialog
是 Java 中预先写好的方法,下文中会提及该方法的定义- 需要补充一点,在 js 文件中还需要声明
NativeModule
,该模块负责与原生建立连接,代码如下:
import {
AppRegistry,
StyleSheet,
NativeModules,
View
} from 'react-native';
var intentModule = NativeModules.ZanIntentModule;
以上就是 React Native
需要做的事情,接下来就是原生 Java
代码需要支持的操作,再说原生操作之前,还需要明确一点就是 React Native
与 Java
通信的数据结构,比如说上述例子中的 options
和 positiveCallback
分别在 Java
中对应什么数据结构。官方给出的数据结构对照关系如下:
Boolean -> Bool
Integer -> Number
Double -> Number
Float -> Number
String -> String
Callback -> function
ReadableMap -> Object
ReadableArray -> Array
左侧对应的是 Java
的数据结构,右侧对应的是 JavaScript
的数据结构,其中 Callback
、 ReadableMap
和 ReadableArray
是 React Native
框架提供的数据类型,具体用法下文会重点提及。
在上文 1.3(1) 中提及了 Application
需要实现 ReactApplication
,并且需要实现其中几个方法,其中 getPackages()
还需要添加自定义的 IntentReactPackage
,代码如下:
@Override
protected List<ReactPackage> getPackages() {
return Arrays.<ReactPackage>asList(
new MainReactPackage(),
new IntentReactPackage()
);
}
IntentReactPackage
类需要实现 ReactPackage
接口,然后在其接口方法中添加自定义的通信模块,代码如下:
public class IntentReactPackage implements ReactPackage {
@Override
public List<NativeModule> createNativeModules(ReactApplicationContext reactContext) {
List<NativeModule> modules = new ArrayList<>();
modules.add(new ZanIntentModule(reactContext));
return modules;
}
@Override
public List<Class<? extends JavaScriptModule>> createJSModules() {
return Collections.emptyList();
}
@Override
public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) {
return Collections.emptyList();
}
}
重点来看 createNativeModules
方法,其他两个方法暂时不需要关心,默认就好了,在 createNativeModules
方法中添加了自定义的 ZanIntentModule
类,这个类就是我们需要暴露给 React Native
的方法,接下来重点来看这个类。
public class ZanIntentModule extends ReactContextBaseJavaModule {
public static final String TAG = "ZanIntentModule";
@Override
public String getName() {
return "ZanIntentModule";
}
/**
* 弹出对话框
*/
@ReactMethod
public void showDialog(final ReadableMap map, final Callback positiveCallback) {
Activity currentActivity = getCurrentActivity();
if (currentActivity == null) return;
new AlertDialog.Builder(currentActivity)
.setCancelable(true)
.setTitle(map.getString("title"))
.setMessage(map.getString("message"))
.setPositiveButton(map.getString("positive"), new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
positiveCallback.invoke();
}
})
.setNegativeButton(map.getString("negative"), null)
.show();
}
}
关于这段代码需要几点说明:
getName()
方法需要返回一个标识字符串,该字符串必须要和 js 文件调用处保持一致,上文中关于React Native
提到的声明中var intentModule = NativeModules.ZanIntentModule;
这里的ZanIntentModule
就必须要和getName()
方法的返回值保持一致。showDialog
方法就是上文中调用显示对话框的方法,该方法必须要用@ReactMethod
注解标识,负责系统无法识别该方法是为JavaScript
提供的接口方法。showDialog
方法的形参对应了上文 js 文件中的调用参数,options
对应此处的ReadableMap
,positiveCallback
对应此处的Callback
,上文提到options
中 key 值的约定就体现在ReadableMap
的key
值,两者必须要严格对应。- 关于
ReadableMap
我们可以看一下官方都提供了那些方法:
package com.facebook.react.bridge;
/**
* Interface for a map that allows typed access to its members. Used to pass parameters from JS to
* Java.
*/
public interface ReadableMap {
boolean hasKey(String name);
boolean isNull(String name);
boolean getBoolean(String name);
double getDouble(String name);
int getInt(String name);
String getString(String name);
ReadableArray getArray(String name);
ReadableMap getMap(String name);
ReadableType getType(String name);
ReadableMapKeySetIterator keySetIterator();
}
看完 ReadableMap
提供的方法应该会对上文提到的数据结构对应表有更直观的理解了吧。
利用上面的数据结构对应类型,可以定义一个通用的参数传递机制,先来看代码:
/**
* 从js页面跳转到原生Activity, 同时也可以从js传递相关数据到原生页面
* @param map
*/
@ReactMethod
public void startActivityFromJS(String name, ReadableMap map){
try{
Activity currentActivity = getCurrentActivity();
if(null != currentActivity) {
Class toActivity = Class.forName(name);
Intent intent = new Intent(currentActivity, toActivity);
// 封装传递参数
ReadableMapKeySetIterator iterator = map.keySetIterator();
while (iterator.hasNextKey()){
String key = iterator.nextKey();
if (ReadableType.Number.equals(map.getType(key))){
intent.putExtra(key, map.getInt(key));
} else if (ReadableType.String.equals(map.getType(key))){
intent.putExtra(key,map.getString(key));
} else if (ReadableType.Boolean.equals(map.getType(key))){
intent.putExtra(key,map.getBoolean(key));
}
}
currentActivity.startActivity(intent);
}
}catch(Exception e){
throw new JSApplicationIllegalArgumentException(
"can not open Activity : " + e.getMessage());
}
}
这是一个根据 js 传递的参数来决定跳转方向以及跳转参数的方法,为了尽量做到通用性,首先判断要跳转的 Activity
名字,然后遍历 ReadableMap
,将 key
和 value
分别填充到 Intent
的 bundle
中,由于 ReadableMap
数字类型只有 Number
,Java
的 Intent
中对应的有 int
、 long
、 float
三种,这里暂时没有做区分。
以上是 Android
平台下 React Native
与原生的通信,但是如果原生需要主动给 js 文件发信息,上面的方法就没法实现了,那 js 主动发信息该如何实现呢?
2.1.2 原生主动发信息
React Native
提供了 RCTDeviceEventEmitter
来实现原生主动发信息,本质上来说就是利用事件总线进行分发。
具体实现方法也很简单,参考下面的代码:
WritableMap params = Arguments.createMap();
params.putString(CONFIG_CONTENT, data.getStringExtra(CONFIG_CONTENT));
params.putInt(CONFIG_CONTENT_ID, data.getIntExtra(CONFIG_CONTENT_ID, 0));
params.putInt(CONFIG_CONTENT_LIST_ID, data.getIntExtra(CONFIG_CONTENT_LIST_ID, 0));
params.putString(CONFIG_CONTENT_NICK_NAME, data.getStringExtra(CONFIG_CONTENT_NICK_NAME));
if (mReactContext.hasActiveCatalystInstance()){
mReactContext.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)
.emit("updateData", params);
}
以上代码完成了原生端发送的操作,发送主要是调用 ReactContext
的 getJSModule
和 emit
方法,其中 "updateData" 表示发送事件名称,params
表示发送的数据封装,查看 emit
源码可以看到数据类型为 Object
,因此可以封装任何数据类型。
@SupportsWebWorkers
public interface RCTDeviceEventEmitter extends JavaScriptModule {
void emit(String eventName, @Nullable Object data);
}
2.2 iOS
平台
2.2.1 React Native
主动发信息
以 React Native
控制原生 ViewController
跳转为例,说明 React Native
如何主动发信息。在 React Native
中,一个“原生模块”就是一个实现了“RCTBridgeModule”协议的 Objective-C
类,代码如下:
#import "RCTBridgeModule.h"
@interface ZanIntentModule : NSObject <RCTBridgeModule>
@end
为了实现 RCTBridgeModule
协议,类需要包含 RCT_EXPORT_MODULE()
宏。这个宏也可以添加一个参数用来指定在 JavaScript
中访问这个模块的名字。如果你不指定,默认就会使用这个 Objective-C
类的名字。
NSString *KDMarketGiftControllerHanlder = @"KDMarketGiftControllerHanlder";
...
RCT_EXPORT_METHOD(KDMarketGiftControllerHanlder : (NSDictionary *)object) {
[[NSNotificationCenter defaultCenter]
postNotificationName:KDMarketGiftControllerHanlder object:object];
}
该宏用来给 React Native
的 js 文件暴露公开接口,这样就可以在 js 文件中调用该方法,实现页面跳转的功能。上面的 KDMarketGiftControllerHanlder
方法通过消息中心发送了一个名称为 "KDMarketGiftControllerHanlder" 的消息,并且传递了一个字典类型的参数,该参数是 js 封装并传递进来的,下文中还会提到,这里暂时不表。
这个名叫 "KDMarketGiftControllerHanlder" 的消息在 KDMarketGiftController
中注册并处理,主要原理是 KDMarketGiftController
接收到该消息后,解析 object
参数,根据定义好的跳转协议进行原生页面的跳转,从而实现 js 代码控制原生页面跳转。
// 解析object参数,定义不同的跳转情况,注意需要注册消息通知
- (void)KDMarketGiftControllerHanlder:(NSNotification *)notification {
NSDictionary *router = notification.object;
NSString *path = router[@"path"];
if ([path isEqualToString:@"openMarketingGiftWebViewFromJS"]) {
KDWebViewController *web = [KDWebViewController webViewWithUrl:[KDURLHelper marketingGiftInfoURL]
usingUIWebView:NO
configWebProgress:NO];
[self.navigationController pushViewController:web animated:YES];
} else if ([path isEqualToString:@"addWord"]) {
[self presentDetailController:nil];
} else if ([path isEqualToString:@"editWord"]) {
NSDictionary *json = router[@"model"];
NSString *type = router[@"type"];
NSError *error = nil;
KDMarketWordModel *giftMarkertModel =
[[KDMarketWordModel alloc] initWithDictionary:json error:&error];
[self presentDetailController:giftMarkertModel];
}
}
以上是原生 Objective-C
需要处理的逻辑,那么 React Native
中 js 文件如何使用原生 Objective-C
暴露的接口方法呢?其实也很简单,在需要控制原生页面跳转的 js 文件中,引入 ZanIntentModule
,然后调用 KDMarketGiftControllerHanlder
方法即可。
import {
AppRegistry,
StyleSheet,
NativeModules,
} from 'react-native';
var ZanIntentModule = NativeModules.ZanIntentModule;
...
// 添加寄语
_addWord() {
var type = this.state.FooterText;
ZanIntentModule.KDMarketGiftControllerHanlder({
"path": "addWord",
"type": type,
"model": giftDataSource,
});
}
KDMarketGiftControllerHanlder
方法中传递的 object
对象就对应了上文Objective-C
中的字典参数,和 Android
平台类似,iOS
原生与 js 通信也可以通过回调获得原生的返回值或者异步动作,而所不同的是在 iOS
平台下,原生处理回调是RCTResponseSenderBlock
实现的,具体使用方法如下:
// Objective-C代码
// js获取原生的token参数,RCT_EXPORT_METHOD不支持return返回值,可以通过回调将token传递给js
RCT_EXPORT_METHOD(getAccessToken : (RCTResponseSenderBlock)callback) {
NSString *token = [KDKeyChainController sharedInstance].token;
callback(@[token]);
}
// js代码,getAccessToken通过回调将token参数传递给js,这是一个异步的过程
async createTokenRequest(url){
return new Promise((resolve, reject) => {
intentMoudle.getAccessToken(
(token) => {
resolve(this.URL_CARMEN + url + "?access_token=" + token);
})
});
},
最后还需要说明 Objective-C
和 js 代码之间的数据类型对应关系
前面是js中的数据类型,圆括号是Objective-C的数据类型
string (NSString)
number (NSInteger, float, double, CGFloat, NSNumber)
boolean (BOOL, NSNumber)
array (NSArray)
object (NSDictionary)
function (RCTResponseSenderBlock)
2.2.2 iOS
原生主动发消息
类似的,iOS 也是通过事件总线完成原生主动发消息的动作,比如原生执行完某个动作需要通知 js 刷新数据,那么就可以通过以下方式实现。
// Objective-C代码 发送刷新数据的事件,参数为nil
- (void)reloadData {
[self.bridge.eventDispatcher sendAppEventWithName:kReloadData body:nil];
}
// js代码,在React Native的生命周期componentWillMount方法中执行刷新数据的动作
componentWillMount() {
this.eventEmitter = NativeAppEventEmitter.addListener(
'ReloadData',
() => this._reloadData()
);
}
3. 热部署
3.1 通用配置
经过调研和选型,最终选择了微软出品的 CodePush
作为 React Native
热部署方案。
CodePush
是提供给 React Native
开发者直接部署移动应用更新给用户设备的云服务。CodePush
作为一个中央仓库,开发者可以推送更新 (JS, HTML, CSS and images),应用可以从客户端 SDK
里面查询更新。CodePush
可以让应用有更多的可确定性,也可以让你直接接触用户群。在修复一些小问题和添加新特性的时候,不需要经过二进制打包,可以直接推送代码进行实时更新。
CodePush
可以进行实时的推送代码更新:
- 直接对用户部署代码更新
- 管理
Alpha
,Beta
和生产环境应用 - 支持
JavaScript
文件与图片资源的更新 - 暂不支持增量更新
CodePush
开源了 react-native
版本,react-native-code-push托管在 GitHub 上。
具体的教程和用法微软都在 Github上
做了详细说明,接下来简单地梳理一下从配置、编码、部署等具体流程。
(1) 安装 CodePush CLI
管理 CodePush
账号需要通过 NodeJS-based CLI
。
只需要在终端输入 npm install -g code-push-cli
,就可以安装了。
安装完毕后,输入 code-push -v
查看版本,如看到版本代表成功。
(2) 创建一个 CodePush
账号
在终端输入 code-push register
,会打开如下注册页面让你选择授权账号。
授权通过之后,CodePush
会告诉你“access key”,复制此 key 到终端即可完成注册。
然后终端输入 code-push login
进行登陆,登陆成功后,你的 session 文件将会写在 /Users/ 你的用户名 /.code-push.config
。
(3) 在 CodePush 服务器注册 app
为了让 CodePush
服务器知道你的 app,我们需要向它注册 app: 在终端输入 code-push app add <appName>
即可完成注册。
例如:
code-push app add shangjiaban-android
如果是 iOS 平台,命令为
code-push app add shangjiaban-ios
,Android
和iOS
必须要区分
还有很多 code-push app
相关的命令,参考如下:
$: code-push app help
Usage: code-push app <command>
命令:
add Add a new app to your account
remove Remove an app from your account
rm Remove an app from your account
rename Rename an existing app
list Lists the apps associated with your account
ls Lists the apps associated with your account
transfer Transfer the ownership of an app to another account
3.2 Android 配置
(1) 集成 CodePush SDK
第一步:在项目中安装 react-native
插件,终端进入你的项目根目录然后运行。
npm install --save react-native-code-push
CodePush
提供了两种方式:RNPM
和 Manual
,接下来重点讲述 Manual
这种方式。
在 android/settings.gradle
中添加如下配置
include ':app', ':react-native-code-push'
project(':react-native-code-push').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-code-push/android/app')
在 android/app/build.gradle
文件中添加如下的配置:
...
apply from: "../node_modules/react-native-code-push/android/codepush.gradle"
...
dependencies {
...
compile project(':react-native-code-push')
}
(2) 配置 Application
文件
import com.microsoft.codepush.react.CodePush;
public class MainApplication extends Application implements ReactApplication {
private final ReactNativeHost mReactNativeHost = new ReactNativeHost(this) {
...
// 1. 覆写getJSBundleFile 方法
@Override
protected String getJSBundleFile() {
return CodePush.getJSBundleFile();
}
@Override
protected List<ReactPackage> getPackages() {
// 2. 添加CodePush 相关的Package,初始化参数需要包含deploymentKey,
// 可以通过运行“code-push deployment ls <appName> -k”获得
return Arrays.<ReactPackage>asList(
new MainReactPackage(),
new CodePush("deployment-key", MainApplication.this, BuildConfig.DEBUG)
);
}
};
}
其中 deployment-key
可以通过如下命令获得。
3.3 iOS 配置
iOS 平台上关于 CodePush
的配置和 Android
平台是类似的,可以参考上文的 (1)(2)(3),iOS 平台集成 CodePush
比较简单,官网提供了 3 种集成方式,这里重点介绍如何通过 cocoapods
来集成。
(1) 引入 CodePush
首先在 Podfile 文件中添加 CodePush
,配置如下:
pod 'CodePush', :path => './node_modules/react-native-code-push'
然后执行 pod install
就可以了。
(2) 声明 bundle
文件来源
引入 CodePush
后还需要在代码中声明 bundle
的加载来源,之前是加载本地的bundle
文件,现在需要调用 CodePush
提供的方法指定加载 Bundle
文件,代码如下:
#import "CodePush.h"
...
// 原来的bundle加载方法
jsCodeLocation = [[NSBundle mainBundle] URLForResource:@"main" withExtension:@"jsbundle"];
// CodePush的bundle加载方法,这里的bundleURL默认加载的是main.jsbundle,
// 如果你的名称不一样,需要调用CodePush提供的其他方法来自定义。
jsCodeLocation = [CodePush bundleURL];
最后还需要在 Info.plist
中添加一个 key
为 CodePushDeploymentKey
, 其value
就是 CodePush
提供的唯一 token
值。具体获取方法,可以通过如下命令获得
code-push deployment ls <appName> -k
3.4 React Native 配置
为了达到更好的体验效果,我们决定采用静默升级的策略,让用户无感知地体验热更新,也可以是具体的升级流程图如下:
如果要达成上述热部署效果,那么还需要在 JavaScript
文件中完成更新时机和更新策略的设置。
(1) 在 js 中导入 CodePush
模块:
import codePush from 'react-native-code-push'
(2) 在 componentDidMount
中调用 sync
方法,后台请求更新
codePush.sync()
如果是非强制并允许更新, CodePush
会在后台静默地将更新下载到本地,等待 APP 再一次启动或者加载 React Native
页面的时候更新应用。
如果更新是强制性的,更新文件下载好之后会立即进行更新。关于如何配置是否强制更新,会在下文发布更新处重点说明。
如果你期望更及时的获得更新,可以在每次 APP 从后台进入前台的时候去主动的检查更新:
AppState.addEventListener("change", (newState) => {
newState === "active" && codePush.sync({
installMode:codePush.InstallMode.ON_NEXT_RESUME,
deploymentKey: DEPLOYMENT_KEY,
});
});
上述流程图提及的三种更新方式,就是通过 installMode
参数控制的,取值方式分别为:
codePush.InstallMode.ON_NEXT_RESTART
即下一次启动的时候安装更新codePush.InstallMode.ON_NEXT_RESUME
即下一次切后台切换的时候安装更新codePush.InstallMode. IMMEDIATE
立即下载安装更新
如果发布更新时
mandatory
参数为 true,即强制更新,则上述设置都会无效,只有mandatory
参数为 fasle 时,设置才会有效。
3.5 打包并发布
(1) 打包 js
发布更新之前,需要先把 js 打包成 bundle
,以下是 Android 的做法:
第一步: 在 Android
工程目录里面新增 release
文件: mkdir release
,对于 iOS 来说,目前 bundle
文件直接放在工程根目录下,所以无需这一步。
第二步: 运行命令打包
react-native bundle --platform 平台 --entry-file 启动文件 --bundle-output 打包js输出文件 --assets-dest 资源输出目录 --dev 是否调试。
例如:
Android
react-native bundle --platform android --entry-file index.android.js --bundle-output ./release/index.android.bundle --assets-dest ./release --dev false
iOS
react-native bundle --platform ios --entry-file index.ios.js --bundle-output ./Koudaitong/main.jsbundle --assets-dest ./Koudaitong --dev false
(2) 发布更新
打包 bundle
结束后,就可以通过 CodePush
发布更新了。在终端输入
code-push release <应用名称> <Bundles所在目录> <对应的应用版本> --deploymentName: 更新环境
--description: 更新描述 --mandatory: 是否强制更新
例如:
Android
code-push release shangjiaban-android ./release 3.12.1 --description "update React Native" --mandatory true
iOS
code-push release shangjiaban-ios ./Koudaitong/main.jsbundle 3.12.0 --description "update React Native" --mandatory false
注意:
-
CodePush
默认是更新Staging
环境的,如果是Staging
,则不需要填写deploymentName
。 -
如果有
mandatory
则Code Push
会根据mandatory
是true
或false
来控制应用是否强制更新。默认情况下mandatory
为false
即不强制更新。 -
对应的应用版本
targetBinaryVersion
是指当前 app 的版本 ( 对应build.gradle
中设置的 versionName “3.12.1”),也就是说此次更新的js/images
对应的是 app 的那个版本。不要将其理解为这次 js 更新的版本。 如客户端版本是 3.12.1,那么我们对 3.12.1 的客户端更新js/images
,targetBinaryVersion
填的就是 3.12.1。 -
对于对某个应用版本进行多次更新的情况,
CodePush
会检查每次上传的bundle
,如果在该版本下如 3.12.1 已经存在与这次上传完全一样的bundle
( 对应一个版本有两个bundle
的md5
完全一样 ),那么CodePush
会拒绝此次更新。