最近偷懶了一段時間(折騰了下小程序、跨平臺開發框架Weex / React Native / Flutter),到現在接入RN混合開發也已經七八個月的時間了,今天針對RN在Android混編項目做個整體的覆盤反思。以下內容是之前總結,本次不再重複記錄。
本次記錄不作爲使用指南記錄,因此不再描述項目初始化等相關方面,需要的可以參考上面鏈接,這裏主要站在我們業務選擇和使用RN的角度來整體上分析跨平臺RN開發的優缺點,及對React Native與 Android 混編項目簡單做下覆盤反思。
一、跨平臺RN開發的優缺點:
-
優點:
- 提高研發和測試效率
- 各終端邏輯保持統一
之前Android、iOS、M同一需求需三端開發人員各自去實現,以各自對需求的理解、各自的實現方式去碼不同的代碼,各自的邏輯和展示也可能就各不相同,很難保證功能一致性,自然測試人員需要針對三端各自測試,邏輯bug產生相對比較零散,各端各不相同,驗證也相對比較耗時耗力,採用RN同一功能,同一套代碼邏輯方式,測試起來發現邏輯bug相對統一,一些平臺差異性兼容性bug除外,大大提高測試效率,同時各終端業務邏輯也相對統一,提高了研發效率。
缺點:
因平臺差異性可能會存在一些兼容性問題,如果想在前期就能抹平各端差異性問題及解決各平臺兼容型問題,就需要RN開發人員熟悉Android、IOS、Web三端開發技術,對技術廣度稍微有點高。
二、React Native在Android混編項目中的頁面跳轉和方法調用
大致通過上面這張簡圖來描述下:
-
頁面跳轉(RN與Android原生)
調查背景:在設計與RN交互時,並不是僅站在Android一端的角度去設計,而是考慮通用型,儘量用RN本身特性去抹平差異性,避免RN在代碼層面進行差異化處理,比如說頁面跳轉處理:- Android的頁面跳轉是通過Intent跳轉
- RN是通過路由(M版也通過路由跳轉)
如果保持各自特性,則兩者直接頁面互相跳轉就需要原生藉助JS暴露接口給RN來實現了,這樣的話RN就需要根據終端環境進行差異化處理,爲了避免RN在代碼層面進行差異化處理,儘量尋找統一性方案確保整個項目的統一,通過調查發現RN提供的Linking方式進行跳轉,那麼就有兩個問題,RN怎麼拿?原生怎麼傳?然後通過源碼發現RN分別針對Android和IOS進行了封裝映射,我們只需要把數據傳送到對應的位置即可,
const LinkingManager = Platform.OS === 'android' ? NativeModules.IntentAndroid : NativeModules.LinkingManager;
在Android中對應的是IntentAndroid,查看對應的源碼:
/** * Return the URL the activity was started with * * @param promise a promise which is resolved with the initial URL */ @ReactMethod public void getInitialURL(Promise promise) { try { Activity currentActivity = getCurrentActivity(); String initialURL = null; if (currentActivity != null) { Intent intent = currentActivity.getIntent(); String action = intent.getAction(); Uri uri = intent.getData(); if (Intent.ACTION_VIEW.equals(action) && uri != null) { initialURL = uri.toString(); } } promise.resolve(initialURL); } catch (Exception e) { promise.reject(new JSApplicationIllegalArgumentException( "Could not get the initial URL : " + e.getMessage())); } }
通過上面源碼也就瞭解了Android與RN如何根據Linking進行頁面跳轉,我先按照這種形式在Android上進行測試發現可行,當然也有bug,就是拿的時候可能爲空,調查發現RN與原生頁面裝載之前就調用這個方法導致拿不到上下文,只要創建rootview就會執行rn的生命週期,而此時rN與原生還沒有綁定,後來發現RN是能監聽到原生頁面的活動狀態,此時再去獲取路由即可。詳細內容可參考React Native Linking與 Android原生頁面路由跳轉實現。
-
方法調用
RN通信原理簡單地講就是,一方native(java)將其部分方法註冊成一個映射表,另一方(js)再在這個映射表中查找並調用相應的方法,而Bridge擔當兩者間橋接的角色。
其實方法調用大致分爲2種情況:- Android主動向JS端傳遞事件、數據
- JS端主動向Android詢問獲取事件、數據
RN調用Android需要module名和方法名相同,而Android調用RN只需要方法名相同。
(1)RCTDeviceEventEmitter 事件方式
優點:可任意時刻傳遞,Native主導控制。
(2)Callback 回調方式
優點:JS調用,Native返回。
缺點:CallBack爲異步操作,返回時機不確定
(3)Promise
優點:JS調用,Native返回。
缺點:每次使用需要JS調用一次
(4)直傳常量數據(原生向RN)
跨域傳值,只能從原生端向RN端傳遞。RN端可通過 NativeModules.[module名].[參數名] 的方式獲取。注意:RN層調用Native層進行界面跳轉時,需要設置FLAG_ACTIVITY_NEW_TASK標誌。
例如:下面是RCTDeviceEventEmitter事件的簡單事例,稍後封裝下更方便與原生的通信交互。
public class EventEmitter {
private static final String TAG = "EventEmitter";
// 在ReactPackage中的createNativeModules()初始化,RNEventEmitter.setReactContext(reactContext);
private static ReactApplicationContext mReactContext;
public static void setReactContext(ReactApplicationContext mReactContext) {
RNEventEmitter.mReactContext = mReactContext;
}
/**
* 顯示RN中loading
* @param data show:顯示,false:隱藏
*/
public static void showLoading(boolean show) {
nativeCallRn("showloading", show);
}
public static void nativeCallRn(String eventName, Object msg) {
if (mReactContext == null) {
Log.e(TAG, "ReactContext is null");
return;
}
mReactContext.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)
.emit(eventName,msg);
}
}
RN中接收原生消息:
/**
* 接收原生調用
*/
componentDidMount() {
DeviceEventEmitter.addListener('showLoading',(msg)=>{
ToastAndroid.show("發送成功"+msg, ToastAndroid.SHORT);
})
//通過DeviceEventEmitter註冊監聽,類似於Android中的監聽事件。第一個參數標識名稱,要與Module中emit的Event Name相同。第二個參數即爲處理回調時間。
}
三、Andorid 與 RN 傳參數據類型映射關係:
Android | React Native | 備註 |
---|---|---|
Boolean | Bool | |
Integer | Number | |
Double | Number | |
Float | Number | |
String | String | |
Callback | function | |
ReadableMap / WritableMap | Object | RN中的對象 |
ReadableArray / WritableArray | Array |
觀察着8種參數類型發現,其中有ReadableMap 和 ReadableArray類型,對應JavaScript的Object和Array。而在Java原生中,可以發現facebook定義了ReadableArray和ReadableMap接口,一層一層找一下,找到了WritableArray和WritableMap接口,以及實現了他們的WritableNativeArray和WritableNativeMap,我嘗試利用WritableNativeArray push了幾個參數,成功的傳遞到了參數:
//Android
@ReactMethod
public void show(Callback callback) {
WritableArray writableArray = new WritableNativeArray();
writableArray.pushString("one");
writableArray.pushString("two");
writableArray.pushString("three");
callback.invoke(writableArray);
}
//React Native
MyTest.show((result)=> {
ToastAndroid.show("結果是:" + result[2], ToastAndroid.SHORT);
}
);
//Android
@ReactMethod
public void show(Callback callback) {
WritableMap writableMap = new WritableNativeMap();
writableMap.putString("1", "first");
writableMap.putString("2", "second");
writableMap.putString("3", "third");
callback.invoke(writableMap);
}
//React Native
MyTest.show((result)=> {
ToastAndroid.show("結果是:" + result["2"], ToastAndroid.SHORT);
}
);
備註: 如何更好地將RN與原生進行參數傳遞呢?雖說上面的映射關係可以讓我們去準確的傳遞參數,但是比如說在Android原生中ReadableMap / WritableMap
對應着RN中的 Object
, 但是我們原生裏面並不會全局使用ReadableMap / WritableMap
來替換現有的Map或者HashMap吧,爲儘量避免RN的侵入型,我們需要Native Module層進行抽象封裝處理,將RN中的數據類型與Android原生的數據類型進行互轉,稍後會整理相關轉化工具類,方便Android與原生快速交互。