RN快速入门iOSRN

React Native混合开发(iOS)下的数据交互

2017-07-25  本文已影响1492人  滴嗒嗒
Hello World

3.2 初始化RCTRootView的数据传递

上文提到在 RCTRootView 初始化的时候可以进行参数的传递,那么参数是如何被接收处理的呢?下面直接看代码:

 NSDictionary *param = @{@"scores" :@[
                                     @{@"name" : @"Alex",@"value": @"42"},
                                     @{@"name" : @"Joel",@"value": @"10"},
                                     @{@"name" : @"Zona",@"value": @"20"}
                                    ]
                        };

NSURL *jsCodeLocation = [NSURL URLWithString:@"http://localhost:8081/index.ios.bundle?platform=ios"];
RCTRootView *rootView = [[RCTRootView alloc] initWithBundleURL:jsCodeLocation
                                                    moduleName:@"ParamPassCp"
                                             initialProperties:param
                                                 launchOptions:nil];

UIViewController *vc = [[UIViewController alloc] init];
vc.view = rootView;
[self.navigationController pushViewController:vc animated:YES];

相比于上面 Hello World 的例子,这里初始化了一个字典,存储了一些名字及对应的分数,并在 RCTRootView 初始化的时候作为 initialProperties 的参数进行传递。

在 JS 端是如何接收的呢?

class ParamPassCp extends React.Component {
    render() {
         var contents = this.props["scores"].map(
           score => <Text key={score.name}>{score.name}:{score.value}{"\n"}</Text>
         );
        return (
            <View style={styles.container}>
                <Text style={styles.highScoresTitle}>
                    {contents}
                </Text>
            </View>
            );
    }
}

同样是在 render() 方法中,我们直接从 props 参数中读取字段的 key 获取对应的数据 Array,并通过 map 方法将其每一个数据单项映射成显示数据的标签,最后将标签列表置于View中返回。其中,对于变量 contents,我们需要用 {} 将其嵌入到 JSX 语句中。

props 即是 React 组件的属性,是一种父级向子级传递数据的方式。上面读取属性的代码也可以写成:this.props.scores。显然,通过 initialProperties 传递过来的字典变成了 React 组件的属性,可直接读取使用。但是 props 对于组件本身来说是不可变的,只能经由父组件传递更新。

我们还设置了 view 的 style,这里将 style 整体定义成变量初始后传递给view,借以保持代码的清晰整洁。

const styles = StyleSheet.create({
     container: {
         flex: 1,
         justifyContent: 'center',
         alignItems: 'center',
         backgroundColor: '#FFFFFF',
     },
     highScoresTitle: {
         fontSize: 20,
         textAlign: 'center',
         margin: 10,
     }
 });

运行结果如下:

Param Pass

除了在初始化 RCTRootView 的时候可以传递参数,OC还可以用更新的方式传递数据给 JS 组件,修改这个属性,JS端会调用相应的渲染方法。

_rootView.appProperties = @{@"scores" :@[
                                        @{@"name" : @"Alex",@"value": @"42"},
                                        @{@"name" : @"Joel",@"value": @"10"},
                                        @{@"name" : @"Zona",@"value": [NSString stringWithFormat:@"%ld",(long)_score++]}
                                        ]
                                };

这两种传递数据的方式是 OC 向 JS 传递数据的主要方式。

3.3 RN调用原生方法

RN向OC传递数据的主要形式之一便是通过在调用原生方法的时候传递参数。再而也为了让React Native可以利用现有原生庞大的组件资源,React Native在设计之初就考虑到了让React Native可以方便的调用Native端的方法。

3.3.1 支持调用的步骤

要想让iOS类内的方法能够被RN调用,类比RN端的组件注册,iOS端同样需要注册该类。首先便需要原生类实现协议:RCTBridgeModule,实现该协议的类,会自动注册到Object-C对应的Bridge中。所以定义可以让RN调用的类可以这样写

#import "RCTBridgeModule.h"

@interface RNIOSLog : NSObject<RCTBridgeModule>

@end

所有实现 RCTBridgeModule 的类都必须显示的使用宏命令:

@implementation RNIOSLog

RCT_EXPORT_MODULE();

@end

该宏的作用是:自动为该类注册为JS端的模块,当Object-c Bridge加载的时候。这个类注册的模块可以被JavaScript Bridge调用。当然该宏可以接受一个参数作为注册的模块名,默认值是该类的名称。

注册完模块之后,还需要注册模块下需要暴露给JS的方法。此外,暴露出的方法返回值必须为void。

RCT_EXPORT_METHOD(show:(NSString *)msg){
    NSLog(@"msg:%@",msg);
}

原生的模块方法注册好之后,JS端该如何引用该类呢?

import {NativeModules} from "react-native";
var RNIOSLog = NativeModules.RNIOSLog;

引入到JS模块下之后,便可直接调用。

class RNLogCp extends Component {
render() {
    return (
            <View style={styles.container}>
            
                <TouchableHighlight onPress={()=>RNIOSLog.show('from react native')}
                                    style={styles.btn}>
                        <Text>showLog</Text>
                        
                </TouchableHighlight>
                
            </View>
            );
         }
}

在RN中,TouchableXXX就表示是按钮控件。TouchableHighlight在点击的时候,该控件会高亮显示。此外还有TouchableOpacity,TouchableNativeFeedback 和TouchableWithoutFeedback。

到这一步之后,便是让 RN 页面展示出来,点击 RN 组件上的按钮便可看到 RN 调用 OC 的效果。同样的,我们初始化 RCTRootView 并设置为新页面的根view,并push出来显示。

NSURL *jsCodeLocation = [NSURL URLWithString:@"http://localhost:8081/index.ios.bundle?platform=ios"];
RCTRootView *rootView = [[RCTRootView alloc] initWithBundleURL:jsCodeLocation
                                                    moduleName:@"RNLogCp"
                                             initialProperties:nil
                                                 launchOptions:nil];

UIViewController *vc = [[UIViewController alloc] init];
vc.view = rootView;
[self.navigationController pushViewController:vc animated:YES];

3.3.2 RN调用OC的回调

对于OC暴露给RN的方法,要求不能有返回值。但是在很多应用场景下,我们也需要对调用之后的返回值进行相应的处理,这样就需要使用回调方法来对结果进行处理。在RN中专门定义了一个用于回调的参数 RCTReponseSenderBlock。

typedef void (^RCTResponseSenderBlock)(NSArray *response);

它接收了一个叫做 response 的 NSArray 的参数,其中 response[0] 代表着错误信息error,如果没有错误则传入null,即[NSNull null],后面的参数传入自定义的内容。

RCT_EXPORT_METHOD(showWithCallback:(RCTResponseSenderBlock)callback){
    //do something you want
    
    //callback(@"error",@"something is wrong");
    callback(@[[NSNull null],@"call back from native"]);
}

在RN中,是这样调用Native方法并处理回调的:

_logCallback() {
    RNIOSLog.showWithCallback(function (err, data){
        if (err) {
            console.warn(err, data);
        } else {
            console.warn(data,'无错回调');
        }
    });
}

<TouchableHighlight onPress={()=>this._logCallback()}>
    <Text>showLogCallback</Text>
</TouchableHighlight>

之后便是同样的 RN 页面展示方法,初始化 RCTRootView 并设置为新页面的根view,并push出来显示。运行之后我们每次点击 RN 页面上的按钮标签都能看到RN调用Native端的回调log,运行效果如下图:

callback

3.3.3 RN调用OC时的线程问题

JavaScript 代码都是单线程运行的,而调用到Native模块时都是默认运行在各自独立的线程上,所以可知RN调用Native的时候都是异步的。因此若是调用的Native方法有需要操作UI的,必须指定在主线程中运行,否则会出现一些莫名其妙的问题。比如RN调用的Native方法里需要弹出原生的 UIAlertView ,则可以在操作 UIAlertView 的时候用 GCD 切换到主线程:

 dispatch_async(dispatch_get_main_queue(), ^{
    //操作UI
});

此外,如果需要对整个导出的类都指定到某个特定的线程中去运行,那么在每个导出的方法里用 GCD 的方式去切换线程会显得很繁琐,则可以在类中实现 methodQueue 方法:

- (dispatch_queue_t)methodQueue{
  return dispatch_get_main_queue();
}

只要实现了该方法并返回了特定的线程,那么该类下所有的方法在被RN调用时都会自觉的运行在该方法指定的线程下。

3.3.4 bridge资源问题

对于 RCTRootView 官方提供了两种初始化方式

- (instancetype)initWithBridge:(RCTBridge *)bridge
                moduleName:(NSString *)moduleName
         initialProperties:(NSDictionary *)initialProperties;

- (instancetype)initWithBundleURL:(NSURL *)bundleURL
                   moduleName:(NSString *)moduleName
            initialProperties:(NSDictionary *)initialProperties
                launchOptions:(NSDictionary *)launchOptions;

对于第二种创建方式(initWithBundleURL),其会在每次调用时在方法内部创建一个 RCTBridge,且多个不同 RCTRootView 并不能共享 RCTBridge,这比较耗费时间和资源。因此对于一个半RN半native的应用的应用来说,最好还是使用第一种方式(initWithBridge)初始化 RCTRootView。

对于 initWithBridge 的方式初始化 RCTRootView,首先需要初始化一个 RCTBridge并保存,以便在需要的时候使用。在此之前,类本身需要实现 RCTBridgeDelegate 协议,

@interface ViewController ()<RCTBridgeDelegate>

@property (nonatomic, strong) RCTBridge *bridge;

@end

@implementation ViewController

- (NSURL *)sourceURLForBridge:(RCTBridge *)bridge {
    return [NSURL URLWithString:@"http://localhost:8081/index.ios.bundle?platform=ios"];
}
@end

在协议方法 sourceURLForBridge 中,返回 RN 模块地址。然后便可以初始化我们的bridge,

//使用保留的 RCTBridge 初始化 RCTRootView 更节省资源,不用每次初始化bridge
_bridge = [[RCTBridge alloc] initWithDelegate:self launchOptions:nil];

最后便可以到处使用该 bridge 初始化 RCTRootView了,这样能有效的节省每次初始化 bridge 的时间和资源耗费。

RCTRootView *rootView = [[RCTRootView alloc] initWithBridge:_bridge
                                                 moduleName:@"HelloWorldCp"
                                          initialProperties:nil];

3.4 原生调用RN方法

现在,我们已经知道了在RN中该怎么直接调用OC中的方法,那么OC该如何主动的去调用 RN方法呢?

在以前的RN版本中,可以使用 sendDeviceEventWithName:body: 的方式来将调用请求发送到JS端,JS端用 addListener 的方式监听对应的关键字并实现方法即可实现OC调用RN方法。但是随着RN版本的更新,当继续使用这种互动方式的时候,在xcode下会出现警告:

<font color=#DC143C>'sendDeviceEventWithName:body:' is deprecated: Subclass RCTEventEmitter instead</font>

适应新的Api调用方式,让我们开始用起 RCTEventEmitter 来,其基本对接步骤是一致的。我们可以定义一个专门用来调用RN方法的类,在不影响其他原生模块的条件下方便和RN端对接。

4.0 Demo Project

写了一个 Demo Project:

https://github.com/xzr123/LittleReactNativeDemo

如果你想试一试运行工程并且还没有安装好 React Native 开发环境,先看这个官方文档配置环境是个不错的选择。

之后,用别忘了启动 RN 本地调试服务器

#cd 到‘node_modules’文件所在目录,然后
npm start

接着用Xcode打开项目工程看看运行效果吧。该Demo是基于 React Native 0.45 版本环境下的。

参考

上一篇 下一篇

猜你喜欢

热点阅读