React Native 做爲一個混合開發解決方案,由於業務、性能上的種種緣由,老是避免不了與原生進行交互。在開發過程當中咱們將 RN 與原生交互的幾種方式進行了梳理,按照途徑主要分爲如下幾類:javascript
經過原生 Module 進行交互是最高頻的使用方式。封裝原生 Module 能夠將定義好的原生方法交給 RN 在 JS 端進行調用,JS 端能夠在調用方法時經過傳參的方式直接將數據傳輸給原生端,而原生端能夠在方法執行事後將須要返回的數據經過 Promise 或者 Callback 將數據返回給 JS 端。java
封裝原生 Module 的步驟包括如下幾步:react
- 建立自定義 Module
- 建立自定義 Package 註冊自定義 Module
- 註冊自定義 Package
- 在RN中使用自定義 Module
建立自定義 Module 其實就是將 RN 但願調用的原生功能封裝成中間件的形式,這個中間件須要繼承 ReactContextBaseJavaModule 類,並重寫它的 getName() 方法。getName() 方法返回了 JS 能夠訪問的自定義 Module 名稱,使得咱們在 JS 端能夠經過 NativeModules.自定義 Module 名稱 的形式訪問這個中間件。小程序
public class MyModule extends ReactContextBaseJavaModule {
public MyModule(@Nonnull ReactApplicationContext reactContext) {
super(reactContext);
}
@Nonnull
@Override
public String getName() {
return "MyNativeModule"; // 暴露給RN的模塊名,在JS端經過 NativeModules.MyNativeModule 便可訪問到本模塊
}
}
複製代碼
自定義 Package 實現了 ReactPackage 接口,該接口提供了2個方法來分別註冊自定義 Module 和自定義 ViewManager,其中自定義 ViewManager 用於封裝原生 View 與 RN 進行交互,具體內容能夠查看後面的「經過原生 View 進行交互」。咱們須要在 createNativeModules 方法中返回一個包含咱們新建的自定義 Module 實例的 List,完成註冊。react-native
public class MyReactPackage implements ReactPackage {
@Nonnull
@Override
public List<NativeModule> createNativeModules(@Nonnull ReactApplicationContext reactContext) {
List<NativeModule> modules = new ArrayList<>();
modules.add(new MyModule(reactContext)); // 將新建的 MyModule 實例加入到 List 中完成註冊
return modules;
}
@Nonnull
@Override
public List<ViewManager> createViewManagers(@Nonnull ReactApplicationContext reactContext) {
return Collections.emptyList();
}
}
複製代碼
僅僅完成了自定義 Module 在自定義 Package 的註冊還不能讓 RN 使用咱們的 Module,咱們須要將剛剛新建的 ReactPackage 實例註冊到 ReactApplication 的 ReactNativeHost 實例中。在默認的 RN 工程下,ReactApplication 一般爲 MainApplication,你也可讓本身原有安卓項目中的 Application 類實現 ReactApplication 接口。promise
public class MainApplication extends Application implements ReactApplication {
private final ReactNativeHost mReactNativeHost = new ReactNativeHost(this) {
@Override
public boolean getUseDeveloperSupport() {
return BuildConfig.DEBUG;
}
@Override
protected List<ReactPackage> getPackages() {
return Arrays.<ReactPackage>asList(
new MainReactPackage(),
new MyReactPackage() // 將新建的 MyReactPackage 實例註冊到 ReactPackage 列表中
);
}
@Override
protected String getJSMainModuleName() {
return "index";
}
};
@Override
public ReactNativeHost getReactNativeHost() {
return mReactNativeHost;
}
@Override
public void onCreate() {
super.onCreate();
SoLoader.init(this, /* native exopackage */ false);
}
}
複製代碼
完成原生端的註冊後,RN 便可使用咱們封裝的自定義 Module 了。在 JS 端引用的代碼以下:微信
import { NativeModules } from "react-native";
let customModule = NativeModules.MyNativeModule; // 此處引用的自定義 Module 名必須與自定義 Module 中 getName() 方法返回的字符串一致
複製代碼
接下來咱們能夠經過這個自定義 Module 實現 RN 與原生的幾種通訊方式。ide
在自定義 Module 中,咱們能夠經過重寫 getConstants() 方法,返回一個 Map。這個 Map 的 key 爲 RN 中能夠被訪問的常量名稱,value 爲預設的常量值。函數
private static final String CUSTOM_CONST_KEY = "TEXT";
@Nullable
@Override
// 獲取模塊預約義的常量值
public Map<String, Object> getConstants() {
final Map<String, Object> constants = new HashMap<>();
constants.put(CUSTOM_CONST_KEY, "這是模塊預設常量值");
return constants;
}
複製代碼
在 RN 中使用 Text 組件調用這個常量顯示內容:工具
<Text>{ customModule.TEXT }</Text>
複製代碼
在原生端,若是想將方法暴露給 RN 調用,能夠在方法前加上 @ReactMethod 註解,但要注意,這個方法的訪問權限和返回類型必需要設置爲 public void 才能夠。
@ReactMethod
public void myFunction(String parmas) {
// To Do Something
// 字符串 params 即爲 RN 傳入的參數
}
複製代碼
在 JS 端調用這個函數,並傳遞參數:
customModule.myFunction("這裏是參數 params 的內容");
複製代碼
注:傳入的參數並不侷限於例子中的 String 類型,且參數數量能夠是多個。而 JS 與 Java 的類型對應以下:
JavaScript | Java |
---|---|
Bool | Boolean |
Number | Integer |
Number | Double |
Number | Float |
String | String |
Function | Callback |
Object | ReadableMap |
Array | ReadableArray |
經過上面的類型對應,咱們瞭解到, JS 中的 function 做爲參數傳輸到 Java 中就是個 Callback。因此咱們可使用回調函數,在完成原生方法的執行事後,將須要的結果或狀態經過 Callback.invoke() 方法回調給 JS。
@ReactMethod
public void myFunction(String params, Callback success, Callback failture) {
try {
if (params != null && !params.equals("")){
// 回調成功,返回結果信息
success.invoke("這是從原生", "返回的字符串");
}
}catch (IllegalViewOperationException e) {
// 回調失敗,返回錯誤信息
failture.invoke(e.getMessage());
}
}
複製代碼
在 JS 中須要定義回調函數的執行內容,這裏定義了一個匿名函數做爲回調函數。你能夠根據本身的業務需求替換爲相應的回調函數。
customModule.myFunction(
"這是帶Callback回調的函數方法",
(parma1, parma2) => {
var result = parma1 + parma2;
console.log(result); // 顯示: 這是從原生返回的字符串
},
errMsg => {
console.log(errMsg);
}
);
複製代碼
除了使用 Callback 進行回調,咱們還能夠在 ReactMethod 中將 Promise 做爲最後一個參數,使用 Promise 實例,完成數據的回傳。Promise 具備 resolve() 和 reject() 兩個方法,能夠用於處理正常和異常的回傳。在使用時,咱們一般在自定義 Module 中定義一個 Promise 類型的私有變量,在調用 ReactMethod 時,對這個私有變量進行賦值,而後在須要回傳數據的地方使用這個私有變量進行回傳,這樣能夠更靈活的控制回傳時機。但要注意的是,Promise 實例只能被 resolve() 或 reject() 一次,若屢次回調將會報錯。
private Promise mPromise;
private Handler handler = new Handler(){
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
mPromise.resolve((String) msg.obj);
}
};
@ReactMethod
public void myFunction(String params, Promise promise) {
mPromise = promise;
try {
if (params != null && !params.equals("")){
// 回調成功,返回結果信息
Message message = handler.obtainMessage();
message.obj = params;
handler.sendMessage(message);
}
}catch (IllegalViewOperationException e) {
// 回調失敗,返回錯誤信息
mPromise.reject('error', e.getMessage());
}
}
複製代碼
在 JS 端的調用狀況
customModule.myFunction("這是使用 Promise 回調的函數方法")
.then(result => {
console.log(result); // 顯示: 這是從原生返回的字符串
})
複製代碼
經過自定義 Module 進行交互已經能夠解決咱們的大部分開發需求,然而有的時候,基於性能和開發工做量的角度考慮,咱們能夠將原生的組件或佈局封裝好,併爲這個原生 View 創建一個繼承自 SimpleViewManager 或 ViewGroupManager 的 ViewManager 類。經過這個 ViewManager 能夠註冊一系列原生端和 JS 端的參數及事件映射,達到交互的目的。
封裝原生 View 包括如下幾步:
- 建立原生 View 類
- 建立 ViewManager
- 將 ViewManager 在自定義 Package 中註冊
- 在 Js 端進行調用
這裏以封裝一個簡單的原生 Button 的子類爲例:
public class MyButton extends Button {
public MyButton(Context context) {
super(context);
}
}
複製代碼
簡單的 View 能夠建立 ViewManager 類繼承 SimpleViewManager ,而經過佈局生成的複雜 View 能夠繼承自 ViewGroupManager 類,這裏咱們繼承 SimpleViewManager:
public class MyButtonViewManager extends SimpleViewManager<MyButton> {
@Override
public String getName() {
return "NativeMyButton"; // 此名稱用於在 JS 中引用
}
// 建立 View 實例
@Override
protected MyButton createViewInstance(ThemedReactContext reactContext) {
return new MyButton(reactContext);
}
}
複製代碼
以前咱們建立了自定義 Package 類 MyReactPackage,咱們只是使用了 createNativeModules() 方法完成了自定義 Module 的註冊,接下來咱們須要在 createViewManagers() 方法中註冊剛剛建立的 MyButtonViewManager 實例:
public class MyReactPackage implements ReactPackage {
@Nonnull
@Override
public List<NativeModule> createNativeModules(@Nonnull ReactApplicationContext reactContext) {
List<NativeModule> modules = new ArrayList<>();
modules.add(new MyModule(reactContext));
return modules;
}
@Nonnull
@Override
public List<ViewManager> createViewManagers(@Nonnull ReactApplicationContext reactContext) {
List<ViewManager> views = new ArrayList<>();
views.add(new MyButtonViewManager()); // 建立 MyButtonViewManager 實例並註冊到 ViewManager List 中
return views;
}
}
複製代碼
注意,若是你沒有完成第一章中 Package 在 Application 的註冊步驟,這裏也須要將 MyReactPackage 註冊到 Application 中。
完成了上面的原生代碼,咱們就能夠在 JS 端完成調用了:
import { requireNativeComponent, View} from 'react-native';
let MyButton = requireNativeComponent('NativeMyButton');
...
render() {
return (
<MyButton/>
);
}
...
複製代碼
在剛剛創建的 ViewManager 類中,咱們能夠經過 @ReactProps 註解方法,爲組件添加屬性,這裏咱們爲 MyButton 添加 text 屬性:
public class MyButtonViewManager extends SimpleViewManager<MyButton> {
@Override
public String getName() {
return "NativeMyButton";
}
@Override
protected MyButton createViewInstance(ThemedReactContext reactContext) {
return MyButton(reactContext);
}
// 暴露給 JS 的參數,用於設定名稱爲「text」的屬性,設定 Button 的文字
@ReactProp(name = "text")
public void setSrc(MyButton view, String text) {
view.setText(text);
}
}
複製代碼
此時能夠在 JS 端爲組件添加 text 屬性和它的值,完成設定 MyButton 的文字:
import { requireNativeComponent, View} from 'react-native';
let MyButton = requireNativeComponent('NativeMyButton');
...
render() {
return (
<MyButton text='這是個按鈕'/> ); } 複製代碼
有些時候咱們不只僅須要將數據以屬性值的形式,從 JS 端傳輸到原生端,還須要原生端對 JS 端發送數據完成交互,這時咱們須要使用事件 Event,經過發送事件完成交互。這裏咱們能夠將 MyButton 的點擊事件通知給 JS 端:
public class MyButtonViewManager extends SimpleViewManager<MyButton> {
@Override
public String getName() {
return "NativeMyButton";
}
@Override
protected MyButton createViewInstance(ThemedReactContext reactContext) {
MyButton button = new MyButton(reactContext);
button.setOnClickListener(v -> {
WritableMap event = Arguments.createMap(); // 這裏傳了個空的 event 對象,使用時能夠在 event 中加入要傳輸的數據
reactContext.getJSModule(RCTEventEmitter.class).receiveEvent(
viewId,
"onNativeClick", // 與下面註冊的要發送的事件名稱必須相同
event);
});
return button;
}
@ReactProp(name = "text")
public void setSrc(MyButton view, String text) {
view.setText(text);
}
@Nullable
@Override
public Map getExportedCustomDirectEventTypeConstants() {
return MapBuilder.of(
"onNativeClick", MapBuilder.of("registrationName", "onReactClick"));
// onNativeClick 是原生要發送的 event 名稱,onReactClick 是 JS 端組件中註冊的屬性方法名稱,中間的 registrationName 不可更改
}
}
複製代碼
而後在 JS 端就可使用 onReactClick 這個屬性來響應點擊事件:
import { requireNativeComponent, View} from 'react-native';
let MyButton = requireNativeComponent('NativeMyButton');
...
render() {
return (
<MyButton text='這是個按鈕' onReactClick={data=>{ // 這裏接收 event 傳過來的數據 console.log(data); }}/> ); } 複製代碼
JS 端不只僅只能從設定屬性值的方法來將數據傳輸給原生端,也可使用 UIManager.dispatchViewManagerCommand 方法來發送命令並攜帶數據給原生端,這裏咱們添加了一條命令 changeText 讓 MyButton 點擊後更換按鈕上的文字:
import { requireNativeComponent, View} from 'react-native';
let MyButton = requireNativeComponent('NativeMyButton');
...
changeButtonText = () => {
UIManager.dispatchViewManagerCommand(
findNodeHandle(this.nativeUI),
UIManager.TemplateMenuView.Commands.changeText, //Commands.changeText須要與native層定義的命令名稱一致
['這是新的按鈕'] //命令攜帶的數據
);
}
render() {
return (
<MyButton ref={view => this.nativeUI = view} text='這是個按鈕' onReactClick={data=>{ this.changeButtonText; // 點擊時回傳給原生端命令 }}/> ); } 複製代碼
在原生端,咱們須要在 ViewManager 中重寫 getCommandsMap() 方法創建命令的映射表,而後重寫 receiveCommand() 方法完成接收命令後的操做
public class MyButtonViewManager extends SimpleViewManager<MyButton> {
@Override
public String getName() {
return "NativeMyButton";
}
@Override
protected MyButton createViewInstance(ThemedReactContext reactContext) {
MyButton button = new MyButton(reactContext);
button.setOnClickListener(v -> {
WritableMap event = Arguments.createMap();
reactContext.getJSModule(RCTEventEmitter.class).receiveEvent(
viewId,
"onNativeClick",
event);
});
return button;
}
@ReactProp(name = "text")
public void setSrc(MyButton view, String text) {
view.setText(text);
}
@Nullable
@Override
public Map getExportedCustomDirectEventTypeConstants() {
return MapBuilder.of(
"onNativeClick", MapBuilder.of("registrationName", "onReactClick"));
}
@Override
public Map<String, Integer> getCommandsMap() {
return MapBuilder.of(
「changeText」, 0 // changeText 是命令名稱,0 是命令 id
);
}
@Override
public void receiveCommand(MyButton view, int commandId, @Nullable ReadableArray args) {
switch (commandId){
case 0: // 當命令 id 爲 0 時
String newText = args.getString(0); // 咱們在 JS 只傳了一個字符串過來,在這裏接收
view.setText(newText); // 爲按鈕設定新的文字
break;
default:
break;
}
}
}
複製代碼
利用原生View進行交互的時候,咱們已經利用了發送事件的機制,完成原生端與 JS 端的通訊,但前提是須要將原生 View 的屬性方法與原生事件先註冊映射,才能獲取這個事件。其實事件 Event 是能夠經過 RCTDeviceEventEmitter 靈活完成發送的,原生端將事件名稱和事件數據發送後,JS 端須要根據事件名稱預先註冊一個監聽器,來響應這個接收的事件,這也是最爲靈活的一種交互方式。
//定義向 RN 發送事件的函數
public void sendEvent(ReactContext reactContext, String eventName, @Nullable WritableMap params) {
reactContext
.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)
.emit(eventName,params);
}
// 發送成功消息
public notifySuccessMessage(ReactContext reactContext, String msg) {
WritableMap event = Arguments.createMap();
event.putString("message", msg);
sendEvent(reactApplicationContext, "SUCCESS", event);
}
// 發送失敗消息
public notifyErrorMessage(ReactContext reactContext) {
WritableMap event = Arguments.createMap();
sendEvent(reactApplicationContext, "ERROR", event);
}
複製代碼
componentDidMount() {
// 收到監聽
this.listener = DeviceEventEmitter.addListener('SUCCESS', (message) => {
// 收到監聽後想作的事情,’SUCCESS‘ 必須與原生層傳遞的 eventName 一致
console.warn(message);
dosomething...
});
this.errorListener = DeviceEventEmitter.addListener('ERROR', (message) => {
// 收到監聽後想作的事情,’ERROR‘ 必須與原生層傳遞的 eventName 一致
console.warn(message);
dosomething...
});
}
componentWillUnmount() {
// 移除監聽
if (this.listener) { this.listener.remove() }
if (this.errorListener) { this.listener.remove() }
}
複製代碼
以上能夠看出發送事件能夠在任什麼時候機,調用 sendEvent() 方法便可。但並非任什麼時候機均可以順利得到 ReactContext 上下文對象,因此最好將發送事件的方法封裝成一個工具類,在 App 生命週期較早的時機進行初始化,傳入 ReactContext 上下文對象,而後便可在想發送事件的時機,只傳遞事件名和數據便可。
Update:
上面的寫法中在 Js 端使用的是 DeviceEventEmitter.addListener(eventName, function) 方法註冊的監聽器,這是由於安卓端使用了 DeviceEventManagerModule.RCTDeviceEventEmitter.class 類完成的事件發送。但 iOS 端發送事件時,使用上面的方法註冊監聽器是沒法響應的,這是由於在發送事件時使用的是 RCTEventEmitter 類,這個類中也是調用了 RCTDeviceEventEmitter 類完成的事件發送,但在發送前檢查了註冊的監聽器數量:
if (_listenerCount > 0) { [_bridge enqueueJSCall:@"RCTDeviceEventEmitter" method:@"emit" args:body ? @[eventName, body] : @[eventName] completion:NULL]; } 複製代碼
爲了兼容兩端,因此 JS 端應該使用 NativeEventEmitter.addListener(eventName, function) 方法來註冊監聽器,完整的 JS 端代碼:
const eventEmitter = NativeModules.EventEmitter; const nativeEmitter = new NativeEventEmitter(eventEmitter); const subscription = nativeEmitter.addListener( eventEmitter.SUCCESS, message => { console.warn(message); dosomething... } ); 複製代碼
能夠看出 JS 端的代碼是經過使用自定義 Module 完成的監聽器註冊,因此安卓端也應該增長一個自定義 Module,代碼以下:
public class ReactEventEmitterModule extends >ReactContextBaseJavaModule { public ReactEventEmitterModule(ReactApplicationContext reactContext) { super(reactContext); } @Override public Map<String, Object> getConstants() { Map<String, Object> constants = new HashMap<>(); constants.put("SUCCESS", "ThisIsSuccessConstant"); return constants; } @Override public String getName() { return "EventEmitter"; } } 複製代碼
原文連接: tech.meicai.cn/detail/96 也可微信搜索小程序「美菜產品技術團隊」,乾貨滿滿且每週更新,想學習技術的你不要錯過哦。