jeffrey1995/MyBlog

React Native-原生模块开发

jeffrey1995 opened this issue · 0 comments

前言:

所谓原生模块开发,很重要的就是Android/iOS原生代码和ReactNative代码的交互,以及在ReactNative代码中使用原生的组件。下面将从这两个地方开始讲解(示例以Android平台为基础):

先回顾一下RN组件的生命周期:

constructor():
在组件被加载前最先调用,第一个语句必须是super(props)。

componentWillMount():
在初始渲染(render函数被RN框架执行之前)前被执行。在函数中调用setState函数改变了某些状态机变量的值,RN框架不会立马执行渲染操作,而是等该函数执行完之后执行初始渲染。

componentDidMount():
执行完初始渲染之后立马被调用。将从网络侧请求数据的代码放在这里比较合适。

componentWillReceiveProps(nextProps):
RN初始化渲染执行完成后,当RN组件收到新的props时,这个函数被调用。函数接受的参数是一个object为新的props。

shouldComponentUpdate(nextProps, nextState):
RN初始化渲染执行完成后,当RN组件接收到新的state或者props时,该函数被调用。接受两个参数:第一个是新的props,第二个是新的state。该函数返回一个布尔类型的值,表示RN框架针对此次改变是否重新渲染该组件。

componentWillUpdate(nextProps, nextState):
RN组件在初始化渲染执行完成之后,RN框架在重新渲染RN组件前会调用这个函数。

componentDidUpdate(prevProps, prevState):
RN组件在初始化渲染执行完成之后,RN框架在重新渲染RN组件完成后会调用这个函数,传入的两个参数是渲染前的Props和state。

componentWillUnmount():
在RN组件被卸载前,这个函数被执行。

一、原生代码和RN代码的交互:

1 RN侧主动调用原生模块方法:回调函数和Promise机制

首先在电脑中部署Android开发和RN开发所需要的环境:
打开Android端的初始化项目(名为MyRNProject):
工程中只有一个Activity,且继承于ReactActivity,将该Activity设置为应用的入口,getMainComponentName()返回服务器上对应的组件名称

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()函数返回加载的第一个js文件名称。将需要注册的ReactPackage添加到这里protected List getPackages(){},new MyRNPackage()为我们自己定义的ReactPackage,该类用来创建工程中的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));   //加入开发接口
        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举例:我创建了一个ToastModule继承于ReactContextBaseJavaModule,getName() 函数返回模块名称,在RN中通过该名称找到该模块,在上面的MyRNPackage中我们已将其加入管理器中。
内部代码如下:

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秒后回调CallBack");
    }
}

1.1 回调函数:
RN调用原生方法,然后在原生侧调用RN侧提供的回调函数,实现交互。
以ToastModule为例,加上@ReactMethod注解的方法为RN模块可以直接调用方法:

@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());
        }
    }

该方法有三个参数,分别是信息字符串、显示时长以及RN端传入的回调,getConstants()中约定了对应的变量方便找RN端使用。

在RN端的代码如下:首先将ToastModule导入,在showToast函数中直接调用ToastModule.showToast(),将需要参数包含回调函数传入。回调成功将改变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(回调函数)</Text>
                <TouchableOpacity onPress={(message) => this.showToast("hello world!")}>
                    <Text style={styles.text_item}>
                        {"回调结果:" + this.state.interfaceResult}
                    </Text>
                </TouchableOpacity>
...
showToast(message) {
        console.log("showToast!");
        if (Platform.OS === "android") {
            ToastModule.showToast(message,
                ToastModule.SHORT,
                (msg) => {
                    this.setState({
                        interfaceResult: msg
                    })
                });
        }
    }

下面是运行截图:
image
image

原生代码调用了回调函数,回调函数并不会马上执行,因为是混合开发的桥接方式本来就是异步的)
注意事项:需要注意的是,回调函数必须要RN侧提供的情况下才能调用,调用Callback.invoke()即可执行回调方法,也可以将Callback引用暂时保存起来,不立即调用,但是该回调方法只能调用一次,多于一次将会如下错误:
image

1.2 Promise机制
建议使用,因为Promise在RN中使用非常广泛,能力也非常强大。
下面我在RN模块中,调用原生代码读取手机通讯录并将其显示在界面上:

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);
    }
}

重点关注一下 public void handleMessage(String aMessage, Promise aPromise)函数,它有两个参数,分别是一个消息字符串和一个Promise引用,在该方法被调用时我们将Promise引用保存下来为interfacePromise,ActivityEventListener负责监听Activity的生命周期,我们已将其注册,在通讯录页面返回的时候,onActivityResult将被调用,我们将用户点击的通讯录好友信息获取到保存为Json格式,使用interfacePromise.resolve(toRNMessage)将其返回到RN端。

RN端代码如下:在then函数中处理成功回调,在catch处理异常回调。

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机制)</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);
            }
        );
    }

运行图如下:
image
image
image

Promise对比回调机制不需要设置和实现事件的监听函数,通过Promise机制让异步处理便于书写、阅读与理解。

2 原生代码主动调用RN代码:通过SendEvent——发送消息的方式。

首先,我定义了一个EventModule用来主动发送消息:

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);
    }
}

重点关注sendEvent方法,通过reactContext.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class).emit(eventName, params),其中eventName代表消息名,params代表参数。
为了方便测试,我写了一个testSendEvent方法,由RN界面来触发sendEvent方法。

RN中代码如下:在componentDidMount进行了监听事件的注册。

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}>原生调RN(sendEvent)</Text>
                <TouchableOpacity onPress={() => this.testSendEvent()}>
                    <Text style={styles.text_item}>
                        {"消息结果:" + this.state.sendEventResult}
                    </Text>
                </TouchableOpacity>
...
 testSendEvent() {
        console.log("testSendEvent!");
        EventModule.testSendEvent();
    }
...
 setReceivedResult() {
        console.log("set received!");
        this.setState({
            sendEventResult: 'received sendevent'
        });
    }

运行截图如下:
image
image