前言
2014年8月,faceBook內部傳出用一種新技術開發App的新方案,次年3月,該技術開源并正式發布,它的名字如雷貫耳——“React Native”,我們簡稱為“RN”。在實現媲美 NativeApp
用戶體驗的同時,RN
允許Web開發者更多地基于現有經驗組件化開發App,并且具備跨平臺特性,開發效率和成本都相當可觀。不過它也有著一般跨平臺App共有的短板,就是它的兼容性,尤其是針對 國內層出不窮的 Android
機型,逐個兼容似乎不太可能。不過好在這類系統提供商不停地升級系統,facebook 也孜孜不倦地提升RN的性能和兼容性,再配合它的強大社區和React
生態圈,目前使用RN構建App這個技術已經日趨成熟。國內很多知名互聯網企業也開始應用這門技術,比如 京東App,攜程App,美團App里也混合了RN頁面。
目前主流的應用大體分為三類:NativeApp、WebApp和HybridApp,先羅列一下三者的特點:
NativeApp特點
- 性能好
- 完美的用戶體驗
- 開發成本高,無法跨平臺
- 升級困難(審核), 維護成本高
WebApp特點
- 開發成本低,更新快,版本升級容易,自動升級
- 跨平臺,”Write Once , Run Anywhere”
- 無法調用系統級的API
- 臨時入口,用戶留存度低
- 性能差,體驗差,設計受限制
- 相比Native App,Web App體驗中受限于以上5個因素:網絡環境,渲染性能,平臺特性,受限于瀏覽器,系統限制。
HybridApp特點
NativeApp 和 WebApp 折中的方案,保留了 NativeApp 和 WebApp 的優點。
-
但是還是性能差。頁面渲染效率低,在Webview中繪制界面,實現動畫,資源消耗都比較大, 受限于技術, 網速等因素
關系圖
為了解決上述問題,一套高效率,高性能的跨平臺方案成為了大家熱衷的話題,也就有了下面要比較的 ReactNative 或 Weex 這類解決方案。
ReactNative的特點
優勢相對HybirdApp
或者WebApp
:
- 不用
Webview
,徹底擺脫了Webview
讓人不爽的交互和性能問題 - 有較強的擴展性,這是因為Native端提供的是基本控件,JS可以自由組合使用
- 可以直接調用Native原生的模塊
優勢相對于NativeApp
:
- 可以通過更新遠端JS,直接更新app(熱更新)
- 跨平臺特性
- 學習成本低,組件式開發,代碼復用性高。
劣勢
[!RN的劣勢]
- 擴展性仍然遠遠不如web,也遠遠不如直接寫 Native code
- 從Native到Web,要做很多概念轉換,勢必造成雙方都要妥協。比如web要用一套CSS的閹割版,Native通過css-layout拿到最終樣式再轉換成native原生的表達方式(比如iOS的Constraint\origin\Center等屬性),再比如動畫。另外,若Android和iOS都要做相同的封裝,概念轉換就更復雜了。
- 內存占用較大。
RN與Weex的選擇
首先我覺得RN和Weex都是很棒的跨平臺解決方案,兩者都有各自的優秀之處,知乎和簡書上關于兩者比較的文章車載斗量,不過推薦RN的居多吧。我本人搞RN開發也1年多了,期間踩過的坑不少,不過基本能通過Google最后解決。這里順帶一提,RN的社區非常強大,不過很大一部分活躍在國外,所以很多優秀的解決方案和三方模塊來自國外,需要一定的外語閱讀能力和Google技巧,算是門檻上比Weex要高一些。我這里不帶太多的情緒,因為沒有實際在項目中運用過 Weex,不過多評價這門框架。我只是從其他角度去考慮:
- 技術棧。我們目前的前端技術棧用的
React
,那么切換到ReactNative
的成本應該比vue
技術棧的 Weex 低一些。 - 現有積累。我對RN有一定的項目經驗,支持現階段的產品需求難度不大,而且目前國內介紹和總結RN的書籍很多,門檻已經沒有1年前那么高了。
- 長遠考慮。
RN
的版本更新速度快(性能和兼容性方面的改進工作迅速),社區活躍,靈活度高,國內成功案例很多,技術成熟度上應該比 Weex 好一些。
潛在隱患
Android稍微好一些,AppStore對于RN和其他跨平臺App開發始終抱有一絲敵意,從長遠考慮,HybridApp
的方案應該保留,可以做為未知情況的降級方案。
RN實現原理簡析
普通的JS-OC通信實際上很簡單,OC向JS傳信息有現成的接口,像Webview
提供的stringByEvaluatingJavaScriptFromString
方法可以直接在當前context
上執行一段JS腳本,并且可以獲取執行后的返回值,這個返回值就相當于JS向OC傳遞信息。ReactNative
也是以此為基礎,通過各種手段,實現了在OC定義一個模塊方法,JS可以直接調用這個模塊方法并還可以無縫銜接回調。簡單地說就是:“模塊化,模塊配置表,傳遞ID,封裝調用,事件響應”
IOS端實現原理
Android端實現原理
RN常用的構建方案
RN目前有兩種構建方案:
(1)RN為主,Native為輔。整個App都由RN構建,Native把功能模塊按照RN的規則進行封裝,然后交給RN進行調用。適合交互場景不太復雜的App,如金融、物流管理類App,基于Labs的微社交類App。
(2) Native為主,RN為輔。整個App構建工作由Native完成,然后把某些功能模塊的入口換成RN,然后控制權交給RN,RN又可以切換回Native。簡單地說也就是替代Hybrid之前的位置。適合做一些功能不需要很酷炫,版本迭代頻繁的業務場景(如專場、活動頁、產品詳情等)。
*RN混合開發技術
也是上面說的第二類構建方案,核心是實現RN與Native之間的通信和互相調用。
IOS原生界面跳轉RN界面
現階段混合開發中,一般就是在原有原生項目基礎上面添加RN開發的頁面。那么這邊我們講解一下從原生界面跳轉到RN頁面的方法。其實是非常簡單的,就是普通push
一個 ViewController
即可,在新打開的 ViewController
中加入 RCTRootView
視圖,具體承載RN頁面的控制器的代碼如下:
#import "TwoViewController.h"
#import "RCTRootView.h"
#import "ThreeViewController.h"
@implementation TwoViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.title=@"RN界面";
NSURL *jsCodeLocation = [NSURL URLWithString:@"http://localhost:8081/index.ios.bundle?platform=ios&dev=true"];
RCTRootView *rootView = [[RCTRootView alloc] initWithBundleURL:jsCodeLocation
moduleName:@"mixedDemo"
initialProperties:nil
launchOptions:nil];
self.view=rootView;
}
@end
RN訪問調用IOS原生方法
要實現這個功能,我們首先需用創建一個實現"RCTBridgeModule"
協議的RNBridgeModule
橋接類,看一下RNBridgeModule.h
文件:
#import <Foundation/Foundation.h>
#import "RCTBridgeModule.h"
@interface RNBridgeModule : NSObject<RCTBridgeModule>
@end
接著我們需要在 RNBridgetModule
的實現類中,實現 RCT_EXPORT_MODULE()
宏定義,括號參數不填為默認橋接類的名稱,也可以自定義填寫。該名稱用來指定在 JavaScript
中訪問這個模塊的名稱。
使用Callback
進行回調
接下來我們在 RNBridgeModule.m
文件里添加如下的方法:
//RN傳參數調用原生OC,并且返回數據給RN 通過CallBack
RCT_EXPORT_METHOD(RNInvokeOCCallBack:(NSDictionary *)dictionary callback:(RCTResponseSenderBlock)callback){
NSLog(@"接收到RN傳過來的數據為:%@",dictionary);
NSArray *events = [[NSArray alloc] initWithObjects:@"張三",@"李四", nil];
callback(@[[NSNull null], events]);
}
如果原生的方法要被 JavaScript
進行訪問,那么該方法需要使用 RCT_EXPORT_METHOD()
宏定義進行聲明。該聲明的 RNInvokeOCCallBack
方法有兩個參數:第一個參數代表從 JavaScript
傳過來的數據,第二個參數是回調方法,通過該回調方法把原生信息發送到 JavaScript
中。其中上面的 callback
方法中傳入一個參數數組,其實該數組的第一個參數為一個 NSError
對象,如果沒有錯誤返回 null
,其余的數據作為該方法的返回值回調給 JavaScritpt
。
最后我們需要在 JavaScript
文件中進行定義導出在原生封裝的模塊,然后調用封裝方法訪問即可:
var { NativeModules } = require('react-native')
var RNBridgeModule = NativeModules.RNBridgeModule
RNBridgeModule.RNInvokeOCCallBack(
{ 'name': 'jiangqq', 'description': 'http://www.lcode.org' },
(error, events) => {
if(error) {
console.error(error)
} else {
this.setState({ events: events })
}
}
)
使用Promise
進行回調
我們在 RNBridgetModule
的實現類添加如下的方法代碼:
//RN傳參數調用原生OC, 并且返回數據給RN通過Promise
RCT_EXPORT_METHOD(RNInvokeOCPromise:(NSDictionary *)dictionary resolver:(RCTPromiseResolveBlock)resolve
rejecter:(RCTPromiseRejectBlock)reject) {
NSLog(@"接收到RN傳過來的數據為:%@",dictionary);
NSString *value = [dictionary objectForKey:@"name"];
if([value isEqualToString:@"jiangqq"]) {
resolve(@"回調成功啦,Promise...");
} else {
NSError *error = [NSError errorWithDomain:@"傳入的name不符合要求,回調失敗啦,Promise..." code:100 userInfo:nil];
reject(@"100",@"傳入的name不符合要求,回調失敗啦,Promise...",error);
}
}
這邊定義了 RNInvokeOCPromise
方法,共有三個參數:
-
dictionary
: JavaScript傳入的數據 -
resolve
: 成功,回調數據 -
reject
: 失敗,回調數據
其中resove方法傳入具體的成功信息即可,但是reject方法必須傳入三個參數分別為,錯誤代碼code ,錯誤信息message以及NSError對象。最終看一下JavaScript中的調用方式:
var { NativeModules } = require('react-native')
var RNBridgeModule = NativeModules.RNBridgeModule
//獲取Promise對象處理
async _updateEvents() {
try {
var events = await RNBridgeModule.RNInvokeOCPromise({ 'name': 'jiangqqlmj' })
this.setState({ events })
} catch(e) {
this.setState({ events: e.message })
}
}
IOS原生訪問調用RN
如果我們需要從iOS原生方法發送數據到JavaScript
中,那么可以使用eventDispatcher
。
首先我們需要在 RCTBridgeModule
的實現中中引入:
#import "RCTBridge.h"
#import "RCTEventDispatcher.h"
@synthesize bridge = _bridge;
接下來就能通過OC原生代碼來訪問JavaScript
了
self.bridge.eventDispatcher sendAppEventWithName:@"EventReminder" body:@{@"name":[NSString stringWithFormat:@"%@",value],@"errorCode":@"0",@"msg":@"成功"}];
這里補充說明一下 sendAppEventWithName
方法,它包含2個參數:
-
EventReminder
:自定義的一個事件名稱 - 具體摻入
JavaScript
的數據信息
OC調用RN的具體代碼
// OC調用RN
RCT_EXPORT_METHOD(VCOpenRN:(NSDictionary *)dictionary) {
NSString *value = [dictionary objectForKey:@"name"];
if([value isEqualToString:@"jiangqq"]) {
[self.bridge.eventDispatcher sendAppEventWithName:@"EventReminder" body:@{@"name":[NSString stringWithFormat:@"%@",value],@"errorCode":@"0",@"msg":@"成功"}];
} else {
[self.bridge.eventDispatcher sendAppEventWithName:@"EventReminder" body:@{@"name":[NSString stringWithFormat:@"%@",value],@"errorCode":@"0",@"msg":@"輸入的name不是jiangqq"}];
}
}
然后在 JavaScript
端進行調用的方法如下:
import { NativeAppEventEmitter } from 'react-native'
// ...省略一部分代碼
componentDidMount() {
console.log('開始訂閱通知...')
subscription = NativeAppEventEmitter.addListener(
'EventReminder',
(reminder) => {
let errorCode = reminder.errorCode
if (errorCode === 0) {
this.setState({ msg: reminder.name })
} else {
this.setState( {msg: reminder.msg })
}
}
)
}
componentWillUnmount() {
subscription.remove()
}
RN界面調用ANDROID原生界面
在Android原生開發中,我們知道打開一個Activity
一般有兩種方法:顯式和隱式。隱式方法一般通過AndroidManifest
配置文件中的Activity
Intent-Filter
中進行相關攔截配置。那么這邊我們主要講解的是顯示啟動Activity
。
下面我們這邊在創建繼承ReactContextBaseJavaModule
的IntentModule
模塊,具體代碼如下:
package com.mixeddemo;
import android.app.Activity;
import android.content.Intent;
import android.text.TextUtils;
import com.facebook.react.bridge.Callback;
import com.facebook.react.bridge.JSApplicationIllegalArgumentException;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod;
public class IntentModule extends ReactContextBaseJavaModule {
public IntentModule(ReactApplicationContext reactContext) {
super(reactContext);
}
@Override
public String getName() {
return "IntentModule";
}
/**
* 從JS頁面跳轉到原生activity 同時也可以從JS傳遞相關數據到原生
* @param name 需要打開的Activity的class
* @param params
*/
@ReactMethod
public void startActivityFromJS(String name, String params){
try {
Activity currentActivity = getCurrentActivity();
if(null!=currentActivity) {
Class toActivity = Class.forName(name);
Intent intent = new Intent(currentActivity,toActivity);
intent.putExtra("params", params);
currentActivity.startActivity(intent);
}
} catch(Exception e) {
throw new JSApplicationIllegalArgumentException(
"不能打開Activity : "+e.getMessage());
}
}
}
我們在這邊加入了一個@ReactMethod
注解的startActivityFromJS
方法,該用于在JS
代碼進行調用,從JS
端傳過來是兩個參數,第一個參數為需要打開的Activity
的class
,第二個參數為傳遞過來的數據。至于為什么用class
,那是因為原生代碼這邊用反射的形式更加方便了,其他方式都有局限性。這邊我們根據傳入的class
,然后直接調用startActivity
直接打開相應的Activity
即可,甚至也可以攜帶一些參數值。具體調用方法方式如下:
const { NativeModules } = require('react-native')
NativeModules.IntentModule.startActivityFromJS("com.hunhedemo.TwoActivity", "我是從JS傳過來的參數信息.456")}
等待原生返回數據
如果我們的RN
界面打開了原生界面,同時獲取到原生的返回數據呢?原生Activity
中回調的數據一般在onActivityResult
中或者其他回調方法中的,但是如果需要返回給RN
的數據是通過封裝的原生模塊方法中的Callback
進行傳輸的。為了解決這個問題,我們這邊創建一個阻塞的隊列來實現,一旦有原生回調數據加入到隊列中,那么數據就會從阻塞隊列中取出來,再通過回調方法傳入到RN
界面中。
下面我們看一下重載了onActivityResult
方法的MainActivity
類中的寫法:
package com.mixeddemo;
import android.content.Intent;
import com.facebook.react.ReactActivity;
import com.facebook.react.ReactPackage;
import com.facebook.react.shell.MainReactPackage;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.ArrayBlockingQueue;
public class MainActivity extends ReactActivity {
//構建一個阻塞的單一數據的隊列
public static ArrayBlockingQueue<String> mQueue = new ArrayBlockingQueue<String>(1);
/**
* Returns the name of the main component registered from JavaScript.
* This is used to schedule rendering of the component.
*/
@Override
protected String getMainComponentName() {
return "hunheDemo";
}
/**
* Returns whether dev mode should be enabled.
* This enables e.g. the dev menu.
*/
@Override
protected boolean getUseDeveloperSupport() {
return BuildConfig.DEBUG;
}
/**
* A list of packages used by the app. If the app uses additional views
* or modules besides the default ones, add more packages here.
*/
@Override
protected List<ReactPackage> getPackages() {
return Arrays.<ReactPackage>asList(
new MainReactPackage(),
new IntentReactPackage()
);
}
/**
* 打開 帶返回的Activity
* @param requestCode
* @param resultCode
* @param data
*/
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (resultCode == RESULT_OK && requestCode == 200) {
String result = data.getStringExtra("three_result");
if (result != null && !result.equals("")) {
mQueue.add(result);
} else {
mQueue.add("無數據啦");
}
} else {
mQueue.add("沒有回調...");
}
}
}
然后在IntentModule
類中添加startActivityFromJSGetResult
方法:
* 從JS頁面跳轉到Activity界面,并且等待從Activity返回的數據給JS
* @param className
* @param successBack
* @param errorBack
*/
@ReactMethod
public void startActivityFromJSGetResult(String className, int requestCode, Callback successBack, Callback errorBack){
try {
Activity currentActivity = getCurrentActivity();
if(currentActivity!=null) {
Class toActivity = Class.forName(className);
Intent intent = new Intent(currentActivity, toActivity);
currentActivity.startActivityForResult(intent, requestCode);
//進行回調數據
successBack.invoke(MainActivity.mQueue.take());
}
} catch (Exception e) {
errorBack.invoke(e.getMessage());
e.printStackTrace();
}
}
請注意上面的方法中,啟動Activity
是通過startActivityForResult()
方法,這樣打開的Activity
有數據返回之后,才會調用之前的onActivityResult()
方法,然后我們在這個方法中把回調的數據添加到阻塞隊列中。
接下來RN
里我們可以通過如下方式進行調用:
import { ToastAndroid } from 'react-native'
const { NativeModules } = require('react-native')
NativeModules.IntentModule.startActivityFromJSGetResult("com.mixedDemo.ThreeActivity", 200,
(msg) => {
ToastAndroid.show('JS界面:從Activity中傳輸過來的數據為:' + msg, ToastAndroid.SHORT)
},
(result) => {
ToastAndroid.show('JS界面:錯誤信息為:' + result, ToastAndroid.SHORT)
}
)
Android原生界面調用RN界面
從上面的介紹,我們發現Android
原生界面打開RN
界面,還是非常簡單的,直接啟動配置了React Native
的界面Activity
即可,但我們如果想在打開RN
界面同時,從原生Activity
中傳點數據過去該怎么實現呢?思路是 在承載RN
界面的Activity
中獲取當前Intent
中的數據,然后通過Callback
方法回調即可。
package com.mixeddemo;
import android.app.Activity;
import android.content.Intent;
import android.text.TextUtils;
import com.facebook.react.bridge.Callback;
import com.facebook.react.bridge.JSApplicationIllegalArgumentException;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod;
public class IntentModule extends ReactContextBaseJavaModule {
public IntentModule(ReactApplicationContext reactContext) {
super(reactContext);
}
@Override
public String getName() {
return "IntentModule";
}
/**
* Activtiy跳轉到JS頁面,傳輸數據
* @param successBack
* @param errorBack
*/
@ReactMethod
public void dataToJS(Callback successBack, Callback errorBack){
try {
Activity currentActivity = getCurrentActivity();
String result = currentActivity.getIntent().getStringExtra("data");
if (TextUtils.isEmpty(result)) {
result = "沒有數據";
}
successBack.invoke(result);
} catch (Exception e) {
errorBack.invoke(e.getMessage());
}
}
}
接著RN
端我們就可以通過在componentDidMount()
時調用原生封裝的方法去獲取傳過來的參數:
componentDidMount() {
//進行從Activity中獲取數據傳輸到JS
NativeModules.IntentModule.dataToJS((msg) => {
console.log(msg)
ToastAndroid.show('JS界面:從Activity中傳輸過來的數據為:' + msg, ToastAndroid.SHORT)
},
(result) => {
ToastAndroid.show('JS界面:錯誤信息為:' + result, ToastAndroid.SHORT)
})
}
如何運用RN
RN作為流行的跨平臺解決方案,不僅適用前端工程師,同樣適合客戶端工程師學習。一個強大的混合開發App需要雙方強強聯手,而RN可以作為中間的Bridge,讓前端和客戶端的銜接更加優雅。
針對前端工程師
主要負責跨平臺部分的頁面構建和業務邏輯實現,RN可以理解為用facebook提供的一套React框架開發移動端應用。所以需要扎實的React
和Redux
功底,對ReactNative
框架提供的組件和API有清晰的認識,并能熟練使用。
針對客戶端工程師
主要負責承載RN的視圖容器和入口,把UI的控制權轉交給RN。除此之外還要掌握Native模塊或SDK封裝成RN組件的技巧,最好能形成一套針對特定業務場景完整的底層組件庫,并配備清晰的說明文檔。
資源推薦
- React Native中文網
- React Native 中文版
- 江清清的技術專欄-ReactNative專題
- js.coach - ReactNative組件庫
- React Native 優秀開源項目大全
- ReactNative開發常用的三方模塊
- React Native通信機制詳解
- 京東618:ReactNative框架在京東無線端的實踐
- React-Native系列Android——Native與Javascript通信原理
常用的開源組件
- 微軟熱更新開源平臺
react-native-code-push
- Google三方統計分析平臺
react-native-google-analytics-bridge
- 極光三方推送平臺
jpush-react-native
- sqlite數據庫組件
react-native-sqlite-storage
- 圖像處理組件
react-native-transformable-image
- 微信SDK組件(授權、分享、支付)
react-native-wechat
- QQSDK組件
react-native-qq-sdk
- 支付寶支付組件
react-native-alipay
- 獲取設備信息組件
react-native-device-info
- 國際化處理組件
react-native-il8n
疑難雜癥
- 小米手機調試需要關閉MUI優化引擎
- Oppo手機、金立手機 小鍵盤在頁面路由跳回后無法再開啟。解決方案是 設置
TextInput
組件autoFocused
為true