跳到主要内容

原生与 React Native 之间的通信

与现有应用集成指南原生 UI 组件指南 中,我们学习了如何将 React Native 嵌入原生组件中,以及如何反之亦然。当我们混合使用原生和 React Native 组件时,最终会需要在这两个世界之间进行通信。实现这一点的一些方法已经在其他指南中提到过。本文总结了可用的技术。

简介

React Native 受 React 的启发,因此信息流的基本思想是相似的。React 中的信息流是单向的。我们维护一个组件层次结构,其中每个组件仅依赖于其父组件和自身的内部状态。我们通过属性(props)来实现这一点:数据以自顶向下的方式从父组件传递到子组件。如果祖先组件依赖于其后代的状态,应该将一个回调函数向下传递,供后代使用来更新祖先组件。

同样的理念也适用于 React Native。只要我们在框架内完全构建我们的应用程序,我们就可以通过属性和回调函数来驱动我们的应用。但是,当我们混合使用 React Native 和原生组件时,我们需要一些特定的、跨语言的机制,以允许我们在它们之间传递信息。

属性

属性是跨组件通信最直接的方式。因此,我们需要一种方法,既可以从原生传递属性到 React Native,也可以从 React Native 传递属性到原生。

从原生传递属性到 React Native

为了将 React Native 视图嵌入原生组件中,我们使用 RCTRootViewRCTRootView 是一个 UIView,它包含一个 React Native 应用。它还提供了原生端和托管应用之间的接口。

RCTRootView 有一个初始化器,允许您将任意属性向下传递给 React Native 应用。initialProperties 参数必须是 NSDictionary 的一个实例。该字典会转换为一个 JSON 对象,顶层 JS 组件可以引用它。

objectivec
NSArray *imageList = @[@"https://dummyimage.com/600x400/ffffff/000000.png",
@"https://dummyimage.com/600x400/000000/ffffff.png"];

NSDictionary *props = @{@"images" : imageList};

RCTRootView *rootView = [[RCTRootView alloc] initWithBridge:bridge
moduleName:@"ImageBrowserApp"
initialProperties:props];
tsx
import React from 'react';
import {View, Image} from 'react-native';

export default class ImageBrowserApp extends React.Component {
renderImage(imgURI) {
return <Image source={{uri: imgURI}} />;
}
render() {
return <View>{this.props.images.map(this.renderImage)}</View>;
}
}

RCTRootView 还提供了一个读写属性 appProperties。设置 appProperties 后,React Native 应用会使用新属性重新渲染。仅当新更新的属性与之前的属性不同时,才会执行更新。

objectivec
NSArray *imageList = @[@"https://dummyimage.com/600x400/ff0000/000000.png",
@"https://dummyimage.com/600x400/ffffff/ff0000.png"];

rootView.appProperties = @{@"images" : imageList};

随时更新属性都是可以的。但是,更新必须在主线程上执行。您可以在任何线程上使用 getter。

注意

目前,有一个已知的问题是,在桥接启动期间设置 appProperties 时,更改可能会丢失。有关更多信息,请参阅 https://github.com/facebook/react-native/issues/20115

目前没有一次只更新少数属性的方法。我们建议您自己构建一个包装器。

从 React Native 传递属性到原生

暴露原生组件属性的问题已在 本文 中详细介绍。简而言之,使用自定义原生组件中的 RCT_CUSTOM_VIEW_PROPERTY 宏导出属性,然后在 React Native 中使用它们,就像该组件是一个普通的 React Native 组件一样。

属性的限制

跨语言属性的主要缺点是它们不支持回调函数,而回调函数可以让我们处理自下而上的数据绑定。想象一下,您有一个小的 RN 视图,您希望它由于 JS 操作而被从原生父视图中移除。使用 props 无法做到这一点,因为信息需要自下而上传递。

虽然我们有回调函数的一种跨语言形式(此处描述),但这些回调函数并不总是我们需要的。主要问题是它们不打算作为属性传递。相反,这个机制允许我们从 JS 触发原生操作,并在 JS 中处理该操作的结果。

其他跨语言交互方式(事件和原生模块)

如上一章所述,使用属性存在一些限制。有时属性不足以驱动我们应用程序的逻辑,我们需要一个更灵活的解决方案。本章介绍 React Native 中可用的其他通信技术。它们可用于内部通信(RN 中的 JS 和原生层之间)以及外部通信(RN 和您应用程序的“纯原生”部分之间)。

React Native 使您能够执行跨语言函数调用。您可以从 JS 执行自定义原生代码,反之亦然。不幸的是,取决于我们工作的这一端,我们以不同的方式达到相同的目标。对于原生端,我们使用事件机制来安排在 JS 中执行处理函数,而对于 React Native,我们直接调用原生模块导出的方法。

从原生调用 React Native 函数(事件)

事件在 本文 中有详细介绍。请注意,使用事件无法保证执行时间,因为事件是在单独的线程上处理的。

事件非常强大,因为它们允许我们在不需要引用它们的情况下更改 React Native 组件。但是,在使用它们时,可能会遇到一些陷阱。

  • 由于事件可以来自任何地方,它们可能会在您的项目中引入“意大利面条式”的依赖关系。
  • 事件共享命名空间,这意味着您可能会遇到一些名称冲突。冲突不会被静态检测到,这使得它们难以调试。
  • 如果您使用多个相同的 React Native 组件实例,并且希望从事件的角度区分它们,您可能需要引入标识符并将它们与事件一起传递(您可以使用原生视图的 reactTag 作为标识符)。

我们在将原生内容嵌入 React Native 时使用的常见模式是让原生组件的 RCTViewManager 成为视图的委托,通过桥接将事件发送回 JavaScript。这使得相关的事件调用集中在一个地方。

从 React Native 调用原生函数(原生模块)

原生模块是 Objective-C 类,在 JS 中可用。通常,每个原生模块在每个 JS 桥接上创建一个实例。它们可以向 React Native 导出任意函数和常量。它们已在 本文 中进行了详细介绍。

原生模块是单例的事实限制了它们在嵌入场景中的机制。假设我们有一个嵌入在原生视图中的 React Native 组件,并且我们想要更新原生父视图。使用原生模块机制,我们将导出一个函数,该函数不仅接收预期的参数,还接收父原生视图的标识符。该标识符将用于检索对父视图的引用以进行更新。也就是说,我们需要在模块中维护一个从标识符到原生视图的映射。

尽管此解决方案很复杂,但它在 RCTUIManager 中使用,RCTUIManager 是一个管理所有 React Native 视图的内部 React Native 类。

原生模块还可用于将现有的原生库暴露给 JS。 Geolocation 库是该理念的一个实际示例。

注意

所有原生模块共享相同的命名空间。创建新的原生模块时,请注意名称冲突。

布局计算流程

在集成原生和 React Native 时,我们还需要一种方法来整合两个不同的布局系统。本节介绍常见的布局问题,并提供解决这些问题的机制的简要说明。

嵌入 React Native 的原生组件的布局

这种情况已在 本文 中介绍。总而言之,由于我们所有的原生 React 视图都是 UIView 的子类,因此大多数样式和尺寸属性都能按预期工作。

嵌入原生中的 React Native 组件的布局

尺寸固定的 React Native 内容

一般场景是当我们的 React Native 应用具有固定尺寸时,原生端已知该尺寸。特别地,全屏 React Native 视图属于这种情况。如果我们想要一个较小的根视图,我们可以显式设置 RCTRootView 的 frame。

例如,为了使 RN 应用的高度为 200(逻辑)像素,而托管视图的宽度则尽可能宽,我们可以这样做:

SomeViewController.m
- (void)viewDidLoad
{
[...]
RCTRootView *rootView = [[RCTRootView alloc] initWithBridge:bridge
moduleName:appName
initialProperties:props];
rootView.frame = CGRectMake(0, 0, self.view.width, 200);
[self.view addSubview:rootView];
}

当根视图具有固定尺寸时,我们需要在 JS 端遵守其边界。换句话说,我们需要确保 React Native 内容可以包含在固定大小的根视图内。确保这一点最简单的方法是使用 Flexbox 布局。如果您使用绝对定位,并且 React 组件显示在根视图边界之外,您将遇到与原生视图重叠的问题,导致某些功能行为异常。例如,“TouchableHighlight”将无法响应根视图边界之外的触摸。

通过重新设置其 frame 属性动态更新根视图的大小是可以的。React Native 会处理内容的布局。

尺寸可变的 React Native 内容

在某些情况下,我们希望渲染初始尺寸未知的内容。假设尺寸将在 JS 中动态确定。我们有两个解决方案。

  1. 您可以将您的 React Native 视图包装在 ScrollView 组件中。这保证了您的内容始终可用,并且不会与原生视图重叠。
  2. React Native 允许我们在 JS 中确定 RN 应用的大小,并将其提供给托管 RCTRootView 的所有者。然后,所有者负责重新布局子视图并保持 UI 的一致性。我们通过 RCTRootView 的灵活性模式来实现这一点。

RCTRootView 支持 4 种不同的尺寸灵活性模式

RCTRootView.h
typedef NS_ENUM(NSInteger, RCTRootViewSizeFlexibility) {
RCTRootViewSizeFlexibilityNone = 0,
RCTRootViewSizeFlexibilityWidth,
RCTRootViewSizeFlexibilityHeight,
RCTRootViewSizeFlexibilityWidthAndHeight,
};

RCTRootViewSizeFlexibilityNone 是默认值,它使根视图的大小固定(但仍可以通过 setFrame: 更新)。其他三种模式允许我们跟踪 React Native 内容的大小更新。例如,将模式设置为 RCTRootViewSizeFlexibilityHeight 将导致 React Native 测量内容的高度,并将该信息传递回 RCTRootView 的委托。可以在委托中执行任意操作,包括设置根视图的 frame,以便内容能够适应。只有当内容尺寸发生变化时,才会调用委托。

注意

在 JS 和原生中使一个维度灵活会导致行为未定义。例如,不要使顶层 React 组件的宽度灵活(使用 flexbox),同时在托管 RCTRootView 上使用 RCTRootViewSizeFlexibilityWidth

我们来看一个例子。

FlexibleSizeExampleView.m
- (instancetype)initWithFrame:(CGRect)frame
{
[...]

_rootView = [[RCTRootView alloc] initWithBridge:bridge
moduleName:@"FlexibilityExampleApp"
initialProperties:@{}];

_rootView.delegate = self;
_rootView.sizeFlexibility = RCTRootViewSizeFlexibilityHeight;
_rootView.frame = CGRectMake(0, 0, self.frame.size.width, 0);
}

#pragma mark - RCTRootViewDelegate
- (void)rootViewDidChangeIntrinsicSize:(RCTRootView *)rootView
{
CGRect newFrame = rootView.frame;
newFrame.size = rootView.intrinsicContentSize;

rootView.frame = newFrame;
}

在该示例中,我们有一个 FlexibleSizeExampleView 视图,它包含一个根视图。我们创建根视图,对其进行初始化并设置委托。委托将处理尺寸更新。然后,我们将根视图的尺寸灵活性设置为 RCTRootViewSizeFlexibilityHeight,这意味着每次 React Native 内容的高度发生变化时,都会调用 rootViewDidChangeIntrinsicSize: 方法。最后,我们设置根视图的宽度和位置。请注意,我们也在那里设置了高度,但由于我们将高度设置为 RN 依赖的,所以它没有效果。

您可以在 此处 查看该示例的完整源代码。

动态更改根视图的尺寸灵活性模式是可以的。更改根视图的灵活性模式将安排一次布局重新计算,并在内容尺寸已知后调用委托方法 rootViewDidChangeIntrinsicSize:

注意

React Native 布局计算是在单独的线程上执行的,而原生 UI 视图更新是在主线程上完成的。这可能导致原生和 React Native 之间出现暂时的 UI 不一致。这是一个已知问题,我们的团队正在努力同步来自不同来源的 UI 更新。

注意

React Native 在根视图成为其他视图的子视图之前,不会执行任何布局计算。如果您想在尺寸已知之前隐藏 React Native 视图,请将根视图添加为子视图并将其初始设置为隐藏(使用 UIViewhidden 属性)。然后在委托方法中更改其可见性。