跳至主内容
版本:0.79

原生与 React Native 的通信

非官方测试版翻译

本页面由 PageTurner AI 翻译(测试版)。未经项目官方认可。 发现错误? 报告问题 →

在集成到现有应用指南和原生 UI 组件指南中,我们学习了如何将 React Native 嵌入原生组件以及反向操作。当混合使用原生组件和 React Native 组件时,最终需要在两者之间建立通信机制。其他指南已提到过若干实现方式,本文则系统总结现有可用技术。

引言​

React Native 的设计灵感源自 React,其数据流的基本理念也与之相似:单向数据流。我们维护组件层级结构,每个组件仅依赖其父组件和自身内部状态。这是通过属性(properties)实现的:数据以自顶向下的方式从父组件传递到子组件。若祖先组件需要依赖后代组件的状态,则应向下传递回调函数供后代更新祖先状态。

相同理念同样适用于 React Native。只要完全在框架内构建应用,我们可通过属性和回调驱动应用。但当混合使用 React Native 和原生组件时,就需要特定的跨语言机制来实现信息传递。

属性​

属性是实现跨组件通信最直接的方式。因此我们需要实现双向属性传递:既支持从原生到 React Native,也支持从 React Native 到原生。

从原生传递属性到 React Native​

若要在原生组件中嵌入 React Native 视图,需使用 RCTRootView。RCTRootView 是一个承载 React Native 应用的 UIView,同时提供原生端与宿主应用间的交互接口。

RCTRootView 的初始化方法支持向 React Native 应用传递任意属性。initialProperties 参数必须是 NSDictionary 实例,该字典在内部会转换为顶层 JS 组件可引用的 JSON 对象。

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

属性可随时更新,但必须在主线程执行更新操作。获取操作可在任意线程执行。

备注

注意:当前存在已知问题,在桥接启动阶段设置 appProperties 可能导致更改丢失。详见 https://github.com/facebook/react-native/issues/20115。

目前不支持局部属性更新。建议自行构建封装器实现此功能。

从 React Native 传递属性到原生​

原生组件属性的暴露问题已在此文详述。简而言之:在自定义原生组件中使用 RCT_CUSTOM_VIEW_PROPERTY 宏导出属性,即可在 React Native 中像使用普通组件属性那样操作它们。

属性通信的局限​

跨语言属性通信的主要缺陷是不支持回调函数,导致无法处理自底向上的数据绑定。例如:当需要通过 JS 操作移除原生父视图中的 RN 子视图时,属性机制无法实现此需求——因为信息需要自底向上传递。

虽然存在跨语言回调机制(详见此处),但这些回调往往并非最佳解决方案。核心问题在于它们并非设计为属性传递机制,而是用于从 JS 触发原生操作并在 JS 中处理操作结果。

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

正如前一章所述,使用属性存在一些局限性。有时属性不足以驱动应用程序的逻辑,我们需要更灵活的解决方案。本章将介绍 React Native 中可用的其他通信技术,这些技术既可用于内部通信(RN 中 JS 与原生层之间),也可用于外部通信(RN 与应用程序"纯原生"部分之间)。

React Native 支持跨语言函数调用。您可以从 JS 执行自定义原生代码,反之亦然。但根据操作端不同,实现方式有所差异:在原生端我们使用事件机制调度 JS 的处理函数执行,而在 React Native 端则直接调用原生模块导出的方法。

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

事件机制在本文中有详细说明。请注意使用事件无法保证执行时间,因为事件处理在独立线程进行。

事件非常强大,因为它允许我们在不需要组件引用的情况下修改 React Native 组件。但使用时需注意以下陷阱:

  • 事件可从任意位置发送,可能导致项目中出现面条式依赖

  • 事件共享命名空间,可能发生名称冲突。冲突无法静态检测,难以调试

  • 当使用多个相同 React Native 组件实例时,若需在事件中区分它们,通常需要引入标识符随事件传递(可使用原生视图的 reactTag 作为标识符)

将原生组件嵌入 React Native 时的通用模式是:让原生组件的 RCTViewManager 作为视图的代理,通过 bridge 将事件传回 JavaScript。这可将相关事件调用集中管理。

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

原生模块是可在 JS 中使用的 Objective-C 类。通常每个模块在 JS bridge 中创建单个实例。它们可以向 React Native 导出任意函数和常量,具体已在本文详述。

原生模块的单例特性在嵌入场景中存在限制。假设我们在原生视图中嵌入了 React Native 组件并需要更新原生父视图:使用原生模块机制时,导出的函数不仅需要接收预期参数,还需包含父原生视图的标识符。该标识符用于获取父视图引用进行更新,这意味着我们需要在模块中维护从标识符到原生视图的映射关系。

虽然此方案较复杂,但 React Native 内部管理所有视图的 RCTUIManager 类正是采用这种方式。

原生模块也可用于向 JS 暴露现有原生库,地理位置库是该理念的典型实践。

注意

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

布局计算流程​

整合原生与 React Native 时,我们还需要统一两种不同的布局系统。本节将探讨常见布局问题,并简要描述解决方案机制。

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

本文已涵盖此情况。简而言之,由于所有原生 React 视图都是 UIView 子类,大多数样式和尺寸属性都可直接按预期工作。

嵌入原生环境的 React Native 组件布局​

固定尺寸的 React Native 内容​

常见场景是当我们有一个固定尺寸的 React Native 应用,且该尺寸已被原生端知晓。特别是全屏 React Native 视图就属于这种情况。如果需要更小的根视图,我们可以显式设置 RCTRootView 的 frame 属性。

例如,要创建高度为 200(逻辑)像素、宽度与宿主视图等宽的 RN 应用,可以这样实现:

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 视图,请先将根视图添加为子视图并初始将其隐藏(使用 UIView 的 hidden 属性),随后在委托方法中更改其可见性。