美文网首页RN快速入门iOSRN
React Native混合开发(iOS)下的数据交互

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

作者: 滴嗒嗒 | 来源:发表于2017-07-25 14:38 被阅读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端对接。

  • 1.该类需要继承自 RCTEventEmitter ,并且需要向RN端那边导出自己:

      #import "RCTEventEmitter.h"  
    
      @interface CallRNTest : RCTEventEmitter<RCTBridgeModule>
      @end
    
  • 2.然后在 .m 文件中,在子类中为父类 RCTEventEmitter 的 bridge 生成 set/get方法,并使用用于导出模块的宏。

      @implementation CallRNTest
    
      @synthesize bridge = _bridge;
    
      RCT_EXPORT_MODULE();
      
      @end
    

    假如不写第二句bridge的代码,在使用时会报没有设置bridge的错误:

    <font color=#DC143C>*** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'bridge is not set. </font>

  • 3.导出所有需要传递的方法的名字

      (NSArray<NSString *> *)supportedEvents{
          return @[@"callRn"];
      }
    
  • 4.你可以在Native端实现在 supportedEvents 中定义的方法的同名方法,便于 区分理解Native端代码,也方便使用者调用。当然你也可以不这么做,反正最终都是使用 sendEventWithName 来进行真正的调用的。

      -(void)nativeCallRn:(NSString*)code result:(NSString*) result
      {
          [self sendEventWithName:@"callRn"
                     body:@{
                            @"code": code,
                            @"result": result,
                            }];
      }
    
  • 5.在 JS 端导出

      import { ...  NativeModules,  NativeEventEmitter} from 'react-native';  
      
      var CallRNTest = NativeModules.CallRNTest;
      const myNativeEvt = new NativeEventEmitter(CallRNTest); 
    
  • 6.在 JS 端绑定

      //在组件的生命周期中绑定与解绑
      componentWillMount() {
      //对应原生端的名字
      this.listener = myNativeEvt.addListener('callRn', this.callRn.bind(this));  
      }
    
      componentWillUnmount() {
      this.listener && this.listener.remove();  //记得remove哦
      this.listener = null;
      }
    
  • 7.在 JS 端实现绑定的方法

      //接受原生传过来的数据 data={code:,result:}
      callRn(data) {
          console.warn(data.code, data.result);
      }
    
  • 8.在 Native 端合适的时机调用,结束啦~

      [self nativeCallRn:@"200" result:@"OC call 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 版本环境下的。

参考

相关文章

网友评论

本文标题:React Native混合开发(iOS)下的数据交互

本文链接:https://www.haomeiwen.com/subject/gobwkxtx.html