前置准备
- 安装
CocoaPods
- 安装
React Native
开发环境
1. 安装JavaScript依赖包
在原生iOS项目根目录下创建RN组件目录,进入该组件目录,并创建包管理文件package.json
。编辑该文件,添加依赖包内容或基础内容。
{
"?name": "需集成到原生项目的名称。(带?的key代表注释)",
"name": "MyReactNativeApp",
"?version": "版本号,如不要上传到npm,无意义",
"version": "0.0.1",
"private": true,
"scripts": {
"start": "yarn react-native start"
}
}
可以在package.json
中添加React Native
版本号及其他额外信息。例如:
{
"name": "RNIntegrationDemo",
"version": "0.0.1",
"private": true,
"scripts": {
"android": "react-native run-android",
"ios": "react-native run-ios",
"start": "react-native start",
"test": "jest",
"lint": "eslint ."
},
"?dependencies": "使用npm install指令安装时添加的依赖信息",
"dependencies": {
"react": "16.9.0",
"react-native": "0.61.3",
"react-native-gesture-handler": "^1.4.1",
"react-native-reanimated": "^1.3.0",
"react-navigation": "^4.0.10",
"react-navigation-stack": "^1.10.2",
"react-navigation-tabs": "^2.5.6"
},
"devDependencies": {
"@babel/core": "^7.6.4",
"@babel/runtime": "^7.6.3",
"@react-native-community/eslint-config": "^0.0.5",
"babel-jest": "^24.9.0",
"eslint": "^6.5.1",
"jest": "^24.9.0",
"metro-react-native-babel-preset": "^0.56.0",
"react-test-renderer": "16.9.0"
},
"jest": {
"preset": "react-native"
}
}
编辑好package.json
,终端进入该文件目录下即可安装相关依赖包。相关指令:
yarn add react-native@0.61.3 // 用yarn安装,需指定版本号,否则总是会安装最新版本
yarn add react@16.9.0 // 有时可能会提示需要安装对应版本的react依赖,则通过此指令安装,且版本号必须与提示的一致
npm install // 直接安装package.json中所有指定依赖包
2. iOS项目添加React Native代码库
使用CocoaPods
初始化项目,打开Podfile
,添加相关React Native
库,然后执行pod install
。
例如:
target 'RNIntegrationDemo' do
# React核心库
pod 'React', :path => './RNComponents/node_modules/react-native/'
# 下列为React所需的依赖库
pod 'React-Core', :path => './RNComponents/node_modules/react-native/'
pod 'React-RCTActionSheet', :path => './RNComponents/node_modules/react-native/Libraries/ActionSheetIOS'
pod 'React-RCTAnimation', :path => './RNComponents/node_modules/react-native/Libraries/NativeAnimation'
pod 'React-RCTBlob', :path => './RNComponents/node_modules/react-native/Libraries/Blob'
pod 'React-RCTImage', :path => './RNComponents/node_modules/react-native/Libraries/Image'
pod 'React-RCTLinking', :path => './RNComponents/node_modules/react-native/Libraries/LinkingIOS'
pod 'React-RCTNetwork', :path => './RNComponents/node_modules/react-native/Libraries/Network'
pod 'React-RCTSettings', :path => './RNComponents/node_modules/react-native/Libraries/Settings'
pod 'React-RCTText', :path => './RNComponents/node_modules/react-native/Libraries/Text'
pod 'React-RCTVibration', :path => './RNComponents/node_modules/react-native/Libraries/Vibration'
pod 'React-cxxreact', :path => './RNComponents/node_modules/react-native/ReactCommon/cxxreact'
pod 'React-jsi', :path => './RNComponents/node_modules/react-native/ReactCommon/jsi'
pod 'React-jsiexecutor', :path => './RNComponents/node_modules/react-native/ReactCommon/jsiexecutor'
pod 'React-jsinspector', :path => './RNComponents/node_modules/react-native/ReactCommon/jsinspector'
# 其他运行在原生端所需核心组件及其依赖库
pod 'React-CoreModules', :path => './RNComponents/node_modules/react-native/React/CoreModules'
pod 'FBLazyVector', :path => "./RNComponents/node_modules/react-native/Libraries/FBLazyVector"
pod 'FBReactNativeSpec', :path => "./RNComponents/node_modules/react-native/Libraries/FBReactNativeSpec"
pod 'RCTRequired', :path => "./RNComponents/node_modules/react-native/Libraries/RCTRequired"
pod 'RCTTypeSafety', :path => "./RNComponents/node_modules/react-native/Libraries/TypeSafety"
pod 'ReactCommon/turbomodule/core', :path => "./RNComponents/node_modules/react-native/ReactCommon"
# 以下为必需依赖库
pod 'Yoga', :path => './RNComponents/node_modules/react-native/ReactCommon/yoga'
pod 'DoubleConversion', :podspec => './RNComponents/node_modules/react-native/third-party-podspecs/DoubleConversion.podspec'
pod 'glog', :podspec => './RNComponents/node_modules/react-native/third-party-podspecs/glog.podspec'
pod 'Folly', :podspec => './RNComponents/node_modules/react-native/third-party-podspecs/Folly.podspec'
# Uncomment the next line if you're using Swift or would like to use dynamic frameworks
# use_frameworks!
# Pods for RNIntegrationDemo
end
添加依赖过程中可能遇到的错误:
Unable to find a specification for 'React-Core' ( = 0.61.3 ) depended upon by 'React'
:找不到React
所依赖的React-Core
库。即缺失React
的依赖库React-Core
。Pod引入指定路径的对应库即可。React
可能会依赖上面所列举的多个库,如果有类似提示,需逐一添加依赖。- 找不到对应的库:
npm
安装的依赖库都在本地,在Pod添加依赖时,需指定正确的路径。如上述依赖库是安装在原生项目根目录下的RNComponents
中,所有的依赖库都是在./RNComponents
文件夹下的。 - 在使用
React Native
模块代码调试之前,进入RNComponents
目录通过npm start
命令启动服务器,其他命令如react-native start
可能会报错。 Podfile
中除了React
核心库及基础依赖库之外,还要导入CoreModules
相关的依赖库,即上述Podfile
中的库都是必须的,否则会报错。
React Native 与 iOS 通信
添加通信模块与方法
一个遵循RCTBridgeModule
协议的类即为一个原生模块。例如:
#import <React/RCTBridgeModule.h>
#import <React/RCTLog.h>
// 定义一个原生模块,需遵循RCTBridgeModule协议
@interface BridgeModule : NSObject <RCTBridgeModule>
@end
@implementation BridgeModule
// 指定此模块在React Native中的名称,可以添加一个字符串参数。不指定参数默认为类名
RCT_EXPORT_MODULE()
// 导出在React Native端可调用的方法
RCT_EXPORT_METHOD(showAlert) {
// 方法的调用是异步的,涉及到UI的操作,需要在主线程中执行
dispatch_async(dispatch_get_main_queue(), ^{
UIAlertController *alertController = [UIAlertController alertControllerWithTitle:@"This is an alert" message:@"Test for export method to react native" preferredStyle:UIAlertControllerStyleAlert];
UIAlertAction *action = [UIAlertAction actionWithTitle:@"OK, I see" style:UIAlertActionStyleDefault handler:nil];
[alertController addAction:action];
UIViewController *rootViewController = [UIApplication sharedApplication].keyWindow.rootViewController;
[rootViewController presentViewController:alertController animated:YES completion:nil];
NSLog(@"This function is called by react native");
});
}
// 导出带参数的方法,并对方法做映射。React Native只取用第一个冒号前的字符串作为JS方法,对方法做映射可以避免方法在JS中重名
// 所有的原生方法返回值都是void,如果需要返回值,要采用其他方式,比如函数回调
RCT_REMAP_METHOD(logSomeInfo, printInfo:(NSString *)info extraInfo:(NSString *)extraInfo) {
NSLog(@"%@, %@", info, extraInfo);
}
@end
在React Native中调用原生模块方法
import { NativeModules } from 'react-native';
var BridgeModule = NativeModules.BridgeModule;
BridgeModule.showAlert();
// 调用原生的方法时,必须严格传入对应个数的参数,并且类型需匹配
BridgeModule.logSomeInfo('This message is from react native', '!');
参数类型
RCT_EXPORT_METHOD
支持所有JSON
的基本类型,包括:
string
(NSString
)number
(NSInteger
,float
,double
,CGFloat
,NSNumber
)boolean
(BOOL
,NSNumber
)array
(NSArray
)(此处所列举的所有类型)object
(NSDictionary
)(此处所列举的所有类型的键值对)function
(RCTResponseSenderBlock
)
此外,React Native还提供了RCTConvert
类用于将部分JSON值转换为原生的Objective-C类型或类。
当方法包含的参数逐渐增多时,应该考虑将多个参数用字典合并接收。
回调函数
可以使用回调函数来给JavaScript回传值。回调函数的形式类似于OC定义方法中的Block参数,而JavaScript在调用该方法的时候会传入一个函数,在原生端调用该block参数,即调用JS端的该函数,从而实现将原生端的参数传入JS。
// RCTResponseSenderBlock是一个只有一个NSArray参数的block
RCT_EXPORT_METHOD(findEvents:(RCTResponseSenderBlock)callback) {
NSArray *params = @[argu1, argu2, ...]
// 一般约定参数数组的第一个参数为error,后面为需要回传的参数
callback(@[[NSNull null], params]);
}
BridgeModule.findEvents((error, params) => {
if (error) {
// 错误处理
} else {
// 操作params
}
})
注:此部分功能并未经过官方严格测试,属于试验性质的功能。
Promise
如果原生通信模块定义的方法最后两个参数是RCTPromiseResolveBlock
和RCTPromiseRejectBlock
,则此方法对应的JS方法会返回一个Promise
对象。
RCT_REMAP_METHOD(findEvents, findEventsWithResolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) {
NSArray *results = ...
if (results) {
resolve(results);
} else {
NSError *error = ...
reject(@"Error domain", @"Error message", error);
}
}
async function updateEvents() {
try {
// 原生的CalendarManager的findEvents方法返回的是一个Promise
var events = await CalendarManager.findEvents();
this.setState({events});
} catch (e) {
console.error(e);
}
}
updateEvents();
多线程
React Native在一个独立的串行队列中调用原生方法。可以在原生通信模块中指定该模块中所有的方法执行的队列。
- (dispatch_queue_t)methodQueue {
// 此模块中的所有方法都会在主队列中执行。如非必须,不要这样做
return dispatch_get_main_queue();
}
注:
methodQueue
在通信模块初始化时就会被创建持有,无需做额外引用。多个独立模块之间如果要共用同一个队列,需返回同一个队列对象,而不是创建同名的队列。
导出常量
原生模块可以导出一些常量方便JavaScript随时访问。导出常量的方法仅仅会在初始化时调用一次,不会再随该方法返回内容的变化而改变。
- (NSDictionary *)constantsToExport {
return @{@"firstDayOfTheWeek" : @"Monday"};
}
实现了上述方法的同时需要实现+ requiresMainQueueSetup
来告知React Native是否需要在主线程中初始化该模块。如果模块涉及到依赖调用UIKit
,则需在方法中返回YES
,否则应该总是返回NO
。
+ (BOOL)requiresMainQueueSetup {
return YES; // 仅在模块需要调用到UIKit框架时返回YES
}
定义可在React Native中使用的枚举类型
使用NS_ENUM
定义的枚举类型在没有对RCTConvert
作扩展之前不能作为方法参数。
例如对于枚举类型:
typedef NS_ENUM(NSInteger, UIStatusBarAnimation) {
UIStatusBarAnimationNone,
UIStatusBarAnimationFade,
UIStatusBarAnimationSlide,
};
先编写RCTConvert
的分类对该枚举做转化:
@implementation RCTConvert (StatusBarAnimation)
RCT_ENUM_CONVERTER(UIStatusBarAnimation, (@{@"statusBarAnimationNone" : @(UIStatusBarAnimationNone),
@"statusBarAnimationFade" : @(UIStatusBarAnimationFade),
@"statusBarAnimationSlide" : @(UIStatusBarAnimationSlide)}),
UIStatusBarAnimationNone, integerValue)
@end
然后导出枚举值作为常量:
- (NSDictionary *)constantsToExport {
return @{@"statusBarAnimationNone" : @(UIStatusBarAnimationNone),
@"statusBarAnimationFade" : @(UIStatusBarAnimationFade),
@"statusBarAnimationSlide" : @(UIStatusBarAnimationSlide) };
};
给JavaScript端发送通知
定义一个继承RCTEventEmitter
的类,实现suppportEvents
,通过调用sendEventWithName:
来实现主动向JavaScript端发送通知。
定义原生模块类
#import <React/RCTBridgeModule.h>
#import <React/RCTEventEmitter.h>
@interface SomeNativeModule : RCTEventEmiter <RCTNativeModule>
@property (nonatomic) BOOL hasListeners;
@end
@implementation SomeNativeModule
RCT_EXPORT_MODULE();
- (NSArray<NSString *> *)supportedEvents {
return @[@"event_name_1", @"event_name_2", ...];
}
/// JS端注册第一个监听时的回调
- (void)startObserving {
self.hasListener = YES;
// 在此处注册监听原生的通知,通过通知的回调来向RN发送通知
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(sendMessageToJavaScript:) name:@"RNSendMessageNotification" object:nil];
}
/// JS端最后一个此模块的监听者解除监听或被释放时的回调
- (void)stopObserving {
self.hasListener = NO;
}
- (void)sendMessageToJavaScript:(NSNotification *)notification {
if (self.hasListeners) {
// 发送事件到 React Native
[self sendEventWithName:@"event_name_1" body:@{@"name" : @"someInfo"}];
}
}
@end
JavaScript端注册监听事件通知,并执行相应操作
import { NativeEventEmitter, NativeModules } from 'react-native';
const { SomeNativeModule } = NativeModules;
var emiter = new NativeEventEmitter(SomeNativeModule);
const listener = (reminder) => console.log(reminder.name);
const subscription = emiter.addListener(
'event_name_1',
// 此处的listener不是指这个代码所在的组件,而是下面这个函数。此函数才是listener
(reminder) => console.log(reminder.name)
);
subscription.removeListener('event_name_1', listener); // 取消订阅,一般在组件被移除时执行
在React Native中使用原生视图
在React Native中使用的原生视图组件需要经由RCTViewManager
的子类来创建和管理。每个定义的RCTViewManager
类型在实际运行时都是一个单例,负责创建对应的原生视图,并将其提供给RCTUIManager
,RCTUIManager
则会反过来委托它们在需要的时候去设置和更新视图的属性。RCTViewManager
还会代理视图的所有委托,并给JavaScript发回对应的事件。
如何创建一个原生视图以在React Native中使用:
- 创建
RCTViewManager
的子类 - 添加
RCT_EXPORT_MODULE()
宏标记 - 实现
- (UIView *)view
方法
例如,创建一个使用原生MapView
的组件供React Native使用:
#import <MapKit/MapKit.h>
#import <React/RCTViewManager.h>
@interface RNTMapManager : RCTViewManager
@end
@implementation RNTMapManager
RCT_EXPORT_MODULE(RNTMap)
- (UIView *)view {
// RNTMapView为自定义的MKMapView的子类,详情见下方代码
RNTMapView *mapView = [[RNTMapView allpc] init];
// 由Manager处理MapDelegate的回调,将事件传递到JS端
mapView.delegate = self;
return mapView;
}
@end
注意:不要
-view
方法中为返回的view设置frame或backgroundColor等。为了与JavaScript中的布局属性保持一致,提前设置的属性可能会被覆盖。
React Native中使用
// MapView.js
import { requireNativeComponent } from 'react-native';
export default requireNativeComponent('RNTMap'); // requireNativeComponent 会自动把'RNTMap'解析为'RNTMapManager'
在原生组件中添加JS端使用的属性
添加属性
// 相当于将RNTMap与RNTMapManager做了映射,JS端会自动将'RNTMap'解析为'RNTMapManager'
RCT_EXPORT_MODULE(RNTMap)
// 添加一条BOOL类型的属性,属性名为zoomEnabled
RCT_EXPORT_VIEW_PROPERTY(zoomEnabled, BOOL)
// 添加一条名为region的属性,类型为MKCoordinateRegion。第三个参数是此宏回调时的view的类型
// 此宏会传入json数据和被设置的view实例。当JS端给此属性设值时,会自动调用附加的代码,完成区域的切换
RCT_CUSTOM_VIEW_PROPERTY(region, MKCoordinateRegion, MKMapView) {
[view setRegion:json ? [RCTConvert MKCoordinateRegion:json] : defaultView.region animated:YES];
}
在React Native中使用原生组件的属性
// 相比上述代码,这里额外添加了MapView参数,React Native底层框架将会检查原生属性与此类的属性是否一致
var RNTMap = requireNativeComponent('RNTMap', MapView);
export default class MapView extends React.Component {
render() {
return <RNTMap
{...this.props}
// 设置region
region={region}
zoomEnabled={false}
style={{ flex: 0.8 }}
/>;
}
}
// React特性:定义MapView属性接口详情
MapView.propTypes = {
zoomEnabled: PropTypes.bool,
region: PropTypes.shape({
latitude: PropTypes.number.isRequired,
longitude: PropTypes.number.isRequired,
latitudeDelta: PropTypes.number.isRequired,
longitudeDelta: PropTypes.number.isRequired,
})
}
处理原生视图的响应事件
在原生视图中定义响应事件的Block属性,在其Manager类中导出该属性,通过JS端为该属性赋值函数,再在合适的地方调用该Block并将参数传递到JS端,即可实现JS端处理原生事件。
例如:
自定义RNTMapView
@interface RNTMapView : MKMapView
@property (nonatomic, copy) RCTBubblingEventBlock onRegionChange;
@end
@implementation RNTMapView
@end
注:所有的
RCTBubblingEventBlock
类型属性都必须以on
开头,并在该view所使用的Manager类中导出。
在Manager类中
RCT_EXPORT_VIEW_PROPERTY(onRegionChange, RCTBubblingEventBlock)
/// MapView的代理回调
- (void)mapView:(RNTMapView *)mapView regionDidChangeAnimated:(BOOL)animated {
if (!mapView.onRegionChange) {
return;
}
MKCoordinateRegion region = mapView.region;
// 调用自定义mapView的block属性(实际是调用JS端的函数),将值传到JS端
mapView.onRegionChange(@{
@"region": @{
@"latitude": @(region.center.latitude),
@"longitude": @(region.center.longitude),
@"latitudeDelta": @(region.span.latitudeDelta),
@"longitudeDelta": @(region.span.longitudeDelta),
}
});
}
在JS端
render() {
return (
<RNTMap
{...this.props}
style={{ flex: 1 }}
onRegionChange={ event => { </* do sth. with event.region... */> } }
/>
);
}