文章目录
- [一、React 和 RN 是什么关系?](#一、React 和 RN 是什么关系?)
- [二、深入探究 RN](#二、深入探究 RN)
- 3.RN通信机制
一、React 和 RN 是什么关系?
首先,给出React的定义:
React 是一个用于构建用户界面的、 声明式、组件化的 JavaScript 库。(🔗React官网、React是什么东东)
我们可以用熟悉的界面开发模式,HTML+CSS+JS来和React进行对比,帮助我们理解:
objectivec
<!-- index.html -->
<div id="app">
<h1 class="title">Hello, World!</h1>
<button id="myButton">Click Me</button>
</div>
<!-- styles.css -->
.title {
color: blue;
font-size: 20px;
}
<!-- script.js -->
document.getElementById('myButton').addEventListener('click', function() {
alert('Button clicked!');
});
objectivec
// App.js
import React from 'react';
import './App.css';
function App() {
const handleClick = () => {
alert('Button clicked!');
};
return (
<div className="container">
<h1 className="title">Hello, World!</h1>
<button onClick={handleClick}>Click Me</button>
</div>
);
}
export default App;
// App.css
.container {
text-align: center;
}
.title {
color: blue;
font-size: 20px;
}
它们的对比如下:
- HTML、CSS、JavaScript:
- 分离的技术栈
UI开发通常分为HTML(结构)、CSS(样式)和JavaScript(行为)三个部分。代码分散在不同的文件中,整合和维护较为复杂。- DOM操作
直接操作DOM,通过选择器或事件监听器来更新UI。大量的DOM操作可能导致性能瓶颈,且代码难以维护。- 全局作用域
样式和脚本在全局作用域中,容易产生冲突和覆盖。- 数据管理
数据管理通常通过全局变量或DOM属性进行,容易导致数据同步问题和状态混乱。复杂的数据交互需要手动处理,增加了代码复杂性。- 性能优化
需要手动优化DOM操作,避免频繁的重排和重绘。难以管理和优化复杂的UI更新。- 生态系统
生态系统成熟,有大量的库和框架支持(如jQuery、Bootstrap等)。但库和框架之间的整合和兼容性可能是个挑战。
- React:
- 组件化开发
使用组件来封装UI和逻辑,每个组件都是独立的模块,可以嵌套、复用和组合。组件之间通过props和state进行数据传递和管理,代码更具结构化和可维护性。- 虚拟DOM
React通过虚拟DOM来提高性能。状态变化时,React会计算出最小的DOM更新并应用到实际DOM中。减少了直接操作DOM的复杂性,提高了性能和开发效率。- 模块化样式
样式可以通过CSS Modules、CSS-in-JS等方式进行模块化管理,避免全局样式冲突。- 数据管理
数据管理通过组件的state和props进行,单向数据流使得数据管理更加清晰和可预测。可以使用上下文(Context)或状态管理库(如Redux、MobX)来处理复杂的数据交互。- 性能优化
虚拟DOM和高效的diff算法自动优化UI更新,减少了手动优化的需求。使用React.memo、useMemo、useCallback等钩子进一步优化性能。- 生态系统
拥有庞大的生态系统,包括React Router、Redux、Next.js等工具和库。社区活跃,提供了丰富的教程、文档和第三方库,支持开发者快速上手和解决问题。
React的声明式编程:
声明式编程是一种编程范式,开发者只需描述"做什么",而不必关心"怎么做"。与命令式编程相对,命令式编程需要逐步描述每个操作步骤。在React中,开发者通过描述组件的状态和UI如何随状态变化来构建界面,而不是手动操作DOM来更新UI。在下面例子,开发者只需描述点击按钮后状态如何变化,React会自动处理DOM更新。
objectivec
function App() {
const [count, setCount] = React.useState(0);
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
这里用命令式编程的例子做一个对比:
objectivec
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Counter Example</title>
</head>
<body>
<p id="counter">You clicked 0 times</p>
<button id="incrementButton">Click me</button>
<script src="path/to/your/script.js"></script>
</body>
</html>
// script.js
// 为 DOMContentLoaded 事件添加监听器,确保在 DOM 完全加载后执行回调函数。
document.addEventListener('DOMContentLoaded', () => {
const counterElement = document.getElementById('counter'); //获取 DOM 元素
const incrementButton = document.getElementById('incrementButton'); //获取 DOM 元素
let count = 0;
//添加点击事件监听器
incrementButton.addEventListener('click', () => {
count += 1;
counterElement.textContent = `You clicked ${count} times`;
});
});
- 逐步描述操作:
开发者需要明确地描述每个操作步骤,如获取DOM元素、添加事件监听器、更新文本内容等。每个步骤都需要手动实现,增加了代码的复杂性。- 直接操作DOM:
通过getElementById获取DOM元素,并在事件处理中直接更新DOM。这种方式在处理复杂UI更新时可能导致性能问题和代码难以维护。- 命令式逻辑:
逻辑和状态管理都在事件处理函数中明确描述。每次点击按钮时,手动增加计数值并更新UI,开发者需要明确地告诉计算机如何执行这些操作。
React的组件式编程:
React 应用由组件组成。组件是 UI(用户界面)的一部分,具有自己的逻辑和外观。组件可以小到一个按钮,也可以大到整个页面。组件化开发是一种将UI拆分为独立、可复用组件的开发方式。每个组件封装了自己的逻辑和样式,可以独立开发、测试和复用。React 组件名称必须始终以大写字母开头,而 HTML 标签必须小写。下面是一个将 Button 组件嵌套到另一个组件中的例子。
2.RN简介
在理解 React 的基础上,我们可以给出 React Native (🔗RN官网教程) 的定义:
- React Native 是一个使用 React 和应用平台的原生功能来构建 Android 和 iOS 应用的开源框架。通过 React Native,您可以使用 JavaScript 来访问移动平台的 API,以及使用 React 组件来描述 UI 的外观和行为:一系列可重用、可嵌套的代码
- 它既保留了 React 的开发效率,又同时拥有 Native 应用的良好体验,加上 Virtual DOM 跨平台的优势,实现了真正意义上的:Learn Once,Write Anywhere
RN的特点:
- 跨平台
- React Native 使用了 Virtual DOM(虚拟 DOM),只需编写一套代码,便可以将代码打包成不同平台的 App,极大提高了开发效率,并且相对全部原生开发的应用来说,维护成本也相对更低。
- 原生体验
- React Native 使用原生组件来构建用户界面,而不是像一些其他跨平台框架那样使用 WebView 或者 HTML5。
- 高效渲染:通过异步的桥接机制,React Native 能够高效地将 JavaScript 逻辑转换为原生视图,确保流畅的用户体验。
- 原生元素:React Native 提供了一套与平台原生组件一一对应的组件库,如 View、Text、Image 等。这些组件在底层实际对应的是 iOS 的 UIView 和 Android 的 View,因此能够提供与原生应用一致的外观和行为。
- 热更新
- React Native 开发的应用支持热更新,因为 React Native 的产物是 bundle 文件,其实本质上就是 JS 代码,在 App 启动的时候就会去服务器上获取 bundle 文件,我们只需要更新 bundle 文件,从而使得 App 不需要重新前往商店下载包体就可以进行版本更新,开发者可以在用户无感知的情况下进行功能迭代或者 bug 修复。但是值得注意的是,AppStore 禁止热更新的功能中有调用私有 API、篡改原生代码和改变 App 的行为。
下面的文章可以说清楚二者的关系:React 与 React Native:有什么区别?
二、深入探究 RN
1.iOS原生模块
- 有时候 App 需要访问平台 API,但 React Native 可能还没有相应的模块封装;或者你需要复用 Objective-C、Swift 或 C++代码,而不是用 JavaScript 重新实现一遍;又或者你需要实现某些高性能、多线程的代码,譬如图片处理、数据库、或者各种高级扩展等等。
- 我们把 React Native 设计为可以在其基础上编写真正的原生代码,并且可以访问平台所有的能力。这是一个相对高级的特性,我们并不认为它应当在日常开发的过程中经常出现,但具备这样的能力是很重要的。如果 React Native 还不支持某个你需要的原生特性,你应当可以自己实现该特性的封装。
- 我们用iOS 日历 API作为示例。我们的目标就是在 Javascript 中可以访问到 iOS 的日历功能。
在 React Native 中,一个"原生模块"就是一个实现了"RCTBridgeModule"协议的 Objective-C 类,其中 RCT 是 ReaCT 的缩写。
objectivec
// CalendarManager.h
#import <React/RCTBridgeModule.h>
@interface CalendarManager : NSObject <RCTBridgeModule>
@end
为了实现 RCTBridgeModule 协议,你的类需要包含 RCT_EXPORT_MODULE() 宏。这个宏也可以添加一个参数用来指定在 JavaScript 中访问这个模块的名字。如果你不指定,默认就会使用这个 Objective-C 类的名字。如果类名以 RCT 开头,则 JavaScript 端引入的模块名会自动移除这个前缀。
objectivec
// CalendarManager.m
#import "CalendarManager.h"
@implementation CalendarManager
// To export a module named CalendarManager
RCT_EXPORT_MODULE();
// This would name the module AwesomeCalendarManager instead
// RCT_EXPORT_MODULE(AwesomeCalendarManager);
@end
你必须明确的声明要给 JavaScript 导出的方法,否则 React Native 不会导出任何方法。声明通过 RCT_EXPORT_METHOD() 宏来实现:
objectivec
#import "CalendarManager.h"
#import <React/RCTLog.h>
@implementation CalendarManager
RCT_EXPORT_MODULE();
RCT_EXPORT_METHOD(addEvent:(NSString *)name location:(NSString *)location)
{
RCTLogInfo(@"Pretending to create an event %@ at %@", name, location);
}
@end
现在从 Javascript 里可以这样调用这个方法:
objectivec
import {NativeModules} from 'react-native';
const CalendarManager = NativeModules.CalendarManager;
CalendarManager.addEvent(
'Birthday Party',
'4 Privet Drive, Surrey',
);
注意: JavaScript 方法名
- 导出到 JavaScript 的方法名是 Objective-C 的方法名的第一个部分。React Native 还定义了一个RCT_REMAP_METHOD()宏,它可以指定 JavaScript 方法名。因为 JavaScript 端不能有同名不同参的方法存在,所以当原生端存在重载方法时,可以使用这个宏来避免在 JavaScript 端的名字冲突。
桥接到 JavaScript 的方法返回值类型必须是void。React Native 的桥接操作是异步的,所以要返回结果给 JavaScript,你必须通过回调或者触发事件来进行。
参数类型
RCT_EXPORT_METHOD 支持所有标准 JSON 类型,包括:
- string (NSString)
- number (NSInteger, float, double, CGFloat, NSNumber)
- boolean (BOOL, NSNumber)
- array (NSArray) 可包含本列表中任意类型
- object (NSDictionary) 可包含 string 类型的键和本列表中任意类型的值
- function (RCTResponseSenderBlock)
我们的 CalendarManager 例子里,我们需要把事件的时间交给原生方法。我们不能在桥接通道里传递 Date 对象,所以需要把日期转化成字符串或数字来传递。我们可以这么实现原生函数:
objectivec
RCT_EXPORT_METHOD(addEvent:(NSString *)name location:(NSString *)location date:(nonnull NSNumber *)secondsSinceUnixEpoch)
{
NSDate *date = [RCTConvert NSDate:secondsSinceUnixEpoch];
}
//或者这样
RCT_EXPORT_METHOD(addEvent:(NSString *)name location:(NSString *)location date:(NSString *)ISO8601DateString)
{
NSDate *date = [RCTConvert NSDate:ISO8601DateString];
}
//不过我们可以依靠自动类型转换的特性,跳过手动的类型转换,而直接这么写:
RCT_EXPORT_METHOD(addEvent:(NSString *)name location:(NSString *)location date:(NSDate *)date)
{
// Date is ready to use!
}
对应 JavaScript 端可以这样:
objectivec
CalendarManager.addEvent(
'Birthday Party',
'4 Privet Drive, Surrey',
date.getTime(),
); // 把日期以unix时间戳形式传递
CalendarManager.addEvent(
'Birthday Party',
'4 Privet Drive, Surrey',
date.toISOString(),
); // 把日期以ISO-8601的字符串形式传递
两个值都会被转换为正确的NSDate类型。但如果提供一个不合法的值,譬如一个Array,则会产生一个"红屏"报错信息。
随着 CalendarManager.addEvent 方法变得越来越复杂,参数的个数越来越多,其中有一些可能是可选的参数。在这种情况下我们应该考虑修改我们的 API,用一个 dictionary 来存放所有的事件参数,像这样:
objectivec
#import <React/RCTConvert.h>
RCT_EXPORT_METHOD(addEvent:(NSString *)name details:(NSDictionary *)details)
{
NSString *location = [RCTConvert NSString:details[@"location"]];
NSDate *time = [RCTConvert NSDate:details[@"time"]];
...
}
//在JS端这样调用
CalendarManager.addEvent('Birthday Party', {
location: '4 Privet Drive, Surrey',
time: date.getTime(),
description: '...',
});
注意: 关于数组和映射
- Objective-C 并没有提供确保这些结构体内部值的类型的方式。你的原生模块可能希望收到一个字符串数组,但如果 JavaScript 在调用的时候提供了一个混合 number 和 string 的数组,你会收到一个NSArray,里面既有NSNumber也有NSString。对于数组来说,RCTConvert提供了一些类型化的集合,譬如NSStringArray或者UIColorArray,你可以用在你的函数声明中。对于映射而言,开发者有责任自己调用RCTConvert的辅助方法来检测和转换值的类型。
回调函数
在 React Native 中,桥接到 JavaScript 的原生方法返回值类型必须是 void,因为桥接操作是异步的。为了将结果返回给 JavaScript,必须使用回调函数或者事件触发机制。原生模块还支持一种特殊的参数------回调函数。它提供了一个函数来把返回值传回给 JavaScript。
RCTResponseSenderBlock 只接受一个参数------传递给 JavaScript 回调函数的参数数组。在上面这个例子里我们用 Node.js 的常用习惯:第一个参数是一个错误对象(没有发生错误的时候为 null),而剩下的部分是函数的返回值。原生模块通常只应调用回调函数一次。但是,它可以保存 callback 并在将来调用。这在封装那些通过"委托函数"来获得返回值的 iOS API 时最为常见。RCTAlertManager 中就属于这种情况。
objectivec
RCT_EXPORT_METHOD(findEvents:(RCTResponseSenderBlock)callback)
{
NSArray *events = ...
callback(@[[NSNull null], events]);
}
//JS端
CalendarManager.findEvents((error, events) => {
if (error) {
console.error(error);
} else {
this.setState({events: events});
}
});
Promises
- 原生模块还可以使用 Promise 来简化代码,搭配 ES2016(ES7)标准的async/await语法则效果更佳。如果桥接原生方法的最后两个参数是RCTPromiseResolveBlock 和 RCTPromiseRejectBlock,则对应的 JS 方法就会返回一个 Promise 对象。
- resolve 和 reject 是 React Native 中用于处理异步操作的 Promise 回调块。resolve 用于返回成功结果,reject 用于返回错误信息。通过这些回调块,你可以在原生代码中执行异步操作,并将结果或错误传递回 JavaScript。
我们把上面的代码用 promise 来代替回调进行重构:
objectivec
RCT_REMAP_METHOD(findEvents,
findEventsWithResolver:(RCTPromiseResolveBlock)resolve
rejecter:(RCTPromiseRejectBlock)reject)
{
NSArray *events = ...
if (events) {
resolve(events);
} else {
NSError *error = ...
reject(@"no_events", @"There were no events", error);
}
}
现在 JavaScript 端的方法会返回一个 Promise。这样你就可以在一个声明了async的异步函数内使用await关键字来调用,并等待其结果返回。(虽然这样写着看起来像同步操作,但实际仍然是异步的,并不会阻塞执行来等待)。
objectivec
async function updateEvents() { //定义一个异步函数 updateEvents,使用 async 关键字表明该函数是异步的,可以使用 await 关键字。
try {//使用 await 关键字等待 CalendarManager.findEvents() 方法的执行结果。findEvents 是一个返回 Promise 的原生模块方法。
const events = await CalendarManager.findEvents();
this.setState({events});
} catch (e) {
console.error(e);
}
}
updateEvents();//调用 updateEvents 函数,开始执行异步操作。
导出常量
- 原生模块可以导出一些常量,这些常量在 JavaScript 端随时都可以访问。用这种方法来传递一些静态数据,可以避免通过 bridge 进行一次来回交互。
- 但是注意这个常量仅仅在初始化的时候导出了一次,所以即使你在运行期间改变 constantToExport 返回的值,也不会影响到 JavaScript 环境下所得到的结果。
constantsToExport 方法用于将常量从原生模块导出到JavaScript端。该方法返回一个NSDictionary对象,其中包含常量的键值对。
objectivec
- (NSDictionary *)constantsToExport
{
return @{ @"firstDayOfTheWeek": @"Monday" };
}
//JavaScript 端可以随时同步地访问这个数据:
console.log(CalendarManager.firstDayOfTheWeek);
这个方法还受 requiresMainQueueSetup 影响,它是一个类方法,用于指示模块是否需要在主线程初始化。如果模块中的常量需要依赖主线程资源或 UI 相关的初始化,则必须确保 requiresMainQueueSetup 返回YES。
equiresMainQueueSetup方法会在模块初始化之前被调用,以确定是否需要在主线程进行初始化。
- 如果requiresMainQueueSetup返回YES,则constantsToExport方法也会在主线程上被调用。
- 如果requiresMainQueueSetup返回NO,则constantsToExport方法可能会在后台线程上被调用。
给 JS 端发送事件
即使没有被 JavaScript 调用,原生模块也可以给 JavaScript 发送事件通知。
最好的方法是继承 RCTEventEmitter,实现 supportEvents 方法并调用self sendEventWithName。
objectivec
// CalendarManager.h
#import <React/RCTBridgeModule.h>
#import <React/RCTEventEmitter.h>
@interface CalendarManager : RCTEventEmitter <RCTBridgeModule>
@end
// CalendarManager.m
#import "CalendarManager.h"
@implementation CalendarManager
RCT_EXPORT_MODULE();
- (NSArray<NSString *> *)supportedEvents
{
return @[@"EventReminder"]; //实现supportedEvents方法,返回一个包含支持事件名称的数组。
}
- (void)calendarEventReminderReceived:(NSNotification *)notification
{
NSString *eventName = notification.userInfo[@"name"];
[self sendEventWithName:@"EventReminder" body:@{@"name": eventName}];//使用sendEventWithName方法发送事件。
}
@end
JavaScript 端的代码可以创建一个包含你的模块的 NativeEventEmitter 实例来订阅这些事件。
objectivec
import { NativeEventEmitter, NativeModules } from 'react-native';
const { CalendarManager } = NativeModules;
const calendarManagerEmitter = new NativeEventEmitter(CalendarManager);
const subscription = calendarManagerEmitter.addListener(
'EventReminder',
(reminder) => console.log(reminder.name)
);
...
// 别忘了取消订阅,通常在componentWillUnmount生命周期方法中实现。
subscription.remove();
2.iOS原生UI组件
- 在如今的 App 中,已经有成千上万的原生 UI 部件了------其中的一些是平台的一部分,另一些可能来自于一些第三方库,而且可能你自己还收藏了很多。React Native 已经封装了大部分最常见的组件,譬如 ScrollView 和 TextInput ,但不可能封装全部组件。而且,说不定你曾经为自己以前的 App 还封装过一些组件,React Native 肯定没法包含它们。幸运的是,在 React Naitve 应用程序中封装和植入已有的组件非常简单。
- 假设我们要把地图组件植入到我们的 App 中------我们用到的是 MKMapView,而现在只需要让它可以在 Javascript 端使用。
原生视图都需要被一个 RCTViewManager 的子类来创建和管理。这些管理器在功能上有些类似"视图控制器",但它们实际都是单例 - React Native 只会为每个管理器创建一个实例。它们创建原生的视图并提供给 RCTUIManager,RCTUIManager 则会反过来委托它们在需要的时候去设置和更新视图的属性。RCTViewManager 还会代理视图的所有委托,并给 JavaScript 发回对应的事件。提供原生视图很简单:
- 首先创建一个RCTViewManager的子类。
- 添加RCT_EXPORT_MODULE()宏标记。
- 实现-(UIView *)view方法。
objectivec
// RNTMapManager.m
#import <MapKit/MapKit.h>
#import <React/RCTViewManager.h>
@interface RNTMapManager : RCTViewManager
@end
@implementation RNTMapManager
RCT_EXPORT_MODULE(RNTMap)
- (UIView *)view
{
return [[MKMapView alloc] init];
}
@end
注意: 请不要在-view中给 UIView 实例设置 frame 或是 backgroundColor 属性。为了和 JavaScript 端的布局属性一致,React Native 会覆盖你所设置的值。 如果您需要这种粒度的操作的话,比较好的方法是用另一个 UIView 来封装你想操作的 UIView 实例,并返回外层的 UIView。
接下来你需要一些 Javascript 代码来让这个视图变成一个可用的 React 组件:
objectivec
// MapView.js
import { requireNativeComponent } from 'react-native';
// requireNativeComponent 自动把'RNTMap'解析为'RNTMapManager'
export default requireNativeComponent('RNTMap');
// MyApp.js
import MapView from './MapView.js';
...
render() {
return <MapView style={{ flex: 1 }} />;
}
现在我们就已经实现了一个完整功能的地图组件了,诸如捏放和其它的手势都已经完整支持。但是现在我们还不能真正的从 Javascript 端控制它。
属性
我们能让这个组件变得更强大的第一件事情就是要能够封装一些原生属性供 Javascript 使用。举例来说,我们希望能够禁用手指捏放操作,然后指定一个初始的地图可见区域。禁用捏放操作只需要一个布尔值类型的属性就行了,所以我们添加这么一行:
objectivec
// RNTMapManager.m
RCT_EXPORT_VIEW_PROPERTY(zoomEnabled, BOOL)
注意我们现在把类型声明为 BOOL 类型------React Native 用 RCTConvert 来在 JavaScript 和原生代码之间完成类型转换。如果转换无法完成,会产生一个"红屏"的报错提示,这样你就能立即知道代码中出现了问题。如果一切进展顺利,上面这个宏就已经包含了导出属性的全部实现。
现在要想禁用捏放操作,我们只需要在 JS 里设置对应的属性:
objectivec
// MyApp.js
<MapView zoomEnabled={false} style={{ flex: 1 }} />
但这样并不能很好的说明这个组件的用法------用户要想知道我们的组件有哪些属性可以用,以及可以取什么样的值,他不得不一路翻到 Objective-C 的代码。要解决这个问题,我们可以创建一个封装组件,并且通过 PropTypes 来说明这个组件的接口。
objectivec
// MapView.js
import PropTypes from 'prop-types';
import React from 'react';
import { requireNativeComponent } from 'react-native';
class MapView extends React.Component {
render() {
return <RNTMap {...this.props} />;
}
}
MapView.propTypes = {
/**
* A Boolean value that determines whether the user may use pinch
* gestures to zoom in and out of the map.
*/
zoomEnabled: PropTypes.bool
};
const RNTMap = requireNativeComponent('RNTMap', MapView);
export default MapView;
现在我们有了一个封装好的组件,还有了一些注释文档,用户使用起来也更方便了。注意我们现在把 requireNativeComponent 的第二个参数从 null 变成了用于封装的组件MapView。这使得 React Native 的底层框架可以检查原生属性和包装类的属性是否一致,来减少出现问题的可能。
现在,让我们添加一个更复杂些的 region 属性。我们首先添加原生代码:
objectivec
// RNTMapManager.m
RCT_CUSTOM_VIEW_PROPERTY(region, MKCoordinateRegion, MKMapView)
{
[view setRegion:json ? [RCTConvert MKCoordinateRegion:json] : defaultView.region animated:YES];
}
//RCT_CUSTOM_VIEW_PROPERTY(name, type, viewClass):定义一个自定义属性。
//name:属性的名称,在JavaScript端使用。
//type:属性的类型。
//viewClass:视图的类。
这段代码比刚才的一个简单的BOOL要复杂的多了。现在我们多了一个需要做类型转换的 MKCoordinateRegion 类型,还添加了一部分自定义的代码,这样当我们在 JS 里改变地图的可视区域的时候,视角会平滑地移动过去。在我们提供的函数体内,json代表了 JS 中传递的尚未解析的原始值。函数里还有一个view变量,使得我们可以访问到对应的视图实例。最后,还有一个defaultView对象,这样当 JS 给我们发送 null 的时候,可以把视图的这个属性重置回默认值。
在React Native中,RCTConvert 类用于将 JavaScript 端的 JSON 数据转换为原生端的对应类型。通过创建 RCTConvert 的 Category,可以添加自定义的转换方法。下面就是用 RCTConvert 实现的 MKCoordinateRegion。它使用了 React Native 中已经存在的 RCTConvert+CoreLocation:
objectivec
/ RNTMapManager.m
#import "RCTConvert+Mapkit.h"
// RCTConvert+Mapkit.h
#import <MapKit/MapKit.h>
#import <React/RCTConvert.h>
#import <CoreLocation/CoreLocation.h>
#import <React/RCTConvert+CoreLocation.h>
@interface RCTConvert (Mapkit)
+ (MKCoordinateSpan)MKCoordinateSpan:(id)json;
+ (MKCoordinateRegion)MKCoordinateRegion:(id)json;
@end
@implementation RCTConvert(MapKit)
+ (MKCoordinateSpan)MKCoordinateSpan:(id)json
{
json = [self NSDictionary:json];
return (MKCoordinateSpan){
[self CLLocationDegrees:json[@"latitudeDelta"]],
[self CLLocationDegrees:json[@"longitudeDelta"]]
};
}
+ (MKCoordinateRegion)MKCoordinateRegion:(id)json
{
return (MKCoordinateRegion){
[self CLLocationCoordinate2D:json],
[self MKCoordinateSpan:json]
};
}
@end
这些转换函数被设计为可以安全的处理任何 JS 扔过来的 JSON:当有任何缺少的键或者其它问题发生的时候,显示一个"红屏"的错误提示。
为了完成region属性的支持,我们还需要在propTypes里添加相应的说明(否则我们会立刻收到一个错误提示),然后就可以像使用其他属性一样使用了:
objectivec
// MapView.js
MapView.propTypes = {
/**
* A Boolean value that determines whether the user may use pinch
* gestures to zoom in and out of the map.
*/
zoomEnabled: PropTypes.bool,
/**
* 地图要显示的区域。
*
* 区域由中心点坐标和区域范围坐标来定义。
*
*/
region: PropTypes.shape({
/**
* 地图中心点的坐标。
*/
latitude: PropTypes.number.isRequired,
longitude: PropTypes.number.isRequired,
/**
* 最小/最大经、纬度间的距离。
*
*/
latitudeDelta: PropTypes.number.isRequired,
longitudeDelta: PropTypes.number.isRequired,
}),
};
// MyApp.js
render() {
const region = {
latitude: 37.48,
longitude: -122.16,
latitudeDelta: 0.1,
longitudeDelta: 0.1,
};
return (
<MapView
region={region}
zoomEnabled={false}
style={{ flex: 1 }}
/>
);
}
有时候你的原生组件有一些特殊的属性希望导出,但并不希望它成为公开的接口。举个例子,Switch 组件可能会有一个 onChange 属性用来传递原始的原生事件,然后导出一个 onValueChange 属性,这个属性在调用的时候会带上 Switch 的状态作为参数之一。这样的话你可能不希望原生专用的属性出现在 API 之中,也就不希望把它放到 propTypes 里。可是如果你不放的话,又会出现一个报错。解决方案就是带上额外的 nativeOnly 参数,像这样:
objectivec
const RCTSwitch = requireNativeComponent('RCTSwitch', Switch, {
nativeOnly: { onChange: true }
});
事件
- 现在我们已经有了一个原生地图组件,并且从 JS 可以很容易的控制它了。不过我们怎么才能处理来自用户的事件,譬如缩放操作或者拖动来改变可视区域?
- 截至目前,我们从 manager 的 -(UIView *)view 方法返回了 MKMapView 实例。我们没法直接为 MKMapView 添加新的属性,所以我们只能创建一个 MKMapView 的子类用于我们自己的视图中。我们可以在这个子类中添加 onRegionChange 回调方法:
objectivec
// RNTMapView.h
#import <MapKit/MapKit.h>
#import <React/RCTComponent.h>
@interface RNTMapView: MKMapView
@property (nonatomic, copy) RCTBubblingEventBlock onRegionChange;
@end
// RNTMapView.m
#import "RNTMapView.h"
@implementation RNTMapView
@end
RCTBubblingEventBlock 是React Native框架中用于处理事件的一个类型,通常用于以下场景:
- 原生组件向JavaScript端发送事件
当原生组件需要向JavaScript端发送事件时,可以使用 RCTBubblingEventBlock类型的属性。例如,按钮点击、滑动手势等用户交互事件。- 事件冒泡
事件可以从子视图向父视图传播,类似于Web中的事件冒泡机制。
需要注意的是,所有 RCTBubblingEventBlock 必须以 on 开头。然后在 RNTMapManager上声明一个事件处理函数属性,将其作为所暴露出来的所有视图的委托,并调用本地视图的事件处理将事件转发至 JS。
objectivec
// RNTMapManager.m
#import <MapKit/MapKit.h>
#import <React/RCTViewManager.h>
#import "RNTMapView.h"
#import "RCTConvert+Mapkit.h"
@interface RNTMapManager : RCTViewManager <MKMapViewDelegate>
@end
@implementation RNTMapManager
RCT_EXPORT_MODULE()
RCT_EXPORT_VIEW_PROPERTY(zoomEnabled, BOOL)
RCT_EXPORT_VIEW_PROPERTY(onRegionChange, RCTBubblingEventBlock)
RCT_CUSTOM_VIEW_PROPERTY(region, MKCoordinateRegion, MKMapView)
{
[view setRegion:json ? [RCTConvert MKCoordinateRegion:json] : defaultView.region animated:YES];
}
- (UIView *)view
{
RNTMapView *map = [RNTMapView new];
map.delegate = self;
return map;
}
#pragma mark MKMapViewDelegate
- (void)mapView:(RNTMapView *)mapView regionDidChangeAnimated:(BOOL)animated
{
if (!mapView.onRegionChange) {
return;
}
MKCoordinateRegion region = mapView.region;
mapView.onRegionChange(@{
@"region": @{
@"latitude": @(region.center.latitude),
@"longitude": @(region.center.longitude),
@"latitudeDelta": @(region.span.latitudeDelta),
@"longitudeDelta": @(region.span.longitudeDelta),
}
});
}
@end
在委托方法 -mapView:regionDidChangeAnimated: 中,根据对应的视图调用事件处理函数并传递区域数据。调用 onRegionChange 事件会触发 JavaScript 端的同名回调函数。这个回调会传递原生事件对象,然后我们通常都会在封装组件里来处理这个对象,以使 API 更简明:
objectivec
// MapView.js
class MapView extends React.Component {
_onRegionChange = (event) => {
if (!this.props.onRegionChange) {
return;
}
// process raw event...
this.props.onRegionChange(event.nativeEvent);
}
render() {
return (
<RNTMap
{...this.props}
onRegionChange={this._onRegionChange}
/>
);
}
}
MapView.propTypes = {
/**
* Callback that is called continuously when the user is dragging the map.
*/
onRegionChange: PropTypes.func,
...
};
// MyApp.js
class MyApp extends React.Component {
onRegionChange(event) {
// Do stuff with event.region.latitude, etc.
}
render() {
const region = {
latitude: 37.48,
longitude: -122.16,
latitudeDelta: 0.1,
longitudeDelta: 0.1,
};
return (
<MapView
region={region}
zoomEnabled={false}
onRegionChange={this.onRegionChange}
/>
);
}
}