进阶 reactnative.dev 2026-10-11 06:04:21 · 27 阅读

第24章 原生与 React Native 之间的通信

版本:0.77 原生与 React Native 之间的通信 在《集成到现有应用》和《原生 UI 组件》两篇指南中,我们学习了如何把 React Native 嵌入原生组件,以及反过来如何做。当原生组件和 React Native 组件混用时,总免不了要让这两个世界互相通信。其他指南里已经提到过一些方法,本文对这些可用技术做一个总结。 简介 React Native 灵感来自 React,因此信息流的基本思路也是类似的:数据单向流动。我们维护一个组件层级,每个组件只依赖它的父组件和自身的内部状态。具体做法是通过属性(props):数据由父组件自上而下传给子组件。如果某个祖先组件需要依赖后代组件的状态,就应传入一个回调函数,让后代用它来更新祖先。 同样的思路也适用于 React Native。只要应用完全在框架内构建,用属性和回调就能驱动整个应用。但一旦混用 React Native 和原生组件,就需要一些特定的跨语言机制,才能在两者之间传递信息。 属性(Props) 属性是组件间通信最直接的方式。因此我们需要一种能在原生和 React Native 双向传递属性的途径。 从原生向 React Native 传递属性 要把 React Native 视图嵌入原生组件,需要用到 RCTRootView。它是一个持有 React Native 应用的 UIView,同时充当原生侧与所承载应用之间的接口。 RCTRootView 提供了一个初始化方法,可以向 React Native 应用传递任意属性。initialProperties 参数必须是 NSDictionary 实例,这个字典会在内部被转换为 JSON 对象,供顶层 JS 组件引用。 Objective-C 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];
React TSX import React from 'react';
import {View, Image} from 'react-native';

export default class ImageBrowserApp extends React.Component {
renderImage(imgURI) {
return ;
}
render() {
return {this.props.images.map(this.renderImage)};
}
}
RCTRootView 还提供了一个可读写属性 appProperties。设置 appProperties 后,React Native 应用会使用新属性重新渲染。仅当新属性与旧属性存在差异时,才会执行更新操作。 Objective-CNSArray *imageList = @[@"https://dummyimage.com/600x400/ff0000/000000.png",
@"https://dummyimage.com/600x400/ffffff/ff0000.png"];

rootView.appProperties = @{@"images" : imageList};
随时都可以更新属性,但更新操作必须在主线程执行。你可以在任意线程使用 getter。 注:目前存在一个已知问题,即在 bridge 启动期间设置 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 充当视图的 delegate,并通过 bridge 将事件发回 JavaScript。这样可以保持相关事件调用的集中管理。 从 React Native 调用原生函数(原生模块)​ 原生模块是可在 JS 中使用的 Objective-C 类。通常每个 JS bridge 会创建每个模块的一个实例。它们可以向 React Native 导出任意函数和常量。本文已对此进行过详细探讨。 原生模块是单例,这在嵌入场景下限制了该机制的灵活性。假设我们有一个嵌入在原生视图中的 React Native 组件,希望更新该原生父视图。使用原生模块机制时,我们可以导出一个函数,该函数不仅接受预期参数,还接受父原生视图的标识符。利用该标识符即可获取父视图的引用以进行更新。不过,这要求模块内部维护一个从标识符到原生视图的映射。 尽管这种方案较为复杂,但 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 侧动态决定。这个问题有两种解决方案。 第一种,把 React Native 视图包在 ScrollView 组件里。这样能保证内容始终可见,也不会与原生视图重叠。 第二种,React Native 允许在 JS 侧确定 RN 应用的尺寸,并把它告知 RCTRootView 的持有者。之后由持有者负责重新布局子视图,保持 UI 一致。这要通过 RCTRootView 的尺寸灵活性模式来实现。 RCTRootView 支持 4 种不同的尺寸灵活性模式: RCTRootView.htypedef NS_ENUM(NSInteger, RCTRootViewSizeFlexibility) {
RCTRootViewSizeFlexibilityNone = 0,
RCTRootViewSizeFlexibilityWidth,
RCTRootViewSizeFlexibilityHeight,
RCTRootViewSizeFlexibilityWidthAndHeight,
};

RCTRootViewSizeFlexibilityNone 是默认值,它会固定根视图的大小(但仍可通过 setFrame: 更新)。其余三种模式则允许我们追踪 React Native 内容的大小变化。例如,将模式设置为 RCTRootViewSizeFlexibilityHeight 后,React Native 会测量内容的高度,并将该信息回传给 RCTRootView 的 delegate。在 delegate 中可执行任意操作,包括设置根视图的 frame,以确保内容合适。只有当内容大小发生变化时,才会调用该 delegate。

注意:在 JS 和 Native 两侧同时让某个维度保持灵活,会导致未定义行为。例如,当宿主 RCTRootView 使用 RCTRootViewSizeFlexibilityWidth 时,不要用 flexbox 让顶层 React 组件的宽度变得灵活。

下面看一个示例。

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 的视图,它持有一个根视图。我们创建并初始化该根视图,同时设置其代理(delegate)。这个代理负责处理尺寸更新。随后,我们将根视图的尺寸灵活性设为 RCTRootViewSizeFlexibilityHeight,这意味着每当 React Native 内容的高度发生变化时,都会调用 rootViewDidChangeIntrinsicSize: 方法。最后,我们设置了根视图的宽度和位置。请注意,我们也设置了高度,但由于我们将高度设为由 React Native 决定,因此这个设置不会生效。 您可以在这里查看该示例的完整源代码。 动态更改根视图的尺寸灵活性模式是完全可行的。改变根视图的灵活性模式将安排一次布局重新计算,一旦内容尺寸确定,就会调用代理的 rootViewDidChangeIntrinsicSize: 方法。 注意:React Native 的布局计算是在独立线程上执行的,而原生 UI 视图的更新则是在主线程上进行的。 这可能导致原生界面和 React Native 之间出现暂时的 UI 不一致。这是一个已知问题,我们的团队正在致力于同步来自不同源的 UI 更新。 注意:在根视图成为其他视图的子视图之前,React Native 不会执行任何布局计算。 如果您希望在 React Native 视图的尺寸已知之前将其隐藏,请将根视图添加为子视图,并初始设置为隐藏状态(使用 UIView 的 hidden 属性)。然后,在代理方法中更改其可见性。

评论 (0)