1. 程式人生 > >Android React Native使用原生模組

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介面,該介面中有三個抽象函式待實現,分別是createNativeModulescreateJSModulescreateViewManagers,這三個函式中,我們需要實現的最關鍵的函式就是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,但是該文件坑太多,還需謹慎參考。