React Native 作为一个混合开发解决方案,因为业务、性能上的种种原因,总是避免不了与原生进行交互。在开发过程中我们将 RN 与原生交互的几种方式进行了梳理,按照途径主要分为以下几类:

  • 通过原生 Module 进行交互
  • 通过原生 View 进行交互
  • 通过发送事件 Event 进行交互

一、通过原生 Module 进行交互

通过原生 Module 进行交互是最高频的使用方式。封装原生 Module 可以将定义好的原生方法交给 RN 在 JS 端进行调用,JS 端可以在调用方法时通过传参的方式直接将数据传输给原生端,而原生端可以在方法执行过后将需要返回的数据通过 Promise 或者 Callback 将数据返回给 JS 端。

1.1 封装原生 Module

封装原生 Module 的步骤包括以下几步:

  • 创建自定义 Module
  • 创建自定义 Package 注册自定义 Module
  • 注册自定义 Package
  • 在RN中使用自定义 Module
1.1.1 创建自定义 Module

创建自定义 Module 其实就是将 RN 希望调用的原生功能封装成中间件的形式,这个中间件需要继承 ReactContextBaseJavaModule 类,并重写它的 getName() 方法。getName() 方法返回了 JS 可以访问的自定义 Module 名称,使得我们在 JS 端可以通过 NativeModules.自定义 Module 名称 的形式访问这个中间件。

public class MyModule extends ReactContextBaseJavaModule {
    
    public MyModule(@Nonnull ReactApplicationContext reactContext) {
        super(reactContext);
    }

    @Nonnull
    @Override
    public String getName() {
        return "MyNativeModule"; // 暴露给RN的模块名,在JS端通过 NativeModules.MyNativeModule 即可访问到本模块
    }
}
复制代码
1.1.2 创建自定义 Package 注册自定义 Module

自定义 Package 实现了 ReactPackage 接口,该接口提供了2个方法来分别注册自定义 Module 和自定义 ViewManager,其中自定义 ViewManager 用于封装原生 View 与 RN 进行交互,具体内容可以查看后面的“通过原生 View 进行交互”。我们需要在 createNativeModules 方法中返回一个包含我们新建的自定义 Module 实例的 List,完成注册。

public class MyReactPackage implements ReactPackage {
    @Nonnull
    @Override
    public List<NativeModule> createNativeModules(@Nonnull ReactApplicationContext reactContext) {
        List<NativeModule> modules = new ArrayList<>();
        modules.add(new MyModule(reactContext)); // 将新建的 MyModule 实例加入到 List 中完成注册
        return modules;
    }

    @Nonnull
    @Override
    public List<ViewManager> createViewManagers(@Nonnull ReactApplicationContext reactContext) {
        return Collections.emptyList();
    }
}
复制代码
1.1.3 注册自定义 Package

仅仅完成了自定义 Module 在自定义 Package 的注册还不能让 RN 使用我们的 Module,我们需要将刚刚新建的 ReactPackage 实例注册到 ReactApplication 的 ReactNativeHost 实例中。在默认的 RN 工程下,ReactApplication 通常为 MainApplication,你也可以让自己原有安卓项目中的 Application 类实现 ReactApplication 接口。

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 MyReactPackage() // 将新建的 MyReactPackage 实例注册到 ReactPackage 列表中
            );
        }

        @Override
        protected String getJSMainModuleName() {
            return "index";
        }
    };

    @Override
    public ReactNativeHost getReactNativeHost() {
        return mReactNativeHost;
    }

    @Override
    public void onCreate() {
        super.onCreate();
        SoLoader.init(this, /* native exopackage */ false);
    }
}
复制代码
1.1.4 在 RN 中使用自定义 Module

完成原生端的注册后,RN 即可使用我们封装的自定义 Module 了。在 JS 端引用的代码如下:

import { NativeModules } from "react-native";

let customModule = NativeModules.MyNativeModule; // 此处引用的自定义 Module 名必须与自定义 Module 中 getName() 方法返回的字符串一致
复制代码

接下来我们可以通过这个自定义 Module 实现 RN 与原生的几种通信方式。

1.2 JS 端获取原生端自定义 Module 预设的常量值

在自定义 Module 中,我们可以通过重写 getConstants() 方法,返回一个 Map。这个 Map 的 key 为 RN 中可以被访问的常量名称,value 为预设的常量值。

private static final String CUSTOM_CONST_KEY = "TEXT";

@Nullable
@Override
// 获取模块预定义的常量值
public Map<String, Object> getConstants() {
    final Map<String, Object> constants = new HashMap<>();
    constants.put(CUSTOM_CONST_KEY, "这是模块预设常量值");
    return constants;
}
复制代码

在 RN 中使用 Text 组件调用这个常量显示内容:

<Text>{ customModule.TEXT }</Text>
复制代码

1.3 原生端通过 @ReactMethod 暴露方法给 JS 端,接受 JS 端调用方法时传入的参数数据

在原生端,如果想将方法暴露给 RN 调用,可以在方法前加上 @ReactMethod 注解,但要注意,这个方法的访问权限和返回类型必须要设置为 public void 才可以。

@ReactMethod
public void myFunction(String parmas) {
    // To Do Something
    // 字符串 params 即为 RN 传入的参数
}
复制代码

在 JS 端调用这个函数,并传递参数:

customModule.myFunction("这里是参数 params 的内容");
复制代码

注:传入的参数并不局限于例子中的 String 类型,且参数数量可以是多个。而 JS 与 Java 的类型对应如下:

JavaScript

Java

Bool

Boolean

Number

Integer

Number

Double

Number

Float

String

String

Function

Callback

Object

ReadableMap

Array

ReadableArray

1.4 原生端通过 Callback 回调函数返回数据给 JS 端

通过上面的类型对应,我们了解到, JS 中的 function 作为参数传输到 Java 中就是个 Callback。所以我们可以使用回调函数,在完成原生方法的执行过后,将需要的结果或状态通过 Callback.invoke() 方法回调给 JS。

@ReactMethod
public void myFunction(String params, Callback success, Callback failture) {

    try {
        if (params != null && !params.equals("")){
            // 回调成功,返回结果信息
            success.invoke("这是从原生", "返回的字符串");
        }
    }catch (IllegalViewOperationException e) {
        // 回调失败,返回错误信息
        failture.invoke(e.getMessage());
    }
}
复制代码

在 JS 中需要定义回调函数的执行内容,这里定义了一个匿名函数作为回调函数。你可以根据自己的业务需求替换为相应的回调函数。

customModule.myFunction(
    "这是带Callback回调的函数方法",
     (parma1, parma2) => {
        var result = parma1 + parma2;
        console.log(result); // 显示: 这是从原生返回的字符串
    },
    errMsg => {
        console.log(errMsg);
    }
);
复制代码

1.5 原生端通过 Promise 函数返回数据给 JS 端

除了使用 Callback 进行回调,我们还可以在 ReactMethod 中将 Promise 作为最后一个参数,使用 Promise 实例,完成数据的回传。Promise 具有 resolve() 和 reject() 两个方法,可以用于处理正常和异常的回传。在使用时,我们通常在自定义 Module 中定义一个 Promise 类型的私有变量,在调用 ReactMethod 时,对这个私有变量进行赋值,然后在需要回传数据的地方使用这个私有变量进行回传,这样可以更灵活的控制回传时机。但要注意的是,Promise 实例只能被 resolve() 或 reject() 一次,若多次回调将会报错。

private Promise mPromise;
private Handler handler = new Handler(){
        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
            mPromise.resolve((String) msg.obj);
        }
    };

@ReactMethod
public void myFunction(String params, Promise promise) {
    mPromise = promise;
    try {
        if (params != null && !params.equals("")){
            // 回调成功,返回结果信息
            Message message = handler.obtainMessage();
            message.obj = params;
            handler.sendMessage(message);
        }
    }catch (IllegalViewOperationException e) {
        // 回调失败,返回错误信息
        mPromise.reject('error', e.getMessage());
    }
}
复制代码

在 JS 端的调用情况

customModule.myFunction("这是使用 Promise 回调的函数方法")
            .then(result => {
                console.log(result); // 显示: 这是从原生返回的字符串
            })
复制代码

二、通过原生 View 进行交互

通过自定义 Module 进行交互已经可以解决我们的大部分开发需求,然而有的时候,基于性能和开发工作量的角度考虑,我们可以将原生的组件或布局封装好,并为这个原生 View 建立一个继承自 SimpleViewManager 或 ViewGroupManager 的 ViewManager 类。通过这个 ViewManager 可以注册一系列原生端和 JS 端的参数及事件映射,达到交互的目的。

2.1 封装原生 View

封装原生 View 包括以下几步:

  • 创建原生 View 类
  • 创建 ViewManager
  • 将 ViewManager 在自定义 Package 中注册
  • 在 Js 端进行调用
2.1.1 创建原生 View 类

这里以封装一个简单的原生 Button 的子类为例:

public class MyButton extends Button {
    public MyButton(Context context) {
        super(context);
    }
}
复制代码
2.1.2 创建相应的 ViewManager 类

简单的 View 可以创建 ViewManager 类继承 SimpleViewManager ,而通过布局生成的复杂 View 可以继承自 ViewGroupManager 类,这里我们继承 SimpleViewManager:

public class MyButtonViewManager extends SimpleViewManager<MyButton> {
    @Override
    public String getName() {
        return "NativeMyButton"; // 此名称用于在 JS 中引用
    }

    // 创建 View 实例
    @Override
    protected MyButton createViewInstance(ThemedReactContext reactContext) {
        return new MyButton(reactContext);
    }
}
复制代码
2.1.3 将 ViewManager 在自定义 Package 中注册

之前我们创建了自定义 Package 类 MyReactPackage,我们只是使用了 createNativeModules() 方法完成了自定义 Module 的注册,接下来我们需要在 createViewManagers() 方法中注册刚刚创建的 MyButtonViewManager 实例:

public class MyReactPackage implements ReactPackage {
    @Nonnull
    @Override
    public List<NativeModule> createNativeModules(@Nonnull ReactApplicationContext reactContext) {
        List<NativeModule> modules = new ArrayList<>();
        modules.add(new MyModule(reactContext));
        return modules;
    }

    @Nonnull
    @Override
    public List<ViewManager> createViewManagers(@Nonnull ReactApplicationContext reactContext) {
        List<ViewManager> views = new ArrayList<>();
        views.add(new MyButtonViewManager()); // 创建 MyButtonViewManager 实例并注册到 ViewManager List 中
        return views;
    }
}
复制代码

注意,如果你没有完成第一章中 Package 在 Application 的注册步骤,这里也需要将 MyReactPackage 注册到 Application 中。

2.1.4 在 JS 端完成调用

完成了上面的原生代码,我们就可以在 JS 端完成调用了:

import { requireNativeComponent, View} from 'react-native';

let MyButton = requireNativeComponent('NativeMyButton');

...
render() {
    return (
      <MyButton/>
    );
  }
...
复制代码

2.2 原生端通过 @ReactProps 将方法暴露给 JS 端,接收 JS 端为组件设定的属性

在刚刚建立的 ViewManager 类中,我们可以通过 @ReactProps 注解方法,为组件添加属性,这里我们为 MyButton 添加 text 属性:

public class MyButtonViewManager extends SimpleViewManager<MyButton> {
    @Override
    public String getName() {
        return "NativeMyButton";
    }

    @Override
    protected MyButton createViewInstance(ThemedReactContext reactContext) {
        return MyButton(reactContext);
    }

    // 暴露给 JS 的参数,用于设定名称为“text”的属性,设定 Button 的文字
    @ReactProp(name = "text")
    public void setSrc(MyButton view,  String text) {
        view.setText(text);
    }
}
复制代码

此时可以在 JS 端为组件添加 text 属性和它的值,完成设定 MyButton 的文字:

import { requireNativeComponent, View} from 'react-native';

let MyButton = requireNativeComponent('NativeMyButton');

...
render() {
    return (
      <MyButton text='这是个按钮'/>
    );
  }
复制代码

2.3 原生端通过注册 View 事件与 JS 端的映射,使 JS 端可以接收原生端发送的事件和数据

有些时候我们不仅仅需要将数据以属性值的形式,从 JS 端传输到原生端,还需要原生端对 JS 端发送数据完成交互,这时我们需要使用事件 Event,通过发送事件完成交互。这里我们可以将 MyButton 的点击事件通知给 JS 端:

public class MyButtonViewManager extends SimpleViewManager<MyButton> {
    @Override
    public String getName() {
        return "NativeMyButton";
    }

    @Override
    protected MyButton createViewInstance(ThemedReactContext reactContext) {
        MyButton button = new MyButton(reactContext);
        button.setOnClickListener(v -> {
            WritableMap event = Arguments.createMap(); // 这里传了个空的 event 对象,使用时可以在 event 中加入要传输的数据
            reactContext.getJSModule(RCTEventEmitter.class).receiveEvent(
                viewId,
                "onNativeClick", // 与下面注册的要发送的事件名称必须相同
                event);
        });
        return button;
    }

    @ReactProp(name = "text")
    public void setSrc(MyButton view,  String text) {
        view.setText(text);
    }

    @Nullable
    @Override
    public Map getExportedCustomDirectEventTypeConstants() {
        return MapBuilder.of(
            "onNativeClick", MapBuilder.of("registrationName", "onReactClick"));
            // onNativeClick 是原生要发送的 event 名称,onReactClick 是 JS 端组件中注册的属性方法名称,中间的 registrationName 不可更改
    }
}
复制代码

然后在 JS 端就可以使用 onReactClick 这个属性来响应点击事件:

import { requireNativeComponent, View} from 'react-native';

let MyButton = requireNativeComponent('NativeMyButton');

...
render() {
    return (
      <MyButton text='这是个按钮' onReactClick={data=>{
          // 这里接收 event 传过来的数据
          console.log(data);
      }}/>
    );
  }
复制代码

2.4 原生端通过注册 View 命令表和响应方法,完成接收来自 JS 端的指令

JS 端不仅仅只能从设定属性值的方法来将数据传输给原生端,也可以使用 UIManager.dispatchViewManagerCommand 方法来发送命令并携带数据给原生端,这里我们添加了一条命令 changeText 让 MyButton 点击后更换按钮上的文字:

import { requireNativeComponent, View} from 'react-native';

let MyButton = requireNativeComponent('NativeMyButton');

...
changeButtonText = () => {
    UIManager.dispatchViewManagerCommand(
      findNodeHandle(this.nativeUI),
      UIManager.TemplateMenuView.Commands.changeText, //Commands.changeText需要与native层定义的命令名称一致
      ['这是新的按钮'] //命令携带的数据
    );
}

render() {
    return (
      <MyButton 
      ref={view => this.nativeUI = view} 
      text='这是个按钮' 
      onReactClick={data=>{
          this.changeButtonText; // 点击时回传给原生端命令
      }}/>
    );
  }
复制代码

在原生端,我们需要在 ViewManager 中重写 getCommandsMap() 方法建立命令的映射表,然后重写 receiveCommand() 方法完成接收命令后的操作

public class MyButtonViewManager extends SimpleViewManager<MyButton> {
    @Override
    public String getName() {
        return "NativeMyButton";
    }

    @Override
    protected MyButton createViewInstance(ThemedReactContext reactContext) {
        MyButton button = new MyButton(reactContext);
        button.setOnClickListener(v -> {
            WritableMap event = Arguments.createMap();
            reactContext.getJSModule(RCTEventEmitter.class).receiveEvent(
                viewId,
                "onNativeClick",
                event);
        });
        return button;
    }

    @ReactProp(name = "text")
    public void setSrc(MyButton view,  String text) {
        view.setText(text);
    }

    @Nullable
    @Override
    public Map getExportedCustomDirectEventTypeConstants() {
        return MapBuilder.of(
            "onNativeClick", MapBuilder.of("registrationName", "onReactClick"));
    }

    @Override
    public Map<String, Integer> getCommandsMap() {
        return MapBuilder.of(
            “changeText”, 0 // changeText 是命令名称,0 是命令 id
        );
    }

    @Override
    public void receiveCommand(MyButton view, int commandId, @Nullable ReadableArray args) {
        switch (commandId){
            case 0: // 当命令 id 为 0 时
                String newText = args.getString(0); // 我们在 JS 只传了一个字符串过来,在这里接收
                view.setText(newText); // 为按钮设定新的文字
                break;
            default:
                break;
        }
    }
}
复制代码

三、通过发送事件 Event 进行交互

利用原生View进行交互的时候,我们已经利用了发送事件的机制,完成原生端与 JS 端的通信,但前提是需要将原生 View 的属性方法与原生事件先注册映射,才能获取这个事件。其实事件 Event 是可以通过 RCTDeviceEventEmitter 灵活完成发送的,原生端将事件名称和事件数据发送后,JS 端需要根据事件名称预先注册一个监听器,来响应这个接收的事件,这也是最为灵活的一种交互方式。

3.1 在原生端通过 RCTDeviceEventEmitter 发送事件

//定义向 RN 发送事件的函数
public void sendEvent(ReactContext reactContext, String eventName, @Nullable WritableMap params) {
    reactContext
        .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)
        .emit(eventName,params);
}

// 发送成功消息
public notifySuccessMessage(ReactContext reactContext, String msg) {
    WritableMap event = Arguments.createMap();
    event.putString("message", msg);
    sendEvent(reactApplicationContext, "SUCCESS", event);
}

// 发送失败消息
public notifyErrorMessage(ReactContext reactContext) {
    WritableMap event = Arguments.createMap();
    sendEvent(reactApplicationContext, "ERROR", event);
}
复制代码

3.2 在 JS 端注册监听器,并在合适的时机移除监听器

componentDidMount() {
  // 收到监听
  this.listener = DeviceEventEmitter.addListener('SUCCESS', (message) => {
    // 收到监听后想做的事情,’SUCCESS‘ 必须与原生层传递的 eventName 一致
    console.warn(message);
    dosomething...
  });
  this.errorListener = DeviceEventEmitter.addListener('ERROR', (message) => {
    // 收到监听后想做的事情,’ERROR‘ 必须与原生层传递的 eventName 一致
    console.warn(message);
    dosomething...
  });
}
componentWillUnmount() {
  // 移除监听
  if (this.listener) { this.listener.remove() }
  if (this.errorListener) { this.listener.remove() }
}
复制代码

以上可以看出发送事件可以在任何时机,调用 sendEvent() 方法即可。但并不是任何时机都可以顺利获得 ReactContext 上下文对象,所以最好将发送事件的方法封装成一个工具类,在 App 生命周期较早的时机进行初始化,传入 ReactContext 上下文对象,然后即可在想发送事件的时机,只传递事件名和数据即可。

Update:

上面的写法中在 Js 端使用的是 DeviceEventEmitter.addListener(eventName, function) 方法注册的监听器,这是因为安卓端使用了 DeviceEventManagerModule.RCTDeviceEventEmitter.class 类完成的事件发送。但 iOS 端发送事件时,使用上面的方法注册监听器是无法响应的,这是因为在发送事件时使用的是 RCTEventEmitter 类,这个类中也是调用了 RCTDeviceEventEmitter 类完成的事件发送,但在发送前检查了注册的监听器数量:

if (_listenerCount > 0) {
   [_bridge enqueueJSCall:@"RCTDeviceEventEmitter"
               method:@"emit"
                 args:body ? @[eventName, body] : @[eventName]
           completion:NULL];
}
复制代码

为了兼容两端,所以 JS 端应该使用 NativeEventEmitter.addListener(eventName, function) 方法来注册监听器,完整的 JS 端代码:

const eventEmitter = NativeModules.EventEmitter;
const nativeEmitter = new NativeEventEmitter(eventEmitter);
const subscription = nativeEmitter.addListener(
   eventEmitter.SUCCESS,
   message => {
       console.warn(message);
       dosomething...
   }
);
复制代码

可以看出 JS 端的代码是通过使用自定义 Module 完成的监听器注册,所以安卓端也应该增加一个自定义 Module,代码如下:

public class ReactEventEmitterModule extends >ReactContextBaseJavaModule {

   public ReactEventEmitterModule(ReactApplicationContext reactContext) {
       super(reactContext);
   }

   @Override
   public Map<String, Object> getConstants() {
       Map<String, Object> constants = new HashMap<>();
       constants.put("SUCCESS", "ThisIsSuccessConstant");
       return constants;
   }

   @Override
   public String getName() {
       return "EventEmitter";
   }
}
复制代码