7. React Native项目集成与通信

1,550 阅读10分钟

前置准备

  1. 安装CocoaPods
  2. 安装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

如果原生通信模块定义的方法最后两个参数是RCTPromiseResolveBlockRCTPromiseRejectBlock,则此方法对应的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类型在实际运行时都是一个单例,负责创建对应的原生视图,并将其提供给RCTUIManagerRCTUIManager则会反过来委托它们在需要的时候去设置和更新视图的属性。RCTViewManager还会代理视图的所有委托,并给JavaScript发回对应的事件。

如何创建一个原生视图以在React Native中使用:

  1. 创建RCTViewManager的子类
  2. 添加RCT_EXPORT_MODULE()宏标记
  3. 实现- (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... */> } }
        />
    );
}