跳至主内容
版本:0.77

原生与 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 视图,需使用 RCTRootViewRCTRootView 是一个承载 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 视图,请先将根视图添加为子视图并初始将其隐藏(使用 UIViewhidden 属性),随后在委托方法中更改其可见性。