原生与 React Native 之间的通信
在与现有应用集成指南和原生 UI 组件指南中,我们学习了如何将 React Native 嵌入到原生组件中,反之亦然。当我们混合使用原生和 React Native 组件时,最终会发现需要在这两个世界之间进行通信。其他指南中已经提到了一些实现方法。本文总结了可用的技术。
简介
React Native 受 React 启发,因此信息流的基本思想是相似的。React 中的流是单向的。我们维护一个组件层次结构,其中每个组件仅依赖于其父组件和自身的内部状态。我们通过属性来做到这一点:数据以自上而下的方式从父组件传递给其子组件。如果祖先组件依赖于其后代组件的状态,则应传递一个回调函数,供后代组件用于更新祖先组件。
同样的概念也适用于 React Native。只要我们纯粹在框架内构建应用程序,我们就可以使用属性和回调来驱动我们的应用程序。但是,当我们混合使用 React Native 和原生组件时,我们需要一些特定的跨语言机制,以允许它们之间传递信息。
属性
属性是跨组件通信最直接的方式。因此,我们需要一种方法来在原生到 React Native 以及 React Native 到原生之间传递属性。
从原生向 React Native 传递属性
为了将 React Native 视图嵌入到原生组件中,我们使用 RCTRootView
。RCTRootView
是一个包含 React Native 应用程序的 UIView
。它还提供了原生端与托管应用程序之间的接口。
RCTRootView
有一个初始化器,允许您将任意属性传递给 React Native 应用程序。initialProperties
参数必须是 NSDictionary
的实例。字典在内部转换为 JSON 对象,顶层 JS 组件可以引用该对象。
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];
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 应用程序将使用新属性重新渲染。仅当新的更新属性与旧属性不同时才执行更新。
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 调用原生函数(原生模块)
原生模块是可在 JS 中使用的 Objective-C 类。通常,每个 JS 桥接器都会创建一个模块实例。它们可以将任意函数和常量导出到 React Native。它们在本文中有详细介绍。
原生模块是单例的事实限制了该机制在嵌入上下文中的作用。假设我们将 React Native 组件嵌入到原生视图中,并且我们希望更新原生父视图。使用原生模块机制,我们将导出一个函数,该函数不仅接受预期的参数,还接受父原生视图的标识符。该标识符将用于检索父视图的引用以进行更新。也就是说,我们需要在模块中维护从标识符到原生视图的映射。
尽管此解决方案很复杂,但它已在 RCTUIManager
中使用,RCTUIManager
是管理所有 React Native 视图的内部 React Native 类。
原生模块还可以用于将现有原生库公开给 JS。地理位置库是该思想的一个活生生的例子。
所有原生模块共享相同的命名空间。创建新模块时请注意名称冲突。
布局计算流程
在集成原生和 React Native 时,我们还需要一种方法来整合两种不同的布局系统。本节涵盖常见的布局问题,并简要描述了解决这些问题的机制。
嵌入 React Native 的原生组件的布局
此案例在本文中有所介绍。总而言之,由于所有原生 React 视图都是 UIView
的子类,因此大多数样式和大小属性都会按预期开箱即用。
嵌入原生的 React Native 组件的布局
固定大小的 React Native 内容
一般场景是当我们有一个固定大小的 React Native 应用程序,并且原生端知道其大小。特别是,全屏 React Native 视图属于这种情况。如果我们想要一个较小的根视图,我们可以显式设置 RCTRootView 的 frame。
例如,要使 RN 应用程序高 200(逻辑)像素,且与托管视图的宽度相同,我们可以这样做:
- (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 中动态定义。我们有两种解决方案来解决这个问题。
- 您可以将 React Native 视图包装在
ScrollView
组件中。这保证了您的内容始终可用,并且不会与原生视图重叠。 - React Native 允许您在 JS 中确定 RN 应用的大小,并将其提供给托管
RCTRootView
的所有者。所有者随后负责重新布局子视图并保持 UI 一致。我们通过RCTRootView
的灵活性模式来实现这一点。
RCTRootView
支持 4 种不同的尺寸灵活性模式
typedef NS_ENUM(NSInteger, RCTRootViewSizeFlexibility) {
RCTRootViewSizeFlexibilityNone = 0,
RCTRootViewSizeFlexibilityWidth,
RCTRootViewSizeFlexibilityHeight,
RCTRootViewSizeFlexibilityWidthAndHeight,
};
RCTRootViewSizeFlexibilityNone
是默认值,它使根视图的大小固定(但仍然可以通过 setFrame:
更新)。其他三种模式允许我们跟踪 React Native 内容的大小更新。例如,将模式设置为 RCTRootViewSizeFlexibilityHeight
将导致 React Native 测量内容的高度并将该信息传递回 RCTRootView
的委托。可以在委托中执行任意操作,包括设置根视图的 frame,以便内容适配。委托仅在内容大小更改时调用。
在 JS 和原生中都使一个维度具有弹性会导致未定义的行为。例如,在使用托管 RCTRootView
上的 RCTRootViewSizeFlexibilityWidth
时,请勿使顶级 React 组件的宽度具有弹性(使用 flexbox
)。
让我们看一个例子。
- (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 视图,请将根视图添加为子视图并使其最初隐藏(使用 UIView
的 hidden
属性)。然后,在委托方法中更改其可见性。