有時(shí)候一個(gè)應(yīng)用需要訪問(wèn) React Native 平臺(tái)目前沒(méi)有對(duì)應(yīng)模塊的 API 。也許你需要復(fù)用一些已經(jīng)存在的 Java 代碼而不需要在 JavaScript 里面重新實(shí)現(xiàn),或者寫一些高性能,多線程的代碼,比如圖片處理,數(shù)據(jù)庫(kù),或者任何先進(jìn)的擴(kuò)展。
我們?cè)O(shè)計(jì)了 React Native 以致于你可以寫一些真正的原生代碼并且可以完全擁有系統(tǒng)的權(quán)限的能力。這是一個(gè)更加先進(jìn)的特征,并且我們不希望這是傳統(tǒng)開發(fā)過(guò)程中的一部分,然而它存在是非常重要的。如果 React Native 不支持你需要的原生特征,那么你應(yīng)該可以自己創(chuàng)建它。
這個(gè)引導(dǎo)將會(huì)使用這個(gè) Toast 的例子。我們將會(huì)可以通過(guò)使用 JavaScript 創(chuàng)建一個(gè) toast 消息。
我們從創(chuàng)建一個(gè)原生模塊開始。一個(gè)原生模塊是一個(gè)通常繼承 ReactContextBaseJavaModule 類的 Java 類,并且實(shí)現(xiàn)了 JavaScript 需要實(shí)現(xiàn)的方法。我們這里的目標(biāo)是允許通過(guò)使用 JavaScript 書寫
ToastAndroid.show('Awesome', ToastAndroid.SHORT);就可以在屏幕上面顯示一個(gè)短短的 toast 消息。
package com.facebook.react.modules.toast;
import android.widget.Toast;
import com.facebook.react.bridge.NativeModule;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod;
import java.util.Map;
public class ToastModule extends ReactContextBaseJavaModule {
private static final String DURATION_SHORT_KEY = "SHORT";
private static final String DURATION_LONG_KEY = "LONG";
public ToastModule(ReactApplicationContext reactContext) {
super(reactContext);
}
}
ReactContextBaseJavaModule 需要一個(gè)叫做 getName 的方法被實(shí)現(xiàn)。這個(gè)方法的目的就是返回在 JavaScript 里面表示這個(gè)類的叫做 NativeModule 的字符串的名字。在這里我們調(diào)用 ToastAndroid 因此我們可以在 JavaScript 里面使用 React.NativeModules.ToastAndroid 來(lái)得到它。
@Override
public String getName() {
return "ToastAndroid";
}
一個(gè)可選的叫做 getConstants 的方法會(huì)將傳遞給 JavaScript 的常量返回。這個(gè)方法的實(shí)現(xiàn)并不是必須的,但是卻對(duì)在 JavaScript 和 Java 中同步的預(yù)定義的關(guān)鍵字的值非常重要。
@Override
public Map<String, Object> getConstants() {
final Map<String, Object> constants = new HashMap<>();
constants.put(DURATION_SHORT_KEY, Toast.LENGTH_SHORT);
constants.put(DURATION_LONG_KEY, Toast.LENGTH_LONG);
return constants;
}
給 JavaScript 暴露一個(gè)方法,一個(gè) Java 方法需要使用 @ReactMethod 來(lái)注解。橋接的方法的返回值類型總是 void。React Native 的橋接是異步的,因此將一個(gè)結(jié)果傳遞給 JavaScript 的唯一方式就是使用回調(diào)函數(shù)或者調(diào)用事件(見(jiàn)下面)。
@ReactMethod
public void show(String message, int duration) {
Toast.makeText(getReactApplicationContext(), message, duration).show();
}
下面的參數(shù)類型是被使用 @ReactMethod 注解的方法支持的,并且它們直接對(duì)應(yīng) JavaScript 中對(duì)應(yīng)的值。
Boolean -> Bool
Integer -> Number
Double -> Number
Float -> Number
String -> String
Callback -> function
ReadableMap -> Object
ReadableArray -> Array
在使用 Java 的最后一步就是注冊(cè)這個(gè)模塊,這將在你的應(yīng)用包中的 createNativeModules 發(fā)生。如果一個(gè)模塊沒(méi)有被注冊(cè),那么它在 JavaScript 是不可用的。
class AnExampleReactPackage implements ReactPackage {
...
@Override
public List<NativeModule> createNativeModules(
ReactApplicationContext reactContext) {
List<NativeModule> modules = new ArrayList<>();
modules.add(new ToastModule(reactContext));
return modules;
}
當(dāng)包被創(chuàng)建的時(shí)候,它需要提供給 ReactInstanceManager 。可以看 UIExplorerActivity.java 這個(gè)例子。當(dāng)你初始化一個(gè)新工程的時(shí)候默認(rèn)的包是MainReactPackage.java。
mReactInstanceManager = ReactInstanceManager.builder()
.setApplication(getApplication())
.setBundleAssetName("AnExampleApp.android.bundle")
.setJSMainModuleName("Examples/AnExampleApp/AnExampleApp.android")
.addPackage(new AnExampleReactPackage())
.setUseDeveloperSupport(true)
.setInitialLifecycleState(LifecycleState.RESUMED)
.build();
為了能讓你更加方便的從 JavaScript 訪問(wèn)你的新功能的時(shí)候,通常會(huì)將原生模塊包裹在一個(gè) JavaScript 模塊里面。這不是必須的,但是節(jié)省了你的類庫(kù)的使用者每次都要 pull NativeModules 的不便。這個(gè) JavaScript 文件也為你增加任何 JavaScript 端功能提供了方便。
/**
* @providesModule ToastAndroid
*/
'use strict';
/**
* This exposes the native ToastAndroid module as a JS module. This has a function 'showText'
* which takes the following parameters:
*
* 1. String message: A string with the text to toast
* 2. int duration: The duration of the toast. May be ToastAndroid.SHORT or ToastAndroid.LONG
*/
var { NativeModules } = require('react-native');
module.exports = NativeModules.ToastAndroid;
現(xiàn)在,在你的 JavaScript 文件里面你可以像下面這樣調(diào)用方法:
var ToastAndroid = require('ToastAndroid')
ToastAndroid.show('Awesome', ToastAndroid.SHORT);
// Note: We require ToastAndroid without any relative filepath because
// of the @providesModule directive. Using @providesModule is optional.
原生模塊也提供了一種特殊的參數(shù)-一個(gè)回調(diào)。在大多數(shù)情況下這是給 JavaScript 返回結(jié)果使用的。
public class UIManagerModule extends ReactContextBaseJavaModule {
...
@ReactMethod
public void measureLayout(
int tag,
int ancestorTag,
Callback errorCallback,
Callback successCallback) {
try {
measureLayout(tag, ancestorTag, mMeasureBuffer);
float relativeX = PixelUtil.toDIPFromPixel(mMeasureBuffer[0]);
float relativeY = PixelUtil.toDIPFromPixel(mMeasureBuffer[1]);
float width = PixelUtil.toDIPFromPixel(mMeasureBuffer[2]);
float height = PixelUtil.toDIPFromPixel(mMeasureBuffer[3]);
successCallback.invoke(relativeX, relativeY, width, height);
} catch (IllegalViewOperationException e) {
errorCallback.invoke(e.getMessage());
}
}
...
使用以下方法可以來(lái)訪問(wèn)在 JavaScript 里面可以使用:
UIManager.measureLayout(
100,
100,
(msg) => {
console.log(msg);
},
(x, y, width, height) => {
console.log(x + ':' + y + ':' + width + ':' + height);
}
);
一個(gè)原生模塊支持只調(diào)用一次它的回調(diào)。它可以保存這個(gè)回調(diào),并且在以后調(diào)用。
有一點(diǎn)需要強(qiáng)調(diào)的就是在原生方法完成之后這個(gè)回調(diào)并不是立即被調(diào)用的-請(qǐng)記住橋接通信是異步的,因此這個(gè)也在運(yùn)行時(shí)循環(huán)里面。
原生模塊不應(yīng)該設(shè)想有它們將在哪些線程里面被調(diào)用,因?yàn)槟壳暗娜蝿?wù)在以后改變是主要的。如果一個(gè)塊調(diào)用是必須的,那么耗時(shí)操作將會(huì)被分配到間歇性的工作線程中,并且任何回調(diào)將會(huì)從這里開始。
原生模塊可以不需要立即被調(diào)用就可以給 JavaScript 發(fā)送事件。最簡(jiǎn)單的方式就是使用從 ReactContext 獲得的 RCTDeviceEventEmitter,就像下面的代碼片段:
...
private void sendEvent(ReactContext reactContext,
String eventName,
@Nullable WritableMap params) {
reactContext
.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)
.emit(eventName, params);
}
...
WritableMap params = Arguments.createMap();
...
sendEvent(reactContext, "keyboardWillShow", params);
JavaScript 模塊在那時(shí)可以通過(guò)使用 Subscribable 的 addListenerOn 來(lái)注冊(cè)并且接收事件。
var RCTDeviceEventEmitter = require('RCTDeviceEventEmitter');
...
var ScrollResponderMixin = {
mixins: [Subscribable.Mixin],
componentWillMount: function() {
...
this.addListenerOn(RCTDeviceEventEmitter,
'keyboardWillShow',
this.scrollResponderKeyboardWillShow);
...
},
scrollResponderKeyboardWillShow:function(e: Event) {
this.keyboardWillOpenTo = e;
this.props.onKeyboardWillShow && this.props.onKeyboardWillShow(e);
},