尊重版權(quán),未經(jīng)授權(quán)不得轉(zhuǎn)載
本文出自:賈鵬輝的技術(shù)博客(http://www.reibang.com/p/dd37ab5f9a09)
告訴大家一個好消息,為大家精心準(zhǔn)備的React Native視頻教程發(fā)布了狮鸭,大家現(xiàn)可以看視頻學(xué)React Native了合搅。
前言
一直想寫一下我在React Native原生模塊封裝方面的一些經(jīng)驗和心得,來分享給大家歧蕉,但實在抽不開身灾部,今天看了一下日歷發(fā)現(xiàn)馬上就春節(jié)了,所以就趕在春節(jié)之前將這篇博文寫好并發(fā)布(其實是兩篇:要看iOS篇的點這里
《React Native iOS原生模塊開發(fā)》)惯退。
我平時在用React Native開發(fā)App時會用到一些原生模塊赌髓,比如:在做社會化分享、第三方登錄、掃描锁蠕、通信錄夷野,日歷等等,想必大家也是一樣匿沛。
關(guān)于在React Native中使用原生模塊扫责,在這里引用React Native官方文檔的一段話:
有時候App需要訪問平臺API,但在React Native可能還沒有相應(yīng)的模塊逃呼。或者你需要復(fù)用一些Java代碼者娱,而不想用JavaScript再重新實現(xiàn)一遍抡笼;又或者你需要實現(xiàn)某些高性能的、多線程的代碼黄鳍,譬如圖片處理推姻、數(shù)據(jù)庫、或者一些高級擴(kuò)展等等框沟。
我們把React Native設(shè)計為可以在其基礎(chǔ)上編寫真正的原生代碼藏古,并且可以訪問平臺所有的能力。這是一個相對高級的特性忍燥,我們并不期望它應(yīng)當(dāng)在日常開發(fā)的過程中經(jīng)常出現(xiàn)拧晕,但它確實必不可少,而且是存在的梅垄。如果React Native還不支持某個你需要的原生特性厂捞,你應(yīng)當(dāng)可以自己實現(xiàn)對該特性的封裝。
上面是我翻譯React Native官方文檔上的一段話队丝,大家如果想看英文版可以點這里:Native Modules
在這篇文章中呢靡馁,我會帶著大家來開發(fā)一個從相冊獲取照片并裁切照片的項目,并結(jié)合這個項目來具體講解一下如何一步步開發(fā)React Native Android原生模塊的机久。
[圖片上傳失敗...(image-f79608-1544634754617)]
提示:告訴大家一個好消息臭墨,React Native視頻教程發(fā)布了,大家現(xiàn)可以看視頻學(xué)React Native了膘盖。
首先胧弛,讓我們先看一下振劳,開發(fā)Android原生模塊的主要流程鸟蟹。
開發(fā)Android原生模塊的主要流程
在這里我把構(gòu)建React Native Android原生模塊的流程概括為以下三大步:
- 編寫原生模塊的相關(guān)Java代碼;
- 暴露接口與數(shù)據(jù)交互勒魔;
- 注冊與導(dǎo)出React Native原生模塊践图;
接下來讓我們一起來看一下每一步所需要做的一些事情掺冠。
原生模塊開發(fā)實戰(zhàn)
在這里我們就以開發(fā)一個從相冊獲取照片并裁切照片的實戰(zhàn)項目,來具體講解一下如何開發(fā)React Native Android原生模塊的。
編寫原生模塊的相關(guān)Java代碼
這一步我們需要用到AndroidStudio德崭。
首先我們用AndroidStudio打開React Native項目根目錄下的android目錄斥黑,如圖:
用AndroidStudio第一次打開這個Android項目的時候,AndroidStudio會下載一些此項目所需要的依賴眉厨,比如項目所依賴的Gradle版本等锌奴。這些依賴下載完成之后呢,AndroidStudio會對項目進(jìn)行初始化憾股,初始化成功之后在AndroidStudio的工具欄中可以看到一個名為“app”的一個可運行的模塊鹿蜀,如圖:
接下來呢,我們就可以編寫Java代碼了服球。
首先呢茴恰,我們先來實現(xiàn)一個Crop接口:
public interface Crop {
/**
* 選擇并裁切照片
* @param outputX
* @param outputY
* @param promise
*/
void selectWithCrop(int outputX,int outputY,Promise promise);
}
我們創(chuàng)建一個CropImpl.java,在這個類中呢斩熊,我們實現(xiàn)了從相冊選擇照片以及裁切照片的功能:
/**
* React Native Android原生模塊開發(fā)
* Author: CrazyCodeBoy
* 技術(shù)博文:http://www.devio.org
* GitHub:https://github.com/crazycodeboy
* Email:crazycodeboy@gmail.com
*/
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,"沒有獲取到結(jié)果");
}
}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);
}
}
關(guān)于Android拍照往枣、從相冊或文件中選擇照片,裁剪以及壓縮照片等更高級的功能實現(xiàn)粉渠,大家可以參考開源項目TakePhoto
實現(xiàn)了從相冊選擇照片以及裁切照片的功能之后呢分冈,接下來我們需要將public void selectWithCrop(int aspectX, int aspectY, Promise promise)
暴露給React Native,以供js調(diào)用霸株。
暴露接口與數(shù)據(jù)交互
接下了我們就向React Native暴露接口以及做一些數(shù)據(jù)交互部分的操作雕沉。為了暴露接口以及進(jìn)行數(shù)據(jù)交互我們需要借助React Native的ReactContextBaseJavaModule
類,在這里我們創(chuàng)建一個ImageCropModule.java
類讓它繼承自ReactContextBaseJavaModule
淳衙。
創(chuàng)建一個ReactContextBaseJavaModule
/**
* React Native Android原生模塊開發(fā)
* Author: CrazyCodeBoy
* 技術(shù)博文:http://www.devio.org
* GitHub:https://github.com/crazycodeboy
* Email:crazycodeboy@gmail.com
*/
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
來調(diào)用我們所暴露給React Native的接口了。
接下來呢靴跛,我們來看一下原生模塊和js模塊是如何進(jìn)行數(shù)據(jù)交互的缀雳?
原生模塊和JS進(jìn)行數(shù)據(jù)交互
在我們要實現(xiàn)的從相冊選擇照片并裁切的項目中,js模塊需要告訴原生模塊照片裁切的比例梢睛,等照片裁切完成后肥印,原生模塊需要對js模塊進(jìn)行回調(diào)來告訴js模塊照片裁切的結(jié)果,在這里我們需要將照片裁切后生成的圖片的路徑告訴js模塊绝葡。
提示:在所有的情況下js和原生模塊之前進(jìn)行通信都是在異步的情況下進(jìn)行的深碱。
接下來我們就來看下一JS是如何向原生模塊傳遞數(shù)據(jù)的?
JS向原生模塊傳遞數(shù)據(jù):
為了實現(xiàn)JS向原生模塊進(jìn)行傳遞數(shù)據(jù)藏畅,我們可以直接通過調(diào)用原生模塊所暴露出來的接口敷硅,來為接口方法設(shè)置參數(shù)。這樣以來我們就可以將數(shù)據(jù)通過接口參數(shù)傳遞到原生模塊中,如:
/**
* 選擇并裁切照片
* @param outputX
* @param outputY
* @param promise
*/
void selectWithCrop(int outputX,int outputY,Promise promise);
通過上述代碼我們可以看出绞蹦,js模塊可以通過selectWithCrop
方法來告訴原生模塊要裁切照片的寬高比力奋,最后一個參數(shù)是一個Promise
,照片裁剪完成之后呢幽七,原生模塊可以通過Promise
來對js模塊進(jìn)行回調(diào)景殷,來告訴裁切結(jié)果。
既然是js和Java進(jìn)行數(shù)據(jù)傳遞澡屡,那么他們兩者之間是如何進(jìn)行類型轉(zhuǎn)換的呢:
在上述例子中我們通過@ReactMethod
注解來暴露接口猿挚,被 @ReactMethod
標(biāo)注的方法支持如下幾種數(shù)據(jù)類型。
被
@ReactMethod
標(biāo)注的方法支持如下幾種數(shù)據(jù)類型的參數(shù):
Boolean -> Bool
Integer -> Number
Double -> Number
Float -> Number
String -> String
Callback -> function
ReadableMap -> Object
ReadableArray -> Array
原生模塊向JS傳遞數(shù)據(jù):
原生模塊向JS傳遞數(shù)據(jù)我們可以借助Callbacks與Promises挪蹭,接下來就講一下如何通過他們兩個進(jìn)行數(shù)據(jù)傳遞的亭饵。
Callbacks
原生模塊支持一個特殊類型的參數(shù)-Callbacks,我們可以通過它來對js進(jìn)行回調(diào)梁厉,以告訴js調(diào)用原生模塊方法的結(jié)果。
將我們selectWithCrop
的參數(shù)改為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);
}
在回調(diào)的時候踏兜,我們就可以這樣寫:
if (resultCode == Activity.RESULT_OK) {
successCallback.invoke(outPutUri.getPath());
}else {
errorCallback.invoke(CODE_ERROR_CROP,"裁剪失敗");
}
在上述代碼中我們通過Callback
的invoke
方法來對js進(jìn)行對調(diào)词顾,下面我們來看一下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
方法接受一個可變參數(shù)肉盹,所以我們可以向js傳遞多個參數(shù)。
接下來呢疹尾,我們在js中就可以這樣來調(diào)用我們所暴露的接口:
ImageCrop.selectWithCrop(parseInt(x),parseInt(y),(error)=>{
console.log(error);
},(result)=>{
console.log(result);
})
提示:另外要告訴大家的是上忍,無論是
Callback
還是我接下來要講的Promise
,我們只能調(diào)用一次纳本,也就是"you call me once,I can only call you once"窍蓝。
Promises
除了上文所講的Callback
之外React Native還為了我們提供了另外一種回調(diào)js的方式叫-Promise。如果我們暴露的接口方法的最后一個參數(shù)是Promise
時繁成,如:
@Override @ReactMethod
public void selectWithCrop(int aspectX, int aspectY, Promise promise) {
getCrop().selectWithCrop(aspectX,aspectY,promise);
}
那么當(dāng)js調(diào)用它的時候?qū)祷匾粋€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));
}
這樣以來代碼就簡化了很多巾腕。
因為面睛,基于回調(diào)的數(shù)據(jù)傳遞無論是Callback還是Promise,都只能調(diào)用一次尊搬。但叁鉴,在實際項目開發(fā)中我們有時會向js多次傳遞數(shù)據(jù),比如二維碼掃描原生模塊佛寿,針對這種多次數(shù)據(jù)傳遞的情況我們該怎么實現(xiàn)呢幌墓?
接下來我就為大家介紹一種原生模塊可以向js多次傳遞數(shù)據(jù)的方式:
向js發(fā)送事件
在原生模塊中我們可以向js發(fā)送多次事件,即使原生模塊沒有被直接的調(diào)用。為了向js傳遞事件我們需要用到RCTDeviceEventEmitter克锣,它是原生模塊和js之間的一個事件發(fā)射器茵肃。
private void sendEvent(ReactContext reactContext,String eventName, @Nullable WritableMap params) {
reactContext.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)
.emit(eventName, params);
}
在上述方法中我們可以向js模塊發(fā)送任意次數(shù)的事件,其中eventName
是我們要發(fā)送事件的事件名袭祟,params
是此次事件所攜帶的數(shù)據(jù)验残,接下來呢我們就可以在js模塊中監(jiān)聽這個事件了:
componentDidMount() {
//注冊掃描監(jiān)聽
DeviceEventEmitter.addListener('onScanningResult',this.onScanningResult);
}
onScanningResult = (e)=> {
this.setState({
scanningResult: e.result,
});
}
另外,不要忘記在組件被卸載的時候移除監(jiān)聽:
componentWillUnmount(){
DeviceEventEmitter.removeListener('onScanningResult',this.onScanningResult);//移除掃描監(jiān)聽
}
到現(xiàn)在呢巾乳,暴露接口以及數(shù)據(jù)傳遞已經(jīng)進(jìn)行完了您没,接下來呢,我們就需要注冊與導(dǎo)出React Native原生模塊了胆绊。
注冊與導(dǎo)出React Native原生模塊
為了向React Native注冊我們剛才創(chuàng)建的原生模塊氨鹏,我們需要實現(xiàn)ReactPackage
,ReactPackage
主要為注冊原生模塊所存在压状,只有已經(jīng)向React Native注冊的模塊才能在js模塊使用仆抵。
/**
* React Native Android原生模塊開發(fā)
* Author: CrazyCodeBoy
* 技術(shù)博文:http://www.devio.org
* GitHub:https://github.com/crazycodeboy
* Email:crazycodeboy@gmail.com
*/
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;
}
}
在上述代碼中,我們實現(xiàn)一個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()//在這里將我們剛才創(chuàng)建的ImageCropReactPackage添加進(jìn)來
);
}
原生模塊注冊完成之后呢,我們接下來就需要為我們的原生模塊導(dǎo)出一個js模塊娱两,以方便我們使用它莺匠。
我們創(chuàng)建一個ImageCrop.js文件,然后添加如下代碼:
import { NativeModules } from 'react-native';
export default NativeModules.ImageCrop;
這樣以來呢十兢,我們就可以在其他地方通過下面方式來使用我們所導(dǎo)出的這個模塊了:
import ImageCrop from './ImageCrop' //導(dǎo)入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
})
});
}
//...省略部分代碼
}
現(xiàn)在呢趣竣,我們這個原生模塊就開發(fā)好了,而且我們也使用了我們的這個原生模塊旱物。關(guān)于Android拍照遥缕、從相冊或文件中選擇照片,裁剪以及壓縮照片等更高級的功能實現(xiàn)异袄,大家也可以參考開源項目TakePhoto
關(guān)于線程
在React Native中通砍,JS模塊運行在一個獨立的線程中。在我們?yōu)镽eact Native開發(fā)原生模塊的時候烤蜕,如果有耗時的操作比如:文件讀寫封孙、網(wǎng)絡(luò)操作等,我們需要新開辟一個線程讽营,不然的話虎忌,這些耗時的操作會阻塞JS線程。在Android中我們可以借助AsyncTask來實現(xiàn)多線程橱鹏。另外膜蠢,如果原生模塊中需要更新UI堪藐,我們需要獲取主線程,然后在主線程中更新UI挑围,如:
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();
}
}
}
});
告訴大家一個好消息礁竞,為大家精心準(zhǔn)備的React Native視頻教程發(fā)布了,大家現(xiàn)可以看視頻學(xué)React Native了杉辙。
如果模捂,大家在開發(fā)原生模塊中遇到問題可以在本文的下方進(jìn)行留言,我看到了后會及時回復(fù)的哦蜘矢。
另外也可以關(guān)注我的新浪微博
狂男,或者關(guān)注我的Github
來獲取更多有關(guān)React Native開發(fā)的技術(shù)干貨
。
推薦學(xué)習(xí):視頻教程《最新版React Native+Redux打造高質(zhì)量上線App》