如何開發React Native 原生模組(Native Modules)?看完這篇文章就夠了(Android)
期待已久的新課上線啦!解鎖React Native開發新姿勢,一網打盡React Native最新與最熱技術,點我Get!!!
前言
一直想寫一下我在React Native原生模組封裝方面的一些經驗和心得,來分享給大家,但實在抽不開身,今天看了一下日曆發現2018年馬上就結束了,所以就趕年底將這篇博文寫好併發布(其實是兩篇: 要看iOS篇的點這裡
《React Native iOS原生模組開發》 )。
我平時在用React Native開發App時會用到一些原生模組,比如:在做社會化分享、第三方登入、掃描、通訊錄,日曆等等,想必大家也是一樣。
關於在React Native中使用原生模組,在這裡引用React Native官方文件的一段話:
有時候App需要訪問平臺API,但在React Native可能還沒有相應的模組。或者你需要複用一些Java程式碼,而不想用JavaScript再重新實現一遍;又或者你需要實現某些高效能的、多執行緒的程式碼,譬如圖片處理、資料庫、或者一些高階擴充套件等等。
我們把React Native設計為可以在其基礎上編寫真正的原生程式碼,並且可以訪問平臺所有的能力。這是一個相對高階的特性,我們並不期望它應當在日常開發的過程中經常出現,但它確實必不可少,而且是存在的。如果React Native還不支援某個你需要的原生特性,你應當可以自己實現對該特性的封裝。
上面是我翻譯React Native官方文件上的一段話,大家如果想看英文版可以點這裡:Native Modules
在這篇文章中呢,我會帶著大家來開發一個從相簿獲取照片並裁切照片的專案,並結合這個專案來具體講解一下如何一步步開發React Native Android原生模組的。
[圖片上傳失敗...(image-611f80-1555862685270)]
提示:告訴大家一個好訊息,React Native視訊教程釋出了,大家現可以看視訊學React Native了。
首先,讓我們先看一下,開發Android原生模組的主要流程。
開發Android原生模組的主要流程
在這裡我把構建React Native Android原生模組的流程概括為以下三大步:
- 編寫原生模組的相關Java程式碼;
- 暴露介面與資料互動;
- 註冊與匯出React Native原生模組;
接下來讓我們一起來看一下每一步所需要做的一些事情。
原生模組開發實戰
在這裡我們就以開發一個從相簿獲取照片並裁切照片的實戰專案,來具體講解一下如何開發React Native Android原生模組的。
編寫原生模組的相關Java程式碼
這一步我們需要用到AndroidStudio。
首先我們用AndroidStudio開啟React Native專案根目錄下的android目錄,如圖:

open-react-native-android-native-project
用AndroidStudio第一次開啟這個Android專案的時候,AndroidStudio會下載一些此專案所需要的依賴,比如專案所依賴的Gradle版本等。這些依賴下載完成之後呢,AndroidStudio會對專案進行初始化,初始化成功之後在AndroidStudio的工具欄中可以看到一個名為“app”的一個可執行的模組,如圖:

open-react-native-android-native-project-success
接下來呢,我們就可以編寫Java程式碼了。
首先呢,我們先來實現一個Crop介面:
public interface Crop { /** * 選擇並裁切照片 * @param outputX * @param outputY * @param promise */ void selectWithCrop(int outputX,int outputY,Promise promise); }
我們建立一個CropImpl.java,在這個類中呢,我們實現了從相簿選擇照片以及裁切照片的功能:
/** * React Native Android原生模組開發 * Author: CrazyCodeBoy * 技術博文:http://www.devio.org * GitHub:https://github.com/crazycodeboy * Email:[email protected] */ public class CropImpl implements ActivityEventListener,Crop{ private final int RC_PICK=50081; private final int RC_CROP=50082; private final String CODE_ERROR_PICK="使用者取消"; private final String CODE_ERROR_CROP="裁切失敗"; private Promise pickPromise; private Uri outPutUri; private int aspectX; private int aspectY; private Activity activity; public static CropImpl of(Activity activity){ return new CropImpl(activity); } private CropImpl(Activity activity) { this.activity = activity; } public void updateActivity(Activity activity){ this.activity=activity; } @Override public void onActivityResult(Activity activity, int requestCode, int resultCode, Intent data) { if(requestCode==RC_PICK){ if (resultCode == Activity.RESULT_OK && data != null) {//從相簿選擇照片並裁剪 outPutUri= Uri.fromFile(Utils.getPhotoCacheDir(System.currentTimeMillis()+".jpg")); onCrop(data.getData(),outPutUri); } else { pickPromise.reject(CODE_ERROR_PICK,"沒有獲取到結果"); } }else if(requestCode==RC_CROP){ if (resultCode == Activity.RESULT_OK) { pickPromise.resolve(outPutUri.getPath()); }else { pickPromise.reject(CODE_ERROR_CROP,"裁剪失敗"); } } } //...省略部分程式碼 private void onCrop(Uri targetUri,Uri outputUri){ this.activity.startActivityForResult(IntentUtils.getCropIntentWith(targetUri,outputUri,aspectX,aspectY),RC_CROP); } }
關於Android拍照、從相簿或檔案中選擇照片,裁剪以及壓縮照片等更高階的功能實現,大家可以參考開源專案 TakePhoto
實現了從相簿選擇照片以及裁切照片的功能之後呢,接下來我們需要將 public void selectWithCrop(int aspectX, int aspectY, Promise promise)
暴露給React Native,以供js呼叫。
暴露介面與資料互動
接下了我們就向React Native暴露介面以及做一些資料互動部分的操作。為了暴露介面以及進行資料互動我們需要藉助React Native的 ReactContextBaseJavaModule
類,在這裡我們建立一個 ImageCropModule.java
類讓它繼承自 ReactContextBaseJavaModule
。
建立一個ReactContextBaseJavaModule
/** * React Native Android原生模組開發 * Author: CrazyCodeBoy * 技術博文:http://www.devio.org * GitHub:https://github.com/crazycodeboy * Email:[email protected] */ public class ImageCropModule extends ReactContextBaseJavaModule implements Crop{ private CropImpl cropImpl; public ImageCropModule(ReactApplicationContext reactContext) { super(reactContext); } @Override public String getName() { return "ImageCrop"; } //...省略部分程式碼 @Override @ReactMethod public void selectWithCrop(int aspectX, int aspectY, Promise promise) { getCrop().selectWithCrop(aspectX,aspectY,promise); } private CropImpl getCrop(){ if(cropImpl==null){ cropImpl=CropImpl.of(getCurrentActivity()); getReactApplicationContext().addActivityEventListener(cropImpl); }else { cropImpl.updateActivity(getCurrentActivity()); } return cropImpl; } }
在 ImageCropModule.java
類中,我們重寫了 public String getName()
方法,來暴露我們原生模組的名字。並在 public void selectWithCrop(int aspectX, int aspectY, Promise promise)
上添加了 @ReactMethod
註解來暴露介面,這樣以來我們就可以在js檔案中通過 ImageCrop.selectWithCrop
來呼叫我們所暴露給React Native的介面了。
接下來呢,我們來看一下原生模組和js模組是如何進行資料互動的?
原生模組和JS進行資料互動
在我們要實現的從相簿選擇照片並裁切的專案中,js模組需要告訴原生模組照片裁切的比例,等照片裁切完成後,原生模組需要對js模組進行回撥來告訴js模組照片裁切的結果,在這裡我們需要將照片裁切後生成的圖片的路徑告訴js模組。
提示:在所有的情況下js和原生模組之前進行通訊都是在非同步的情況下進行的。
接下來我們就來看下一JS是如何向原生模組傳遞資料的?
JS向原生模組傳遞資料:
為了實現JS向原生模組進行傳遞資料,我們可以直接通過呼叫原生模組所暴露出來的介面,來為介面方法設定引數。這樣以來我們就可以將資料通過介面引數傳遞到原生模組中,如:
/** * 選擇並裁切照片 * @param outputX * @param outputY * @param promise */ void selectWithCrop(int outputX,int outputY,Promise promise);
通過上述程式碼我們可以看出,js模組可以通過 selectWithCrop
方法來告訴原生模組要裁切照片的寬高比,最後一個引數是一個 Promise
,照片裁剪完成之後呢,原生模組可以通過 Promise
來對js模組進行回撥,來告訴裁切結果。
既然是js和Java進行資料傳遞,那麼他們兩者之間是如何進行型別轉換的呢:
在上述例子中我們通過 @ReactMethod
註解來暴露介面,被 @ReactMethod
標註的方法支援如下幾種資料型別。
被 @ReactMethod
標註的方法支援如下幾種資料型別的引數:
Boolean -> Bool Integer -> Number Double -> Number Float -> Number String -> String Callback -> function ReadableMap -> Object ReadableArray -> Array
原生模組向JS傳遞資料:
原生模組向JS傳遞資料我們可以藉助Callbacks與Promises,接下來就講一下如何通過他們兩個進行資料傳遞的。
Callbacks
原生模組支援一個特殊型別的引數-Callbacks,我們可以通過它來對js進行回撥,以告訴js呼叫原生模組方法的結果。
將我們 selectWithCrop
的引數改為Callbacks之後:
@Override public void selectWithCrop(int aspectX, int aspectY, Callback errorCallback,Callback successCallback) { this.errorCallback=errorCallback; this.successCallback=successCallback; this.aspectX=aspectX; this.aspectY=aspectY; this.activity.startActivityForResult(IntentUtils.getPickIntentWithGallery(),RC_PICK); }
在回撥的時候,我們就可以這樣寫:
if (resultCode == Activity.RESULT_OK) { successCallback.invoke(outPutUri.getPath()); }else { errorCallback.invoke(CODE_ERROR_CROP,"裁剪失敗"); }
在上述程式碼中我們通過 Callback
的 invoke
方法來對js進行對調,下面我們來看一下 Callback.java
的原始碼:
public interface Callback { /** * Schedule javascript function execution represented by this {@link Callback} instance * * @param args arguments passed to javascript callback method via bridge */ public void invoke(Object... args); }
從 Callback.java
的原始碼中我們可以看出,它是一個只有一個 public void invoke(Object... args)
方法的介面, invoke
方法接受一個可變引數,所以我們可以向js傳遞多個引數。
接下來呢,我們在js中就可以這樣來呼叫我們所暴露的介面:
ImageCrop.selectWithCrop(parseInt(x),parseInt(y),(error)=>{ console.log(error); },(result)=>{ console.log(result); })
提示:另外要告訴大家的是,無論是 Callback
還是我接下來要講的 Promise
,我們只能呼叫一次,也就是"you call me once,I can only call you once"。
Promises
除了上文所講的 Callback
之外React Native還為了我們提供了另外一種回撥js的方式叫-Promise。如果我們暴露的介面方法的最後一個引數是 Promise
時,如:
@Override @ReactMethod public void selectWithCrop(int aspectX, int aspectY, Promise promise) { getCrop().selectWithCrop(aspectX,aspectY,promise); }
那麼當js呼叫它的時候將會返回一個Promsie:
ImageCrop.selectWithCrop(parseInt(x),parseInt(y)).then(result=> { this.setState({ result: result }) }).catch(e=> { this.setState({ result: e }) });
另外,我們也可以使用ES2016的 async/await
語法,來簡化我們的程式碼:
async onSelectCrop() { var result=await ImageCrop.selectWithCrop(parseInt(x),parseInt(y)); }
這樣以來程式碼就簡化了很多。
因為,基於回撥的資料傳遞無論是Callback還是Promise,都只能呼叫一次。但,在實際專案開發中我們有時會向js多次傳遞資料,比如二維碼掃描原生模組,針對這種多次資料傳遞的情況我們該怎麼實現呢?
接下來我就為大家介紹一種原生模組可以向js多次傳遞資料的方式:
向js傳送事件
在原生模組中我們可以向js傳送多次事件,即使原生模組沒有被直接的呼叫。為了向js傳遞事件我們需要用到RCTDeviceEventEmitter,它是原生模組和js之間的一個事件發射器。
private void sendEvent(ReactContext reactContext,String eventName, @Nullable WritableMap params) { reactContext.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class) .emit(eventName, params); }
在上述方法中我們可以向js模組傳送任意次數的事件,其中 eventName
是我們要傳送事件的事件名, params
是此次事件所攜帶的資料,接下來呢我們就可以在js模組中監聽這個事件了:
componentDidMount() { //註冊掃描監聽 DeviceEventEmitter.addListener('onScanningResult',this.onScanningResult); } onScanningResult = (e)=> { this.setState({ scanningResult: e.result, }); }
另外,不要忘記在元件被解除安裝的時候移除監聽:
componentWillUnmount(){ DeviceEventEmitter.removeListener('onScanningResult',this.onScanningResult);//移除掃描監聽 }
到現在呢,暴露介面以及資料傳遞已經進行完了,接下來呢,我們就需要註冊與匯出React Native原生模組了。
註冊與匯出React Native原生模組
為了向React Native註冊我們剛才建立的原生模組,我們需要實現 ReactPackage
, ReactPackage
主要為註冊原生模組所存在,只有已經向React Native註冊的模組才能在js模組使用。
/** * React Native Android原生模組開發 * Author: CrazyCodeBoy * 技術博文:http://www.devio.org * GitHub:https://github.com/crazycodeboy * Email:[email protected] */ public class ImageCropReactPackage implements ReactPackage { @Override public List<Class<? extends JavaScriptModule>> createJSModules() { return Collections.emptyList(); } @Override public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) { return Collections.emptyList(); } @Override public List<NativeModule> createNativeModules( ReactApplicationContext reactContext) { List<NativeModule> modules = new ArrayList<>(); modules.add(new ImageCropModule(reactContext)); return modules; } }
在上述程式碼中,我們實現一個 ReactPackage
,接下來呢,我們還需要在 android/app/src/main/java/com/your-app-name/MainApplication.java
中註冊我們的 ImageCropReactPackage
:
@Override protected List<ReactPackage> getPackages() { return Arrays.<ReactPackage>asList( new MainReactPackage(), new ImageCropReactPackage()//在這裡將我們剛才建立的ImageCropReactPackage新增進來 ); }
原生模組註冊完成之後呢,我們接下來就需要為我們的原生模組匯出一個js模組,以方便我們使用它。
我們建立一個ImageCrop.js檔案,然後新增如下程式碼:
import { NativeModules } from 'react-native'; export default NativeModules.ImageCrop;
這樣以來呢,我們就可以在其他地方通過下面方式來使用我們所匯出的這個模組了:
import ImageCrop from './ImageCrop' //匯入ImageCrop.js //...省略部分程式碼 onSelectCrop() { let x=this.aspectX?this.aspectX:ASPECT_X; let y=this.aspectY?this.aspectY:ASPECT_Y; ImageCrop.selectWithCrop(parseInt(x),parseInt(y)).then(result=> { this.setState({ result: result }) }).catch(e=> { this.setState({ result: e }) }); } //...省略部分程式碼 }
現在呢,我們這個原生模組就開發好了,而且我們也使用了我們的這個原生模組。關於Android拍照、從相簿或檔案中選擇照片,裁剪以及壓縮照片等更高階的功能實現,大家也可以參考開源專案 TakePhoto
關於執行緒
在React Native中,JS模組執行在一個獨立的執行緒中。在我們為React Native開發原生模組的時候,如果有耗時的操作比如:檔案讀寫、網路操作等,我們需要新開闢一個執行緒,不然的話,這些耗時的操作會阻塞JS執行緒。在Android中我們可以藉助AsyncTask來實現多執行緒。另外,如果原生模組中需要更新UI,我們需要獲取主執行緒,然後在主執行緒中更新UI,如: http://coding.imooc.com/class/304.html
activity.runOnUiThread(new Runnable() { @Override public void run() { if (!activity.isFinishing()) { mSplashDialog = new Dialog(activity,fullScreen? R.style.SplashScreen_Fullscreen:R.style.SplashScreen_SplashTheme); mSplashDialog.setContentView(R.layout.launch_screen); mSplashDialog.setCancelable(false); if (!mSplashDialog.isShowing()) { mSplashDialog.show(); } } } });
可參考: SplashScreen.java
告訴大家一個好訊息,為大家精心準備的React Native視訊教程釋出了,大家現可以看視訊學React Native了。
如果,大家在開發原生模組中遇到問題可以在課程的對應章節的右邊進行留言,我看到了後會及時回覆的哦。
推薦學習:視訊教程 《最新版React Native+Redux打造高質量上線App》