你知道原生端和JavaScript端如何通信吗?

我心飞翔 分类:javascript

React Native 作为跨平台开发框架,采用了 JavaScript 语言开发视图和逻辑,iOS 和 Android 两个原生端通过桥接层与 JavaScript 环境进行通信。利用桥接层提供的通信方式,JavaScript 和原生端可以实现互相调用各自定义好的方法。这是 React Native 可以实现跨平台的基本原理,同时也是 React Native 框架的性能瓶颈所在。

其实在React Native 的官方文档中,已经详细的介绍了我们应该怎么编写"原生模块",以供JavaScript端调用。
QQ截图20210321112310.png

但并没有介绍如何编写 "JavaScript模块" 以供原生端调用。

而很多开发者也不了解, JavaScript端和原生端是如何通信的,所以下面我就从

  • 初始化阶段
  • 原生端调用JavaScript端
  • JavaScript端调用原生端

三个场景,给你简单介绍一下,原生端和JavaScript端是如何通信的。

React Native初始化阶段

1.png

  1. 在React Native初始化的时候

    1. 原生端会遍历开发者自定义的原生模块和Reat Native框架需要的核心原生模块。一起注册到一个"原生模块映射表中",这个映射表注册着所有原生模块实例,和它们导出的模块名。
    2. 原生端还会定义一些JavaScript模块的抽象接口,注册进"JavaScript模块映射表" 中。这些JavaScript模块的抽象接口之定义了模块名称,包含的方法名以及入参等,并没有具体的实现逻辑。
  2. 原生端将这两个映射表传递给桥阶层(JavaScriptCore 简称 JSC) 中, JSC再将两个映射表转发给JavaScript端

  3. JavaScript端会在JavaScript环境建立起来后

    1. 将所有的JavaScript模块实例注册进"JavaScript模块映射表" 中。
    2. 然后JavaScript端遍历 "原生模块映射表" ,根据原生模块导出的模块名和方法名将所有的原生模块挂载到 NativeModules 对象下。

可能这么讲有些抽象,不好理解!那么我们可以做一下类比
01.png
一个性格比较主动的商人(原生端),想要找一个性格比较被动的商人(JavaScript端) 做生意。但是他们的语言不通。所以他们找到了一位翻译(JavaScriptCore)

  1. 主动的商人开门见山的说,我手里有一批货(原生模块映射表),我想交易得到另外一批货(JavaScript模块映射表)
  2. 翻译将主动的商人提出的诉求转告给被动的商人。
  3. 被动的商人听到后,盘点的了一些自己手上的货物(JavaScript模块映射表),并欣然接受了对方提供的货物(原生模块映射表)

至此,双方签订了协议,为后续的交易奠定了基础

原生端调用JavaScript端

完成了初始化的工作后,如果原生端想要调用JavaScript模块中的方法怎么办呢?
3.png

  1. 原生端需要先从"JavaScript模块映射表"中查找到想要调用的JavaScript模块,调用JavaScript接口中的方法去传递参数,此时调用的模块名/方法名/参数等信息都会传递给桥阶层JSC

  2. 桥阶层JSC转发给JavaScript端

  3. JavaScript端接收到信息后,会根据模块名在"JavaScript模块映射表"中进行查找。最后根据方法名和参数信息直接调用查找匹配到的JavaScript模块实例中的对应方法。传递参数并执行方法

沿用上面类比的示例,我们可以这样描述一下原生端调用JavaScript端的流程
5.png

  1. 性格主动的商人(原生端)想要购置一批货物,所以他找出了当期和性格被动的商人(JavaScript端)签订的协议(原生端的JavaScript模块映射表)。

主动的商人查阅并确认了协议中包含了他想要的货物,所以他找来了翻译(JavaScriptCore)。将他的购置需求都告诉了翻译

  1. 翻译将购置需求一五一十地转述给了被动商人

  2. 被动商人拿出了自己的库存清单(JavaScript端的JavaScript模块映射表)。 找到了购置需求匹配的货物,最终将货物交易了出去

封装一个JavaScript模块

讲到这里,你可能对React Native的通信机制有了一个大概的了解,但是同时也产生了一个疑惑: JavaScript模块是怎么封装的,为什么 React Native的官方文档里只介绍了如何封装原生模块给JavaScript端调用,却不介绍如何封装JavaScript模块给原生模块调用呢?

别着急, 下一步我就带你一步一步封装一个JavaScript模块出来,这里可以查看源码

JavaScript端创建工具

在JavaScript端,我们需要定义一个对象

// ShareStore.ts

const ShareStore = {
  pool: new Map(),
  // 存储数据
  setItem(key: string, value: any) {
    this.pool.set(key, value);
    console.log('setItem', key, value);
  },
  // 获取数据
  getItem(key: string) {
    return this.pool.get(key);
  },
};

// __fbBatchedBridge 是React Native 中的一个全局对象
const BatchedBridge = __fbBatchedBridge;
BatchedBridge.registerCallableModule('ShareStore', ShareStore);

export default ShareStore;

 

规定好它提供的方法,最后将这个对象通过registerCallabkeModule()方法,注册到 BatchBridge中,以构成"JavaScript模块映射表"

在Android端,我们需要
QQ截图20210322000647.png

  1. 注册原生模块提供的对应接口
  2. 然后在 React Native上下文对象创建完毕后通过 getJSModule 方法传入对应的接口,就能够完成JavaScript模块实例的获取,
  3. 然后调用想要的方法就行了

Android端注册原生模块接口

// ShareStore.java

package com.reactnativedemo.utils; // 注意自己的包名

import com.facebook.react.bridge.JavaScriptModule;

public interface ShareStore extends JavaScriptModule {
    void setItem(String key,Object value);
}

 

Android端将原生模块接口挂载到 getJSModule 上

//  MainApplication.java  

@Override
  public void onCreate() {
    super.onCreate();
		.........
        .........    
            
      ReactInstanceManager reactInstanceManager = getReactNativeHost().getReactInstanceManager();
      reactInstanceManager.addReactInstanceEventListener((new ReactInstanceManager.ReactInstanceEventListener() {
          @Override
          public void onReactContextInitialized(ReactContext context) {
              context.getCatalystInstance().getJSModule(ShareStore.class).setItem("example","hello RN!");
          }

      }));

  }
 

JavaScript端使用

import React, { useEffect, useState } from 'react';

import {
  SafeAreaView,
  Text,
  View,
  Button,
} from 'react-native';

import ShareStore from './components/ShareStore';

const App = props => {
  const [exampleValue, setExampleValue] = useState(null);

  return (
    <SafeAreaView >
      <Button
            title="点击我获取原生设置的值"
            onPress={() => {
              const value = ShareStore.getItem('example');
              setExampleValue(value);
            }}
          />
        <Text style={styles.text}>
          原生端传过来的信息:
          {exampleValue}
        </Text>
    </SafeAreaView>
  );
};

export default App;

 

其实,React Native中,需要原生端主动发起的操作都需要JavaScript模块来完成,例如,展示React Native视图时,需要原生端调用 AppRegistry模块下的runApplication()方法等等

官方文档-NativeEventEmitter 的使用

那么为什么React Native 没有在文档中介绍如何封装JavaScript模块呢?因为 React Native已经封装了 NativeEventEmitter模块,作为原生端向JavaScript端通信的主要手段,并且 Reat Native在JavaScript端还封装了事件的订阅和分发机制,如果你有原生端主动向JavaScript端通信的需求,可以按照 Reat Native官方文档的描述优先选用NativeEventEmitter模块
0.png

JavaScript端调用原生端

02.png
如果JavaScript端想要调用原生模块的方法。

  1. JavaScript端首先要从 "原生模块映射表" 中查找到想要调用的原生模块,调用NativeModules对象下挂载的原生模块的对应方法去传递参数。如果这个方法具有返回值或者回调,那么就会生成一个回调ID,与调用的模块名/方法名/参数一起传递到桥阶层JSC

  2. 桥阶层JSC将信息转发给原生端

  3. 原生端接收到信息后会根据模块名在"原生模块映射表" 中进行查找

    1. 根据方法名和参数信息调用查找匹配到的原生模块实例中的对应方法,传递参数并且执行方法
    2. 如果这个方法需要回传一个返回值或者执行回调,那么原生端会将执行好的方法和回调ID一起传递给桥阶层JSC
  4. 桥阶层JSC将信息转发到JavaScript端

  5. JavaScript端接收到执行解雇哦吼,会根据回调ID将刚刚的结果进行分发

到这里,整个调用流程就完成了!上面的流程有些复杂,我们可以这样类比

  1. 有一天被动的商人(JavaScript端)想要购置一批货物,所以他找出了当初和性格主动的商人(原生端)签订的协议(JavaScript端的原生模块映射表)

被动的商人检查了协议中包含了他想要的货物,所以他找来了翻译(JSC),将他的购置需求详细告诉了翻译。并且按照自己的需求告诉翻译自己需不需要发票单据(原生模块/方法的执行结果)

  1. 翻译将被动商人的需求转告给主动的商人

  2. 主动的商人盘点了一下自己的库存清单(原生端的原生模块映射表),找到需要交易的货物,然后按照被动商人的需求决定要不要开发票

  3. 最后翻译将发票单据带给了性格被动的商人,完成了这次交易

~~

总结

最后我们总结一下,其实整个通信过程的核心是利用桥阶层JSC在原生端和JavaScript端进行信息传递,但是 React Native 将传递信息的机制进行了规范化封装,两端传递信息的方法调用必须在使用原生模块和JavaScript模块下进行
000.png

所以在初始化阶段,原生端主动发起请求,同步"原生模块映射表"和"JavaScript模块映射表"给JavaScript端

此后,在任意一段注定发起通信时,都需要查阅对应的模块映射表,严格按照映射表上规定的方法和参数进行通信,这提升了通信流程的安全性。

今天分享的内容就到这里,希望能够帮助到你。欢迎有不同见解的同学在评论区留言!

回复

我来回复
  • 暂无回复内容