Android React Native使用原生模組
有時候我們的App需要訪問平臺API,並且React Native可能還沒有相應的模組包裝;或者你需要複用一些Java程式碼,而不是用Javascript重新實現一遍;又或者你需要實現某些高效能的、多執行緒的程式碼,譬如圖片處理、資料庫、或者各種高階擴充套件等等。
而用React Native可以在它的基礎上編寫真正原生的程式碼,並且可以訪問平臺所有的能力。如果React Native還不支援某個你需要的原生特性,你應當可以自己實現該特性的封裝。
不過在開始編寫程式碼使用原生模組前,有一個知識點需要掌握,免得又坑進去了。
在使用React Native的時候,經常會看到這麼一段程式碼
var React = require('react-native');
那麼require這個語句的作用到底是什麼呢,下面的流程提取自require() 原始碼解讀
當遇到 require(X) 時,按下面的順序處理。
(1)如果 X 是內建模組(比如 require(‘http’))
a. 返回該模組。
b. 不再繼續執行。
(2)如果 X 以 “./” 或者 “/” 或者 “../” 開頭
a. 根據 X 所在的父模組,確定 X 的絕對路徑。
b. 將 X 當成檔案,依次查詢下面檔案,只要其中有一個存在,就返回該檔案,不再繼續執行。
- X
- X.js
- X.json
- X.node
c. 將 X 當成目錄,依次查詢下面檔案,只要其中有一個存在,就返回該檔案,不再繼續執行。
- X/package.json(main欄位)
- X/index.js
- X/index.json
- X/index.node
(3)如果 X 不帶路徑
a. 根據 X 所在的父模組,確定 X 可能的安裝目錄。
b. 依次在每個目錄中,將 X 當成檔名或目錄名載入。
(4) 丟擲 “not found”
以上就是require語句的整個執行過程。那麼require(‘react-native’);請求的到底是什麼呢,其實就是node_modules\react-native\Libraries\react-native\react-native.js這個檔案
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
* @flow
*/
'use strict';
// Export React, plus some native additions.
//
// The use of Object.create/assign is to work around a Flow bug (#6560135).
// Once that is fixed, change this back to
//
// var ReactNative = {...require('React'), /* additions */}
//
var ReactNative = Object.assign(Object.create(require('React')), {
// Components
ActivityIndicatorIOS: require('ActivityIndicatorIOS'),
DatePickerIOS: require('DatePickerIOS'),
DrawerLayoutAndroid: require('DrawerLayoutAndroid'),
Image: require('Image'),
ListView: require('ListView'),
MapView: require('MapView'),
Modal: require('Modal'),
Navigator: require('Navigator'),
NavigatorIOS: require('NavigatorIOS'),
PickerIOS: require('PickerIOS'),
ProgressBarAndroid: require('ProgressBarAndroid'),
ProgressViewIOS: require('ProgressViewIOS'),
ScrollView: require('ScrollView'),
SegmentedControlIOS: require('SegmentedControlIOS'),
SliderIOS: require('SliderIOS'),
SnapshotViewIOS: require('SnapshotViewIOS'),
Switch: require('Switch'),
SwitchAndroid: require('SwitchAndroid'),
SwitchIOS: require('SwitchIOS'),
TabBarIOS: require('TabBarIOS'),
Text: require('Text'),
TextInput: require('TextInput'),
ToastAndroid: require('ToastAndroid'),
ToolbarAndroid: require('ToolbarAndroid'),
TouchableHighlight: require('TouchableHighlight'),
TouchableNativeFeedback: require('TouchableNativeFeedback'),
TouchableOpacity: require('TouchableOpacity'),
TouchableWithoutFeedback: require('TouchableWithoutFeedback'),
View: require('View'),
ViewPagerAndroid: require('ViewPagerAndroid'),
WebView: require('WebView'),
// APIs
ActionSheetIOS: require('ActionSheetIOS'),
AdSupportIOS: require('AdSupportIOS'),
AlertIOS: require('AlertIOS'),
Animated: require('Animated'),
AppRegistry: require('AppRegistry'),
AppStateIOS: require('AppStateIOS'),
AsyncStorage: require('AsyncStorage'),
BackAndroid: require('BackAndroid'),
CameraRoll: require('CameraRoll'),
Dimensions: require('Dimensions'),
Easing: require('Easing'),
ImagePickerIOS: require('ImagePickerIOS'),
InteractionManager: require('InteractionManager'),
LayoutAnimation: require('LayoutAnimation'),
LinkingIOS: require('LinkingIOS'),
NetInfo: require('NetInfo'),
PanResponder: require('PanResponder'),
PixelRatio: require('PixelRatio'),
PushNotificationIOS: require('PushNotificationIOS'),
Settings: require('Settings'),
StatusBarIOS: require('StatusBarIOS'),
StyleSheet: require('StyleSheet'),
VibrationIOS: require('VibrationIOS'),
// Plugins
DeviceEventEmitter: require('RCTDeviceEventEmitter'),
NativeAppEventEmitter: require('RCTNativeAppEventEmitter'),
NativeModules: require('NativeModules'),
Platform: require('Platform'),
processColor: require('processColor'),
requireNativeComponent: require('requireNativeComponent'),
// Prop Types
EdgeInsetsPropType: require('EdgeInsetsPropType'),
PointPropType: require('PointPropType'),
// See http://facebook.github.io/react/docs/addons.html
addons: {
LinkedStateMixin: require('LinkedStateMixin'),
Perf: undefined,
PureRenderMixin: require('ReactComponentWithPureRenderMixin'),
TestModule: require('NativeModules').TestModule,
TestUtils: undefined,
batchedUpdates: require('ReactUpdates').batchedUpdates,
cloneWithProps: require('cloneWithProps'),
createFragment: require('ReactFragment').create,
update: require('update'),
},
});
if (__DEV__) {
ReactNative.addons.Perf = require('ReactDefaultPerf');
ReactNative.addons.TestUtils = require('ReactTestUtils');
}
module.exports = ReactNative;
瞭解了這個知識點後,我們來自定義一個模組,去使用原生的模組。假設有這麼一個需求,我們需要使用Andorid中的Log類,但是React Native並沒有為我們進行封裝,那麼我們自己動手實現一下吧。
- 我們需要繼承ReactContextBaseJavaModule這個抽象類,重寫getName()函式,用於返回一個字串,這個字串在JavaScript端標記這個模組,暴露一個函式給javascript端,並且使用註解@ReactMethod進行標記,該函式的返回值必須為void,React Native的跨語言訪問是非同步進行的,所以想要給JavaScript返回一個值的唯一辦法是使用回撥函式或者傳送事件。
- 我們需要實現一個類實現ReactPackage介面,該介面中有三個抽象函式待實現,分別是createNativeModules,createJSModules,createViewManagers,這三個函式中,我們需要實現的最關鍵的函式就是createNativeModules,在該函式中我們需要新增前一步建立的ReactContextBaseJavaModule子類
- 構建ReactInstanceManager的例項時,通過呼叫 addPackage()函式,將上一步實現的ReactPackage新增進去。
接下來我們來實現程式碼。為了簡單方便,這裡只演示Log類中的d方法,即Log.d(String tag,String msg)
第一步,繼承ReactContextBaseJavaModule類,重寫getName()方法,因為是Log模組,所以直接返回字串Log,暴露一個d方法給javascript端,返回值為void,只用註解進行標記。最終的程式碼如下。
public class LogModule extends ReactContextBaseJavaModule{
private static final String MODULE_NAME="Log";
public LogModule(ReactApplicationContext reactContext) {
super(reactContext);
}
@Override
public String getName() {
return MODULE_NAME;
}
@ReactMethod
public void d(String tag,String msg){
Log.d(tag,msg);
}
}
第二步,實現ReactPackage介面,在createNativeModules函式中新增我們的日誌模組。其餘兩個函式返回空的List即可。
public class AppReactPackage implements ReactPackage {
@Override
public List<NativeModule> createNativeModules(ReactApplicationContext reactContext) {
List<NativeModule> modules=new ArrayList<>();
modules.add(new LogModule(reactContext));
return modules;
}
@Override
public List<Class<? extends JavaScriptModule>> createJSModules() {
return Collections.emptyList();
}
@Override
public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) {
return Collections.emptyList();
}
}
第三步,新增AppReactPackage 到ReactInstanceManager的例項中去,在我們的MainActivity中可以看到這麼一段程式碼
mReactInstanceManager = ReactInstanceManager.builder()
.setApplication(getApplication())
.setBundleAssetName("index.android.bundle")
.setJSMainModuleName("index.android")
.addPackage(new MainReactPackage())
.setUseDeveloperSupport(BuildConfig.DEBUG)
.setInitialLifecycleState(LifecycleState.RESUMED)
.build();
我們再build函式之前呼叫addPackage進行新增即可,最終程式碼如下。
mReactInstanceManager = ReactInstanceManager.builder()
.setApplication(getApplication())
.setBundleAssetName("index.android.bundle")
.setJSMainModuleName("index.android")
.addPackage(new MainReactPackage())
.addPackage(new AppReactPackage())
.setUseDeveloperSupport(BuildConfig.DEBUG)
.setInitialLifecycleState(LifecycleState.RESUMED)
.build();
可以看到我們增加了一行.addPackage(new AppReactPackage())
這樣,在Java端我們要做的就做完了,接下來就是javascript端了,這時候編譯一下apk重新後執行,接下來我們來編寫javascript端。
如果你不嫌麻煩,每次都要從NativeModules來訪問我們的Log,那麼現在你可以直接在javascript中進行訪問了。就像這樣子。
var React = require('react-native');
var {
NativeModules,
} = React;
var Log1= NativeModules.Log;
Log1.d("Log1","LOG");
但是,假如我再增加一個需求,就是當Log類在java層打印出一個日誌的之後,希望在js端也輸出以下這個日誌,那麼你會怎麼做呢,或許你會說,這個簡單,我再輸出一下js的日誌就ok了。就像這樣子。
var React = require('react-native');
var {
NativeModules,
} = React;
var Log1= NativeModules.Log;
Log1.d("Log1","LOG");
console("Log1","LOG");
沒錯是沒錯,就是看著蛋疼,不好維護不說,通樣的程式碼你得寫多少遍。
這時候,我們就有必要封裝一下javascript端的程式碼了,在index.android.js檔案同目錄下新建一個log.js,輸入如下程式碼。
'use strict';
var { NativeModules } = require('react-native');
var RCTLog= NativeModules.Log;
var Log = {
d: function (
tag: string,
msg: string
): void {
console.log(tag,msg);
RCTLog.d(tag, msg);
},
};
module.exports = Log;
程式碼很簡單,我們通過NativeModules拿到我們的Log模組在本地的實現,賦值給變數RCTLog,並且還聲明瞭一個Log變數,裡面有一個函式d,呼叫了RCTLog的d函式,並且在呼叫前輸出了javascript端的日誌。最後使用module.exports=Log將Log變數匯出
上面我們規定了引數型別為string,java與javascript之間引數型別的對應關係如下
Boolean -> Bool
Integer -> Number
Double -> Number
Float -> Number
String -> String
Callback -> function
ReadableMap -> Object
ReadableArray -> Array
接下來就是引用log.js檔案了,看過上面的require語句的解析,這對你應該不成問題了。
var Log=require('./log');
Log.d("TAG","111");
這還沒完,我們再提一個需求,就是我們希望這個Log模組能夠提供一個常量,也就是TAG供我們使用,而這個常量定義在java層,以便以後我們使用的時候如果不想輸入TAG,可以直接使用這個預設的TAG,就像這樣子
var Log=require('./log');
Log.d(Log.TAG,"111");
那麼這個要怎麼實現呢,很顯然,我們需要在log.js中加入這個變數,就像這樣子
'use strict';
var { NativeModules } = require('react-native');
var RCTLog= NativeModules.Log;
var Log = {
TAG: RCTLog.TAG,
d: function (
tag: string,
msg: string
): void {
console.log(tag,msg);
RCTLog.d(tag, msg);
},
};
module.exports = Log;
這樣雖然我們可以使用Log.TAG返回到這個值了,由於我們java層沒有定義TAG,所以這時候會報錯。因此我們需要在java層返回這個值,這又要怎麼做呢,別急。我們重新回過頭來看看我們實現的類LogModule,我們繼續在該類中定義兩個常量
private static final String TAG_KEY = "TAG";
private static final String TAG_VALUE = "LogModule";
什麼用呢,看常量名字就是到了,key和value,鍵值對,我們希望通過TAG_KEY拿到TAG_VALUE ,也就是我們日誌要用到的TAG,怎麼實現呢。重寫getConstants函式即可。
@Override
public Map<String, Object> getConstants() {
final Map<String, Object> constants = MapBuilder.newHashMap();
constants.put(TAG_KEY, TAG_VALUE);
return constants;
}
這時候重寫編譯執行一下,你就可以在javascript層通過Log.TAG就可以訪問到對應的值了,值為LogModule,而為什麼是Log.TAG而不是其他的值呢,因為我們constants中put進去的鍵就是TAG。
那麼這個有什麼作用呢,還記得android中我們的Toast的使用,顯示時間有兩個值嗎,一個是Toast.LENGTH_SHORT,另一個是Toast.LENGTH_LONG,我們希望在javascript層通用有這麼兩個常量可以使用,那麼就可以使用這種方法。我們可以看看系統的ToastAndroid的實現。
首先看java層
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);
}
@Override
public String getName() {
return "ToastAndroid";
}
@Override
public Map<String, Object> getConstants() {
final Map<String, Object> constants = MapBuilder.newHashMap();
constants.put(DURATION_SHORT_KEY, Toast.LENGTH_SHORT);
constants.put(DURATION_LONG_KEY, Toast.LENGTH_LONG);
return constants;
}
@ReactMethod
public void show(String message, int duration) {
Toast.makeText(getReactApplicationContext(), message, duration).show();
}
}
看到沒有,在getConstants函式中暴露了兩個值給javascript層,SHORT對應java層的Toast.LENGTH_SHORT,LONG對應java層的Toast.LENGTH_LONG。接著看javascript層的程式碼
'use strict';
var RCTToastAndroid = require('NativeModules').ToastAndroid;
var ToastAndroid = {
SHORT: RCTToastAndroid.SHORT,
LONG: RCTToastAndroid.LONG,
show: function (
message: string,
duration: number
): void {
RCTToastAndroid.show(message+"lizhangqu", duration);
},
};
module.exports = ToastAndroid;
直接可以通過定義的變數SHORT或者LONG訪問到,最終我們的使用就是這樣子的。
var React = require('react-native');
var {
ToastAndroid
} = React;
ToastAndroid.show("toast",ToastAndroid.SHORT);
這還沒完,這僅僅是沒有返回值的情況,假如有返回值情況又是怎麼樣呢,比如javascript呼叫java層的方法,但是java層需要將結果返回javascript,沒錯,答案就是回撥!,最典型的一個場景就是javascript層呼叫java層的網路請求方法,java層拿到網路資料後需要將結果返回給javascript層。通用的,我們用最快的速度實現一下這個模組。
繼承ReactContextBaseJavaModule,實現getName方法,返回值為Net,暴露一個getResult方法給javascript,並進行註解,注意這個函式有一個Callback型別的入參,返回結果就是通過這個進行回撥
public class NetModule extends ReactContextBaseJavaModule {
private static final String MODULE_NAME="Net";
public NetModule(ReactApplicationContext reactContext) {
super(reactContext);
}
@Override
public String getName() {
return MODULE_NAME;
}
@ReactMethod
public void getResult(String url,final Callback callback){
Log.e("TAG","正在請求資料");
new Thread(new Runnable() {
@Override
public void run() {
try {
String result="這是結果";
Thread.sleep(1000);//模擬網路請求
callback.invoke(true,result);
} catch (Exception e) {
e.printStackTrace();
}
}
}).start();
}
}
Callback的定義如下,它是一個介面,invoke函式的入參是個數是任意的。
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);
}
在前面的AppReactPackage類createNativeModules函式中註冊該模組
modules.add(new NetModule(reactContext));
之後新建一個net.js檔案,實現javascript層
'use strict';
var { NativeModules } = require('react-native');
var RCTNet= NativeModules.Net;
var Net = {
getResult: function (
url: string,
callback:Function,
): void {
RCTNet.getResult(url,callback);
},
};
module.exports = Net;
進行使用
var Net=require('./net');
Net.getResult(
"http://baidu.com",
(code,result)=>{
console.log("callback",code,result);
}
);
如果不出意外,在java層將輸出日誌
11-20 22:30:53.598 25323-1478/com.awesomeproject E/TAG: 正在請求資料
在javascript層,控制檯將輸出
callback true 這是結果
以上就是回撥的一個示例,你可以簡單想象成java層的網路請求模型,主執行緒開啟子執行緒請求資料,子執行緒拿到資料後回撥對應的方法使用handler通知主執行緒返回結果。
除了回撥之外,還可以使用事件呼叫javascript層的方法,我們以第一個LogModule為例,假設呼叫了java層方法後,我們希望傳送一個事件給javascript,在javascript層再次進行列印輸出。則d方法修改下面的程式碼
@ReactMethod
public void d(String tag,String msg){
Log.d(tag, msg);
//傳送事件給javascript層
WritableMap params = Arguments.createMap();
params.putString("TAG",tag);
params.putString("MSG",msg);
getReactApplicationContext()
.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)
.emit("logInConsole", params);//對應的javascript層的事件名為logInConsole,註冊該事件即可進行回撥
}
而對應的log.js並不需要做什麼修改,我們只需要在想獲得該事件的地方註冊事件即可,比如我們想在主介面接收該事件,則在index.android.js中進行註冊,註冊的方式有兩種。
第一種如下
'use strict';
var React = require('react-native');
var {
AppRegistry,
StyleSheet,
View,
DeviceEventEmitter,
} = React;
//注意下面兩個模組的引入
var Log=require('./log');
var Subscribable = require('Subscribable');
var AwesomeProject = React.createClass({
mixins: [Subscribable.Mixin],//這句必須要加
render: function() {
return (
<View style={styles.container}>
</View>
);
},
componentDidMount:function(){
Log.d("tag","tag");//呼叫java層log方法,之後就會回撥對應的事件(前提是註冊了事件)
//註冊事件
this.addListenerOn(DeviceEventEmitter,
'logInConsole',
this.logInConsole);
},
logInConsole:function(event){
console.log(event);
},
});
var styles = StyleSheet.create({
container:{
},
});
AppRegistry.registerComponent('AwesomeProject', () => AwesomeProject);
控制檯輸出效果如下
這種方式顯得程式碼有點多,還有另外一種方式,程式碼如下
'use strict';
var React = require('react-native');
var {
AppRegistry,
StyleSheet,
View,
DeviceEventEmitter,
} = React;
//只需要引入Log
var Log=require('./log');
var AwesomeProject = React.createClass({
render: function() {
return (
<View style={styles.container}>
</View>
);
},
componentDidMount:function(){
Log.d("tag","tag");//呼叫java層log方法,之後就會回撥對應的事件(前提是註冊了事件)
//直接使用DeviceEventEmitter進行事件註冊
DeviceEventEmitter.addListener('logInConsole',(e)=>{
console.log(e);
});
},
});
var styles = StyleSheet.create({
container:{
},
});
AppRegistry.registerComponent('AwesomeProject', () => AwesomeProject);
效果是一樣的。都會在控制檯輸出。
基本上,掌握了上面的內容,對原始模組的使用也差不多了,本篇文章是基於官方文件的最佳實踐Native Modules,但是該文件坑太多,還需謹慎參考。