Skip to main content

Native 与 React Native 之间的通信

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

¥In Integrating with Existing Apps guide and Native UI Components guide we learn how to embed React Native in a native component and vice versa. When we mix native and React Native components, we'll eventually find a need to communicate between these two worlds. Some ways to achieve that have been already mentioned in other guides. This article summarizes available techniques.

介绍

¥Introduction

React Native 受到了 React 的启发,因此信息流的基本思想是相似的。React 中的流程是单向的。我们维护组件的层次结构,其中每个组件仅依赖于其父组件及其自身的内部状态。我们用属性来做到这一点:数据以自上而下的方式从父级传递到子级。如果祖级组件依赖于其后代的状态,则应该传递一个回调以供后代使用来更新祖级。

¥React Native is inspired by React, so the basic idea of the information flow is similar. The flow in React is one-directional. We maintain a hierarchy of components, in which each component depends only on its parent and its own internal state. We do this with properties: data is passed from a parent to its children in a top-down manner. If an ancestor component relies on the state of its descendant, one should pass down a callback to be used by the descendant to update the ancestor.

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

¥The same concept applies to React Native. As long as we are building our application purely within the framework, we can drive our app with properties and callbacks. But, when we mix React Native and native components, we need some specific, cross-language mechanisms that would allow us to pass information between them.

属性

¥Properties

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

¥Properties are the most straightforward way of cross-component communication. So we need a way to pass properties both from native to React Native, and from React Native to native.

将属性从原生传递到 React Native

¥Passing properties from native to React Native

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

¥In order to embed a React Native view in a native component, we use RCTRootView. RCTRootView is a UIView that holds a React Native app. It also provides an interface between native side and the hosted app.

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

¥RCTRootView has an initializer that allows you to pass arbitrary properties down to the React Native app. The initialProperties parameter has to be an instance of NSDictionary. The dictionary is internally converted into a JSON object that the top-level JS component can reference.

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 应用将使用新属性重新渲染。仅当新更新的属性与之前的属性不同时才会执行更新。

¥RCTRootView also provides a read-write property appProperties. After appProperties is set, the React Native app is re-rendered with new properties. The update is only performed when the new updated properties differ from the previous ones.

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

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

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

¥It is fine to update properties anytime. However, updates have to be performed on the main thread. You use the getter on any thread.

注意

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

¥Currently, there is a known issue where setting appProperties during the bridge startup, the change can be lost. See https://github.com/facebook/react-native/issues/20115 for more information.

无法一次只更新几个属性。我们建议你将其构建到你自己的封装器中。

¥There is no way to update only a few properties at a time. We suggest that you build it into your own wrapper instead.

将属性从 React Native 传递到 Native

¥Passing properties from React Native to native

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

¥The problem exposing properties of native components is covered in detail in this article. In short, export properties with RCT_CUSTOM_VIEW_PROPERTY macro in your custom native component, then use them in React Native as if the component was an ordinary React Native component.

属性限制

¥Limits of properties

跨语言属性的主要缺点是它们不支持回调,而回调允许我们处理自下而上的数据绑定。想象一下,你有一个小的 RN 视图,你希望通过 JS 操作将其从原生父视图中删除。使用 props 无法做到这一点,因为信息需要自下而上。

¥The main drawback of cross-language properties is that they do not support callbacks, which would allow us to handle bottom-up data bindings. Imagine you have a small RN view that you want to be removed from the native parent view as a result of a JS action. There is no way to do that with props, as the information would need to go bottom-up.

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

¥Although we have a flavor of cross-language callbacks (described here), these callbacks are not always the thing we need. The main problem is that they are not intended to be passed as properties. Rather, this mechanism allows us to trigger a native action from JS, and handle the result of that action in JS.

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

¥Other ways of cross-language interaction (events and native modules)

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

¥As stated in the previous chapter, using properties comes with some limitations. Sometimes properties are not enough to drive the logic of our app and we need a solution that gives more flexibility. This chapter covers other communication techniques available in React Native. They can be used for internal communication (between JS and native layers in RN) as well as for external communication (between RN and the 'pure native' part of your app).

React Native 使你能够执行跨语言函数调用。你可以从 JS 执行自定义原生代码,反之亦然。不幸的是,根据我们正在努力的方面,我们以不同的方式实现相同的目标。对于本地人 - 在 JS 中,我们使用事件机制来调度处理函数的执行,而对于 React Native,我们直接调用原生模块导出的方法。

¥React Native enables you to perform cross-language function calls. You can execute custom native code from JS and vice versa. Unfortunately, depending on the side we are working on, we achieve the same goal in different ways. For native - we use events mechanism to schedule an execution of a handler function in JS, while for React Native we directly call methods exported by native modules.

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

¥Calling React Native functions from native (events)

事件在 本文 中有详细描述。请注意,使用事件并不能保证执行时间,因为事件是在单独的线程上处理的。

¥Events are described in detail in this article. Note that using events gives us no guarantees about execution time, as the event is handled on a separate thread.

事件非常强大,因为它们允许我们更改 React Native 组件而无需引用它们。然而,在使用它们时,你可能会陷入一些陷阱:

¥Events are powerful, because they allow us to change React Native components without needing a reference to them. However, there are some pitfalls that you can fall into while using them:

  • 由于事件可以从任何地方发送,因此它们可以将意大利面条式的依赖引入到你的项目中。

    ¥As events can be sent from anywhere, they can introduce spaghetti-style dependencies into your project.

  • 事件共享命名空间,这意味着你可能会遇到一些名称冲突。不会静态检测冲突,这使得它们难以调试。

    ¥Events share namespace, which means that you may encounter some name collisions. Collisions will not be detected statically, which makes them hard to debug.

  • 如果你使用同一 React Native 组件的多个实例,并且希望从事件的角度区分它们,则可能需要引入标识符并将它们与事件一起传递(你可以使用原生视图的 reactTag 作为标识符) 。

    ¥If you use several instances of the same React Native component and you want to distinguish them from the perspective of your event, you'll likely need to introduce identifiers and pass them along with events (you can use the native view's reactTag as an identifier).

在 React Native 中嵌入原生时,我们使用的常见模式是使原生组件的 RCTViewManager 成为视图的委托,通过桥将事件发送回 JavaScript。这将相关的事件调用保留在一处。

¥The common pattern we use when embedding native in React Native is to make the native component's RCTViewManager a delegate for the views, sending events back to JavaScript via the bridge. This keeps related event calls in one place.

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

¥Calling native functions from React Native (native modules)

原生模块是 JS 中可用的 Objective-C 类。通常,每个 JS 桥都会创建每个模块的一个实例。他们可以将任意函数和常量导出到 React Native。它们已在 本文 中详细介绍。

¥Native modules are Objective-C classes that are available in JS. Typically one instance of each module is created per JS bridge. They can export arbitrary functions and constants to React Native. They have been covered in detail in this article.

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

¥The fact that native modules are singletons limits the mechanism in the context of embedding. Let's say we have a React Native component embedded in a native view and we want to update the native, parent view. Using the native module mechanism, we would export a function that not only takes expected arguments, but also an identifier of the parent native view. The identifier would be used to retrieve a reference to the parent view to update. That said, we would need to keep a mapping from identifiers to native views in the module.

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

¥Although this solution is complex, it is used in RCTUIManager, which is an internal React Native class that manages all React Native views.

原生模块还可以用于向 JS 公开现有的原生库。地理定位库 就是这一理念的活生生的例子。

¥Native modules can also be used to expose existing native libraries to JS. The Geolocation library is a living example of the idea.

提醒

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

¥All native modules share the same namespace. Watch out for name collisions when creating new ones.

布局计算流程

¥Layout computation flow

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

¥When integrating native and React Native, we also need a way to consolidate two different layout systems. This section covers common layout problems and provides a brief description of mechanisms to address them.

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

¥Layout of a native component embedded in React Native

该案例包含在 本文 中。总而言之,由于我们所有的原生 React 视图都是 UIView 的子类,因此大多数样式和大小属性都将像你期望的开箱即用一样工作。

¥This case is covered in this article. To summarize, since all our native react views are subclasses of UIView, most style and size attributes will work like you would expect out of the box.

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

¥Layout of a React Native component embedded in native

使用固定大小的 React 原生内容

¥React Native content with fixed size

一般情况是当我们有一个具有固定大小的 React Native 应用时,这是原生端已知的。特别是,全屏 React Native 视图就属于这种情况。如果我们想要一个更小的根视图,我们可以显式设置 RCTRootView 的框架。

¥The general scenario is when we have a React Native app with a fixed size, which is known to the native side. In particular, a full-screen React Native view falls into this case. If we want a smaller root view, we can explicitly set RCTRootView's frame.

例如,要使 RN 应用的高度为 200(逻辑)像素,并且托管视图的宽度为宽度,我们可以这样做:

¥For instance, to make an RN app 200 (logical) pixels high, and the hosting view's width wide, we could do:

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' 不会高亮根视图边界之外的触摸。

¥When we have a fixed size root view, we need to respect its bounds on the JS side. In other words, we need to ensure that the React Native content can be contained within the fixed-size root view. The easiest way to ensure this is to use Flexbox layout. If you use absolute positioning, and React components are visible outside the root view's bounds, you'll get overlap with native views, causing some features to behave unexpectedly. For instance, 'TouchableHighlight' will not highlight your touches outside the root view's bounds.

通过重新设置根视图的框架属性来动态更新根视图的大小是完全可以的。React Native 将负责内容的布局。

¥It's totally fine to update root view's size dynamically by re-setting its frame property. React Native will take care of the content's layout.

使用灵活大小的 React 原生内容

¥React Native content with flexible size

在某些情况下,我们希望渲染最初未知大小的内容。假设大小将在 JS 中动态定义。对于这个问题我们有两种解决方案。

¥In some cases we'd like to render content of initially unknown size. Let's say the size will be defined dynamically in JS. We have two solutions to this problem.

  1. 你可以将 React Native 视图封装在 ScrollView 组件中。这保证了你的内容始终可用并且不会与原生视图重叠。

    ¥You can wrap your React Native view in a ScrollView component. This guarantees that your content will always be available and it won't overlap with native views.

  2. React Native 允许你在 JS 中确定 RN 应用的大小并将其提供给托管 RCTRootView 的所有者。然后,所有者负责重新布局子视图并保持 UI 一致。我们通过 RCTRootView 的灵活性模式来实现这一目标。

    ¥React Native allows you to determine, in JS, the size of the RN app and provide it to the owner of the hosting RCTRootView. The owner is then responsible for re-laying out the subviews and keeping the UI consistent. We achieve this with RCTRootView's flexibility modes.

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

¥RCTRootView supports 4 different size flexibility modes:

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

RCTRootViewSizeFlexibilityNone 是默认值,这使得根视图的大小固定(但仍然可以使用 setFrame: 进行更新)。其他三种模式允许我们跟踪 React Native 内容的大小更新。例如,将模式设置为 RCTRootViewSizeFlexibilityHeight 将导致 React Native 测量内容的高度并将该信息传递回 RCTRootView 的委托。可以在委托内执行任意操作,包括设置根视图的框架,以便内容适合。仅当内容大小发生更改时才调用委托。

¥RCTRootViewSizeFlexibilityNone is the default value, which makes a root view's size fixed (but it still can be updated with setFrame:). The other three modes allow us to track React Native content's size updates. For instance, setting mode to RCTRootViewSizeFlexibilityHeight will cause React Native to measure the content's height and pass that information back to RCTRootView's delegate. An arbitrary action can be performed within the delegate, including setting the root view's frame, so the content fits. The delegate is called only when the size of the content has changed.

提醒

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

¥Making a dimension flexible in both JS and native leads to undefined behavior. For example - don't make a top-level React component's width flexible (with flexbox) while you're using RCTRootViewSizeFlexibilityWidth on the hosting RCTRootView.

让我们看一个例子。

¥Let's look at an example.

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。

¥In the example we have a FlexibleSizeExampleView view that holds a root view. We create the root view, initialize it and set the delegate. The delegate will handle size updates. Then, we set the root view's size flexibility to RCTRootViewSizeFlexibilityHeight, which means that rootViewDidChangeIntrinsicSize: method will be called every time the React Native content changes its height. Finally, we set the root view's width and position. Note that we set there height as well, but it has no effect as we made the height RN-dependent.

你可以查看示例 此处 的完整源代码。

¥You can checkout full source code of the example here.

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

¥It's fine to change root view's size flexibility mode dynamically. Changing flexibility mode of a root view will schedule a layout recalculation and the delegate rootViewDidChangeIntrinsicSize: method will be called once the content size is known.

注意

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

¥React Native layout calculation is performed on a separate thread, while native UI view updates are done on the main thread. This may cause temporary UI inconsistencies between native and React Native. This is a known problem and our team is working on synchronizing UI updates coming from different sources.

注意

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

¥React Native does not perform any layout calculations until the root view becomes a subview of some other views. If you want to hide React Native view until its dimensions are known, add the root view as a subview and make it initially hidden (use UIView's hidden property). Then change its visibility in the delegate method.