前言:
所謂原生模塊開發(fā)藕赞,很重要的就是Android/iOS原生代碼和ReactNative代碼的交互缚忧,以及在ReactNative代碼中使用原生的組件哼凯。下面將從這兩個地方開始講解(示例以Android平臺為基礎(chǔ)):
先回顧一下RN組件的生命周期:
constructor():
在組件被加載前最先調(diào)用,第一個語句必須是super(props)。
componentWillMount():
在初始渲染(render函數(shù)被RN框架執(zhí)行之前)前被執(zhí)行耻涛。在函數(shù)中調(diào)用setState函數(shù)改變了某些狀態(tài)機(jī)變量的值废酷,RN框架不會立馬執(zhí)行渲染操作,而是等該函數(shù)執(zhí)行完之后執(zhí)行初始渲染抹缕。
componentDidMount():
執(zhí)行完初始渲染之后立馬被調(diào)用澈蟆。將從網(wǎng)絡(luò)側(cè)請求數(shù)據(jù)的代碼放在這里比較合適。
componentWillReceiveProps(nextProps):
RN初始化渲染執(zhí)行完成后歉嗓,當(dāng)RN組件收到新的props時丰介,這個函數(shù)被調(diào)用。函數(shù)接受的參數(shù)是一個object為新的props鉴分。
shouldComponentUpdate(nextProps, nextState):
RN初始化渲染執(zhí)行完成后哮幢,當(dāng)RN組件接收到新的state或者props時,該函數(shù)被調(diào)用志珍。接受兩個參數(shù):第一個是新的props橙垢,第二個是新的state。該函數(shù)返回一個布爾類型的值伦糯,表示RN框架針對此次改變是否重新渲染該組件柜某。
componentWillUpdate(nextProps, nextState):
RN組件在初始化渲染執(zhí)行完成之后,RN框架在重新渲染RN組件前會調(diào)用這個函數(shù)敛纲。
componentDidUpdate(prevProps, prevState):
RN組件在初始化渲染執(zhí)行完成之后喂击,RN框架在重新渲染RN組件完成后會調(diào)用這個函數(shù),傳入的兩個參數(shù)是渲染前的Props和state淤翔。
componentWillUnmount():
在RN組件被卸載前翰绊,這個函數(shù)被執(zhí)行。
一旁壮、原生代碼和RN代碼的交互:
1 RN側(cè)主動調(diào)用原生模塊方法:回調(diào)函數(shù)和Promise機(jī)制
首先在電腦中部署Android開發(fā)和RN開發(fā)所需要的環(huán)境:
打開Android端的初始化項目(名為MyRNProject):
工程中只有一個Activity监嗜,且繼承于ReactActivity,將該Activity設(shè)置為應(yīng)用的入口抡谐,getMainComponentName()返回服務(wù)器上對應(yīng)的組件名稱
package com.myrnproject;
import android.os.Bundle;
import com.facebook.react.ReactActivity;
public class MainActivity extends ReactActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
}
/**
* Returns the name of the main component registered from JavaScript.
* This is used to schedule rendering of the component.
*/
@Override
protected String getMainComponentName() {
return "MyRNProject";
}
}
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.myrnproject"
android:versionCode="1"
android:versionName="1.0">
<uses-sdk
android:minSdkVersion="16"
android:targetSdkVersion="22" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
<uses-permission android:name="android.permission.MOUNT_UNMOUNT_FILESYSTEMS" />
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
<uses-permission android:name="android.permission.READ_CONTACTS" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<application
android:name=".MainApplication"
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:theme="@style/AppTheme">
<activity
android:name=".MainActivity"
android:configChanges="keyboard|keyboardHidden|orientation|screenSize"
android:label="@string/app_name"
android:windowSoftInputMode="adjustResize">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity android:name="com.facebook.react.devsupport.DevSettingsActivity" />
</application>
</manifest>
MainApplication為工程的Application裁奇,同樣需要繼承ReactApplication,getJSMainModuleName()函數(shù)返回加載的第一個js文件名稱麦撵。將需要注冊的ReactPackage添加到這里protected List<ReactPackage> getPackages(){}刽肠,new MyRNPackage()為我們自己定義的ReactPackage,該類用來創(chuàng)建工程中的NativeModule以及ViewManager等類厦坛。NativeModule為原生為RN提供的方法五垮,ViewManager為供RN使用的原生組件。
package com.myrnproject;
import android.app.Application;
import com.facebook.react.ReactApplication;
import com.facebook.react.ReactNativeHost;
import com.facebook.react.ReactPackage;
import com.facebook.react.shell.MainReactPackage;
import com.facebook.soloader.SoLoader;
import java.util.Arrays;
import java.util.List;
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 MyRNPackage()
);
}
@Override
protected String getJSMainModuleName() {
return "index";
}
};
@Override
public ReactNativeHost getReactNativeHost() {
return mReactNativeHost;
}
@Override
public void onCreate() {
super.onCreate();
SoLoader.init(this, /* native exopackage */ false);
}
}
package com.myrnproject;
import com.facebook.react.ReactPackage;
import com.facebook.react.bridge.NativeModule;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.uimanager.ViewManager;
import java.util.ArrayList;
import java.util.List;
/**
* Created by tianxiying on 2017/12/6.
*/
public class MyRNPackage implements ReactPackage {
@Override
public List<NativeModule> createNativeModules(ReactApplicationContext reactContext) {
List<NativeModule> moudles = new ArrayList<>();
moudles.add(new AddressModule(reactContext)); //加入開發(fā)接口
moudles.add(new ToastModule(reactContext));
moudles.add(new EventModule(reactContext));
return moudles;
}
@Override
public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) {
List<ViewManager> list = new ArrayList<ViewManager>();
list.add(new MyTextViewManager());
list.add(new MyCalendarViewManager());
return list;
}
}
接下來我要為RN提供一個安卓原生代碼的方法杜秸,用Toast舉例:我創(chuàng)建了一個ToastModule繼承于ReactContextBaseJavaModule,getName() 函數(shù)返回模塊名稱润绎,在RN中通過該名稱找到該模塊撬碟,在上面的MyRNPackage中我們已將其加入管理器中诞挨。
內(nèi)部代碼如下:
package com.myrnproject;
import android.os.Handler;
import android.widget.Toast;
import com.facebook.react.bridge.Callback;
import com.facebook.react.bridge.Promise;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod;
import java.util.HashMap;
import java.util.Map;
/**
* Created by tianxiying on 2017/12/14.
*/
public class ToastModule extends ReactContextBaseJavaModule {
private ReactApplicationContext aContext;
private Promise interfacePromise;
private Handler handler;
private static final String DURATION_SHORT_KEY = "SHORT";
private static final String DURATION_LONG_KEY = "LONG";
public ToastModule(ReactApplicationContext reactContext) {
super(reactContext);
aContext = reactContext;
}
@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;
}
@Override
public String getName() {
return "ToastModule";
}
@ReactMethod
public void showToast(String message, int duration, Callback callback) {
try {
Toast.makeText(aContext, message, duration).show();
callback.invoke("success");
} catch (Exception e) {
callback.invoke("exception" + e.toString());
}
}
@ReactMethod
public void showToastLater(String message, int duration, final Callback callback) {
try {
Toast.makeText(aContext, message, duration).show();
handler = new Handler();
handler.postDelayed(new Runnable() {
@Override
public void run() {
doCallBack(callback);
}
}, 5000);
} catch (Exception e) {
callback.invoke("exception" + e.toString());
}
}
public void doCallBack(Callback callback) {
callback.invoke("原生5秒后回調(diào)CallBack");
}
}
1.1 回調(diào)函數(shù):
RN調(diào)用原生方法,然后在原生側(cè)調(diào)用RN側(cè)提供的回調(diào)函數(shù)呢蛤,實現(xiàn)交互惶傻。
以ToastModule為例,加上@ReactMethod注解的方法為RN模塊可以直接調(diào)用方法:
@ReactMethod
public void showToast(String message, int duration, Callback callback) {
try {
Toast.makeText(aContext, message, duration).show();
callback.invoke("success");
} catch (Exception e) {
callback.invoke("exception" + e.toString());
}
}
該方法有三個參數(shù)其障,分別是信息字符串银室、顯示時長以及RN端傳入的回調(diào),getConstants()中約定了對應(yīng)的變量方便找RN端使用励翼。
在RN端的代碼如下:首先將ToastModule導(dǎo)入蜈敢,在showToast函數(shù)中直接調(diào)用ToastModule.showToast(),將需要參數(shù)包含回調(diào)函數(shù)傳入∑В回調(diào)成功將改變state中的變量值抓狭。
import React, {Component} from 'react';
import {
View,
Text,
Image,
StyleSheet,
PixelRatio,
TouchableOpacity,
NativeModules,
DeviceEventEmitter,
Platform,
} from 'react-native';
let ToastModule = NativeModules.ToastModule;
...
<Text style={styles.text_title}>Toast(回調(diào)函數(shù))</Text>
<TouchableOpacity onPress={(message) => this.showToast("hello world!")}>
<Text style={styles.text_item}>
{"回調(diào)結(jié)果:" + this.state.interfaceResult}
</Text>
</TouchableOpacity>
...
showToast(message) {
console.log("showToast!");
if (Platform.OS === "android") {
ToastModule.showToast(message,
ToastModule.SHORT,
(msg) => {
this.setState({
interfaceResult: msg
})
});
}
}
下面是運行截圖:
原生代碼調(diào)用了回調(diào)函數(shù)造烁,回調(diào)函數(shù)并不會馬上執(zhí)行否过,因為是混合開發(fā)的橋接方式本來就是異步的)
注意事項:需要注意的是,回調(diào)函數(shù)必須要RN側(cè)提供的情況下才能調(diào)用惭蟋,調(diào)用Callback.invoke()即可執(zhí)行回調(diào)方法苗桂,也可以將Callback引用暫時保存起來,不立即調(diào)用告组,但是該回調(diào)方法只能調(diào)用一次煤伟,多于一次將會如下錯誤:
1.2 Promise機(jī)制
建議使用,因為Promise在RN中使用非常廣泛惹谐,能力也非常強(qiáng)大持偏。
下面我在RN模塊中,調(diào)用原生代碼讀取手機(jī)通訊錄并將其顯示在界面上:
package com.myrnproject;
import android.app.Activity;
import android.content.Intent;
import android.database.Cursor;
import android.net.Uri;
import android.os.Bundle;
import android.provider.ContactsContract;
import com.facebook.react.bridge.ActivityEventListener;
import com.facebook.react.bridge.BaseActivityEventListener;
import com.facebook.react.bridge.Promise;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod;
import org.json.JSONObject;
import static android.app.Activity.RESULT_OK;
/**
* Created by tianxiying on 2017/12/6.
*/
public class AddressModule extends ReactContextBaseJavaModule {
private ReactApplicationContext aContext;
private Promise interfacePromise;
private final ActivityEventListener mActivityEventListener = new BaseActivityEventListener() {
@Override
public void onActivityResult(Activity activity, int requestCode, int resultCode, Intent data) {
if (requestCode != 1 || resultCode != RESULT_OK) return;
Uri contactData = data.getData();
Cursor cursor = activity.managedQuery(contactData, null, null, null, null);
cursor.moveToFirst();
String toRNMessage = getContactInfo(cursor);
if (toRNMessage != null) {
interfacePromise.resolve(toRNMessage);
}
}
};
private String getContactInfo(Cursor cursor) {
try {
String name = "";
String phoneNumber = "";
int idColumn = cursor.getColumnIndex(ContactsContract.Contacts._ID);
String contactId = cursor.getString(idColumn);
String queryString = ContactsContract.CommonDataKinds.Phone.CONTACT_ID + "=" + contactId;
Uri aUri = ContactsContract.CommonDataKinds.Phone.CONTENT_URI;
Cursor phone = aContext.getContentResolver().query(aUri, null, queryString,
null, null);
String dn = ContactsContract.Contacts.DISPLAY_NAME;
String pn = ContactsContract.CommonDataKinds.Phone.NUMBER;
if (phone.moveToFirst()) {
for (; !phone.isAfterLast(); phone.moveToNext()) {
dn = name = cursor.getString(cursor.getColumnIndex(dn));
phoneNumber = phone.getString(phone.getColumnIndex(pn));
}
phone.close();
}
JSONObject jsonObject = new JSONObject();
jsonObject.put("msgType","pickContactResult");
jsonObject.put("displayName",name);
jsonObject.put("peerNumber",phoneNumber);
return jsonObject.toString();
} catch (Exception e) {
interfacePromise.reject("error while get contact", e);
}
return null;
}
public AddressModule(ReactApplicationContext reactContext) {
super(reactContext);
aContext = reactContext;
reactContext.addActivityEventListener(mActivityEventListener);
}
@Override
public String getName() {
return "AddressModule";
}
@ReactMethod
public void handleMessage(String aMessage, Promise aPromise) {
interfacePromise = aPromise;
Intent aIntent = new Intent(Intent.ACTION_PICK);
aIntent.setType(ContactsContract.Contacts.CONTENT_TYPE);
Bundle b = new Bundle();
aContext.startActivityForResult(aIntent, 1, b);
}
}
重點關(guān)注一下 public void handleMessage(String aMessage, Promise aPromise)函數(shù)氨肌,它有兩個參數(shù)鸿秆,分別是一個消息字符串和一個Promise引用,在該方法被調(diào)用時我們將Promise引用保存下來為interfacePromise怎囚,ActivityEventListener負(fù)責(zé)監(jiān)聽Activity的生命周期卿叽,我們已將其注冊,在通訊錄頁面返回的時候恳守,onActivityResult將被調(diào)用考婴,我們將用戶點擊的通訊錄好友信息獲取到保存為Json格式,使用interfacePromise.resolve(toRNMessage)將其返回到RN端催烘。
RN端代碼如下:在then函數(shù)中處理成功回調(diào)沥阱,在catch處理異常回調(diào)伊群。
import React, {Component} from 'react';
import {
View,
Text,
Image,
StyleSheet,
PixelRatio,
TouchableOpacity,
NativeModules,
DeviceEventEmitter,
Platform,
} from 'react-native';
let AddressModule = NativeModules.AddressModule;
...
<Text style={styles.text_title}>讀取通訊錄(Promise機(jī)制)</Text>
<TouchableOpacity onPress={() => this.userPressAddressBook()}>
<Text style={styles.text_item}>
{this.state.ContactName + ":" + this.state.PhoneNumber}
</Text>
</TouchableOpacity>
...
userPressAddressBook() {
console.log("clickUserPressAddressBook考杉!");
AddressModule.handleMessage("testMessage").then(
(result) => {
console.log(result);
let aObj = JSON.parse(result);
this.setState({
PhoneNumber: aObj.peerNumber,
ContactName: aObj.displayName,
})
}
).catch(
(error) => {
console.log(error);
}
);
}
運行圖如下:
Promise對比回調(diào)機(jī)制不需要設(shè)置和實現(xiàn)事件的監(jiān)聽函數(shù)策精,通過Promise機(jī)制讓異步處理便于書寫、閱讀與理解崇棠。
2 原生代碼主動調(diào)用RN代碼:通過SendEvent——發(fā)送消息的方式咽袜。
首先,我定義了一個EventModule用來主動發(fā)送消息:
package com.myrnproject;
import com.facebook.react.bridge.Arguments;
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 com.facebook.react.bridge.WritableMap;
import com.facebook.react.modules.core.DeviceEventManagerModule;
import javax.annotation.Nullable;
/**
* Created by tianxiying on 2017/12/14.
*/
public class EventModule extends ReactContextBaseJavaModule {
private ReactApplicationContext aContext;
public EventModule(ReactApplicationContext reactContext) {
super(reactContext);
aContext = reactContext;
}
@Override
public String getName() {
return "EventModule";
}
public void sendEvent(ReactContext reactContext, String eventName, @Nullable WritableMap params) {
reactContext
.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)
.emit(eventName, params);
}
@ReactMethod
public void testSendEvent() {
WritableMap parms = Arguments.createMap();
sendEvent(aContext,"testEvent",parms);
}
}
重點關(guān)注sendEvent方法枕稀,通過reactContext.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class).emit(eventName, params)询刹,其中eventName代表消息名,params代表參數(shù)萎坷。
為了方便測試凹联,我寫了一個testSendEvent方法,由RN界面來觸發(fā)sendEvent方法食铐。
RN中代碼如下:在componentDidMount進(jìn)行了監(jiān)聽事件的注冊匕垫。
import React, {Component} from 'react';
import {
View,
Text,
Image,
StyleSheet,
PixelRatio,
TouchableOpacity,
NativeModules,
DeviceEventEmitter,
Platform,
} from 'react-native';
let EventModule = NativeModules.EventModule;
...
componentDidMount() {
DeviceEventEmitter.addListener('testEvent', (message) => {
// handle event.
console.log("testEvent!" + message);
this.setReceivedResult();
});
}
...
<Text style={styles.text_title}>原生調(diào)RN(sendEvent)</Text>
<TouchableOpacity onPress={() => this.testSendEvent()}>
<Text style={styles.text_item}>
{"消息結(jié)果:" + this.state.sendEventResult}
</Text>
</TouchableOpacity>
...
testSendEvent() {
console.log("testSendEvent虐呻!");
EventModule.testSendEvent();
}
...
setReceivedResult() {
console.log("set received象泵!");
this.setState({
sendEventResult: 'received sendevent'
});
}
運行截圖如下: