React Native混合開發(fā)(iOS)下的數(shù)據(jù)交互

React Native
React Native

導(dǎo)語

React Native是一套由 Facebook 開源的跨平臺、動態(tài)更新的 Javascript 框架,其主張 “Learn once, write anywhere”,即學(xué)會一次 React,就可以編寫所支持的兩大移動平臺(iOS,Android)的應(yīng)用。通過結(jié)合 Web 端和 Native 端的開發(fā)優(yōu)勢,可以使用 JavaScript 來開發(fā) iOS 和 Android 原生應(yīng)用,并使用戶獲得和原生應(yīng)用一致的順暢 UI 體驗。

1. ReactNative 概要

1.1 ReactNative 結(jié)構(gòu)

ReactNative 這個單詞能拆成兩部分:React 和 Native,除此之外,還有一個連接 React 和 Native 的 Bridge 橋梁。

  • React

    React 代表的是前端框架 React.JS,整個RN框架的JS代碼部分,就是React.JS,所有這個框架的特點,完全都可以在RN中使用。我們知道RN采用了 React 和 ES6 的語法,因為要想學(xué)習(xí)RN,對于這些語法的了解是必不可少的。

    關(guān)于 Javascript 的基礎(chǔ)語法 ,推薦 W3School 里的 JS語法 ;在此之上,關(guān)于 React ,推薦阮一峰寫的 React 入門實例教程;關(guān)于 ES6, 推薦這篇博客進行了解,中文的比較好理解,當(dāng)然也可以翻看這里。

  • Native

    顧名思義,使用RN開發(fā)出的應(yīng)用具有原生的UI體驗,其實質(zhì)調(diào)用的也是純原生的UI組件。React Native可以同時支持對 iOS、Android 兩端的原生模塊的調(diào)用。

  • Bridge

    作為一個專注 View 層的一個前端框架,React.JS 會計算每個頁面元素的位置大小并把數(shù)據(jù)傳遞給瀏覽器,讓瀏覽器進行渲染。但是在RN中,這些UI數(shù)據(jù)傳輸?shù)哪康牡夭辉偈菫g覽器了,而是通過一個 JS/OC 的橋梁,去映射成原生下的 View 的初始化布局方法,以此用原生的方式渲染出了界面,相當(dāng)于用 React.JS 繪制出了一個 native 的 View。

    當(dāng)用戶在這個 View 上發(fā)生了觸摸點擊等事件時,也是通過一個 OC/JS 的橋梁去將事件傳遞回 React.JS 的事件處理方法中進行處理。

    這樣 React.JS 還是那個 React.JS ,他的使用方法沒發(fā)生變化,但是卻獲得了原生 native 的體驗。

1.2 ReactNative 優(yōu)勢

  • 跨平臺+代碼復(fù)用

    React Native 可以支持 iOS、Android 兩大平臺。一般而言,同一款產(chǎn)品下的 Android 和 iOS 兩端除 UI 有些許不同外,多數(shù)業(yè)務(wù)邏輯幾乎完全一致,因此在RN下,iOS、Android 兩大平臺下能夠復(fù)用絕大部分代碼。

    Instagram 的官博 React Native at Instagram 一文中提到,利用RN開發(fā)的 feature 可以實現(xiàn) 85% - 99% 的代碼復(fù)用率。這意味著利用RN進行開發(fā)的產(chǎn)品,我們可以用更少的人力成本來達到相同的效果。

  • 動態(tài)更新

    App的發(fā)布時,React等一系列資源會被打包成 js bundle 文件置于App安裝包中。App啟動時系統(tǒng)會加載 js bundle 文件,解析并渲染出來。所以,React Native 熱更新的根本原理就是從服務(wù)器請求新的 js bundle 文件來更換本地舊的 js bundle 文件,并重新加載,新的內(nèi)容就完美的展示出來了。

  • 原生UI體驗

    React Native本質(zhì)上還是調(diào)用的原生模塊進行 UI 操作,所以用戶體驗也是和原生一致的。

  • 開發(fā)效率高

    對于熟練的 React Native 使用者來說,使用前端框架 React 來進行復(fù)雜頁面布局的效率會大幅優(yōu)于原生開發(fā)。并且使用 RN 在開發(fā)時,UI是實時熱更新的,可以節(jié)省掉代碼修改之后的編譯用時,進一步提升效率。

2. 原生項目集成ReactNative

基于現(xiàn)有原生項目的規(guī)模,將現(xiàn)有的項目完全切換到 React Native 上對于絕大部分公司來說都是極為困難的。那么將 React Native 集成到現(xiàn)有項目中,用其來開發(fā)一些變化性較大的業(yè)務(wù)頁面,這不失為一種良好的策略。

在 RN 的官方文檔里有一節(jié) Integration with Existing Apps ,當(dāng)然也有中文版的, 只需要按照一步步做即可。不過在集成的過程中可能會遇到一些問題,下面就提提我遇到的一些問題(React Native 0.45):

  • 錯誤:'jschelpers/JavaScriptCore.h' file not found

    Podfile文件中,需要添加 BatchedBridge ,如下:

      pod 'React', :path => '../node_modules/react-native', :subspecs => [
      'Core'
      ’BatchedBridge']
    

path是相對于Podfile文件的相對路徑。

  • warning :Native component for “RCTImageView” does not exist

    同樣是需要引入 RCTImage 模塊解決

      pod 'React', :path => '../node_modules/react-native', :subspecs => [
      'Core',
      'RCTImage']
    

3. 原生和RN混合開發(fā)中的交互

3.1 原生加載RN

首先自然是來寫一個Hello World的例子吧~

經(jīng)過上一步的在原生項目中集成了RN之后,項目中創(chuàng)建了 index.ios.js 的js文件,這是RN中iOS的js端入口文件,我們可以在里邊添加代碼如下:

import React, { Component } from 'react';
import { AppRegistry, View, Text} from 'react-native';

class HelloWorldCp extends Component {
    render() {
        return (
            <View style={{flex:1 , justifyContent: 'center', alignItems: 'center'}}>
              <Text>Hello world!</Text>
            </View>
        );
    }
}

AppRegistry.registerComponent('HelloWorldCp', () => HelloWorldCp);

在這里,我們創(chuàng)建了一個 HelloWorldCp 的React組件,并用 AppRegistry.registerComponent 注冊了該組件,這樣原生系統(tǒng)才可以使用該組件。在組件里,我們在默認的 render() 方法中輸出了默認的view,view下包含了一個 "Hello World!" 的標(biāo)簽。對于view,設(shè)置了一個style,大意是將view下的 標(biāo)簽置于View的中央。

render 是 Component 默認的輸出 UI 的方法。當(dāng)你想制造出一個組件時,你繼承 Component 并實現(xiàn)自定義的 render() 方法,在里邊返回你想展現(xiàn)的UI,這就是自定義組件的創(chuàng)造方法。

接下來的事情便是在原生UI中將這個組件顯示出來,我們需要用到React容器類RCTRootView。

NSURL *jsCodeLocation = [NSURL URLWithString:@"http://localhost:8081/index.ios.bundle?platform=ios"];
RCTRootView *rootView = [[RCTRootView alloc] initWithBundleURL:jsCodeLocation
                                                    moduleName:@"HelloWorldCp"
                                             initialProperties:nil
                                                 launchOptions:nil];
UIViewController *vc = [[UIViewController alloc] init];
vc.view = rootView;
[self.navigationController pushViewController:vc animated:YES];

看代碼可知,首先我們初始化了一個 NSURL 對象,它指向本地 JS 的調(diào)試服務(wù)地址,以供 RCTRootView 初始化時使用。RCTRootView 用來承載 JS 特定的組件,在原生下可以當(dāng)做普通的 UIView 來進行處理,如添加到 superview,設(shè)置frame等操作。初始化時第一個參數(shù)為 JS 文件的服務(wù)器地址,moduleName 是 React 中注冊好的組件, initialProperties 接收一個字典,用來傳遞參數(shù)給 JS,最后一個則是啟動項參數(shù)。在這里,我們加載了上面創(chuàng)建的 HelloWorldCp 組件。最后將初始化的 RCTRootView 設(shè)置成新頁面的根view并展示。

運行工程之前,我們需要先啟動本地 js 服務(wù)

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

接著直接用xcode運行工程即可,假如沒其他問題的,那么運行效果如下:

Hello World

3.2 初始化RCTRootView的數(shù)據(jù)傳遞

上文提到在 RCTRootView 初始化的時候可以進行參數(shù)的傳遞,那么參數(shù)是如何被接收處理的呢?下面直接看代碼:

 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 的例子,這里初始化了一個字典,存儲了一些名字及對應(yīng)的分數(shù),并在 RCTRootView 初始化的時候作為 initialProperties 的參數(shù)進行傳遞。

在 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 參數(shù)中讀取字段的 key 獲取對應(yīng)的數(shù)據(jù) Array,并通過 map 方法將其每一個數(shù)據(jù)單項映射成顯示數(shù)據(jù)的標(biāo)簽,最后將標(biāo)簽列表置于View中返回。其中,對于變量 contents,我們需要用 {} 將其嵌入到 JSX 語句中。

props 即是 React 組件的屬性,是一種父級向子級傳遞數(shù)據(jù)的方式。上面讀取屬性的代碼也可以寫成:this.props.scores。顯然,通過 initialProperties 傳遞過來的字典變成了 React 組件的屬性,可直接讀取使用。但是 props 對于組件本身來說是不可變的,只能經(jīng)由父組件傳遞更新。

我們還設(shè)置了 view 的 style,這里將 style 整體定義成變量初始后傳遞給view,借以保持代碼的清晰整潔。

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

運行結(jié)果如下:

Param Pass

除了在初始化 RCTRootView 的時候可以傳遞參數(shù),OC還可以用更新的方式傳遞數(shù)據(jù)給 JS 組件,修改這個屬性,JS端會調(diào)用相應(yīng)的渲染方法。

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

這兩種傳遞數(shù)據(jù)的方式是 OC 向 JS 傳遞數(shù)據(jù)的主要方式。

3.3 RN調(diào)用原生方法

RN向OC傳遞數(shù)據(jù)的主要形式之一便是通過在調(diào)用原生方法的時候傳遞參數(shù)。再而也為了讓React Native可以利用現(xiàn)有原生龐大的組件資源,React Native在設(shè)計之初就考慮到了讓React Native可以方便的調(diào)用Native端的方法。

3.3.1 支持調(diào)用的步驟

要想讓iOS類內(nèi)的方法能夠被RN調(diào)用,類比RN端的組件注冊,iOS端同樣需要注冊該類。首先便需要原生類實現(xiàn)協(xié)議:RCTBridgeModule,實現(xiàn)該協(xié)議的類,會自動注冊到Object-C對應(yīng)的Bridge中。所以定義可以讓RN調(diào)用的類可以這樣寫

#import "RCTBridgeModule.h"

@interface RNIOSLog : NSObject<RCTBridgeModule>

@end

所有實現(xiàn) RCTBridgeModule 的類都必須顯示的使用宏命令:

@implementation RNIOSLog

RCT_EXPORT_MODULE();

@end

該宏的作用是:自動為該類注冊為JS端的模塊,當(dāng)Object-c Bridge加載的時候。這個類注冊的模塊可以被JavaScript Bridge調(diào)用。當(dāng)然該宏可以接受一個參數(shù)作為注冊的模塊名,默認值是該類的名稱。

注冊完模塊之后,還需要注冊模塊下需要暴露給JS的方法。此外,暴露出的方法返回值必須為void。

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

原生的模塊方法注冊好之后,JS端該如何引用該類呢?

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

引入到JS模塊下之后,便可直接調(diào)用。

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 調(diào)用 OC 的效果。同樣的,我們初始化 RCTRootView 并設(shè)置為新頁面的根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調(diào)用OC的回調(diào)

對于OC暴露給RN的方法,要求不能有返回值。但是在很多應(yīng)用場景下,我們也需要對調(diào)用之后的返回值進行相應(yīng)的處理,這樣就需要使用回調(diào)方法來對結(jié)果進行處理。在RN中專門定義了一個用于回調(diào)的參數(shù) RCTReponseSenderBlock。

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

它接收了一個叫做 response 的 NSArray 的參數(shù),其中 response[0] 代表著錯誤信息error,如果沒有錯誤則傳入null,即[NSNull null],后面的參數(shù)傳入自定義的內(nèi)容。

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

在RN中,是這樣調(diào)用Native方法并處理回調(diào)的:

_logCallback() {
    RNIOSLog.showWithCallback(function (err, data){
        if (err) {
            console.warn(err, data);
        } else {
            console.warn(data,'無錯回調(diào)');
        }
    });
}

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

之后便是同樣的 RN 頁面展示方法,初始化 RCTRootView 并設(shè)置為新頁面的根view,并push出來顯示。運行之后我們每次點擊 RN 頁面上的按鈕標(biāo)簽都能看到RN調(diào)用Native端的回調(diào)log,運行效果如下圖:

callback

3.3.3 RN調(diào)用OC時的線程問題

JavaScript 代碼都是單線程運行的,而調(diào)用到Native模塊時都是默認運行在各自獨立的線程上,所以可知RN調(diào)用Native的時候都是異步的。因此若是調(diào)用的Native方法有需要操作UI的,必須指定在主線程中運行,否則會出現(xiàn)一些莫名其妙的問題。比如RN調(diào)用的Native方法里需要彈出原生的 UIAlertView ,則可以在操作 UIAlertView 的時候用 GCD 切換到主線程:

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

此外,如果需要對整個導(dǎo)出的類都指定到某個特定的線程中去運行,那么在每個導(dǎo)出的方法里用 GCD 的方式去切換線程會顯得很繁瑣,則可以在類中實現(xiàn) methodQueue 方法:

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

只要實現(xiàn)了該方法并返回了特定的線程,那么該類下所有的方法在被RN調(diào)用時都會自覺的運行在該方法指定的線程下。

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;

對于第二種創(chuàng)建方式(initWithBundleURL),其會在每次調(diào)用時在方法內(nèi)部創(chuàng)建一個 RCTBridge,且多個不同 RCTRootView 并不能共享 RCTBridge,這比較耗費時間和資源。因此對于一個半RN半native的應(yīng)用的應(yīng)用來說,最好還是使用第一種方式(initWithBridge)初始化 RCTRootView。

對于 initWithBridge 的方式初始化 RCTRootView,首先需要初始化一個 RCTBridge并保存,以便在需要的時候使用。在此之前,類本身需要實現(xiàn) RCTBridgeDelegate 協(xié)議,

@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

在協(xié)議方法 sourceURLForBridge 中,返回 RN 模塊地址。然后便可以初始化我們的bridge,

//使用保留的 RCTBridge 初始化 RCTRootView 更節(jié)省資源,不用每次初始化bridge
_bridge = [[RCTBridge alloc] initWithDelegate:self launchOptions:nil];

最后便可以到處使用該 bridge 初始化 RCTRootView了,這樣能有效的節(jié)省每次初始化 bridge 的時間和資源耗費。

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

3.4 原生調(diào)用RN方法

現(xiàn)在,我們已經(jīng)知道了在RN中該怎么直接調(diào)用OC中的方法,那么OC該如何主動的去調(diào)用 RN方法呢?

在以前的RN版本中,可以使用 sendDeviceEventWithName:body: 的方式來將調(diào)用請求發(fā)送到JS端,JS端用 addListener 的方式監(jiān)聽對應(yīng)的關(guān)鍵字并實現(xiàn)方法即可實現(xiàn)OC調(diào)用RN方法。但是隨著RN版本的更新,當(dāng)繼續(xù)使用這種互動方式的時候,在xcode下會出現(xiàn)警告:

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

適應(yīng)新的Api調(diào)用方式,讓我們開始用起 RCTEventEmitter 來,其基本對接步驟是一致的。我們可以定義一個專門用來調(diào)用RN方法的類,在不影響其他原生模塊的條件下方便和RN端對接。

  • 1.該類需要繼承自 RCTEventEmitter ,并且需要向RN端那邊導(dǎo)出自己:

      #import "RCTEventEmitter.h"  
    
      @interface CallRNTest : RCTEventEmitter<RCTBridgeModule>
      @end
    
  • 2.然后在 .m 文件中,在子類中為父類 RCTEventEmitter 的 bridge 生成 set/get方法,并使用用于導(dǎo)出模塊的宏。

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

    假如不寫第二句bridge的代碼,在使用時會報沒有設(shè)置bridge的錯誤:

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

  • 3.導(dǎo)出所有需要傳遞的方法的名字

      (NSArray<NSString *> *)supportedEvents{
          return @[@"callRn"];
      }
    
  • 4.你可以在Native端實現(xiàn)在 supportedEvents 中定義的方法的同名方法,便于 區(qū)分理解Native端代碼,也方便使用者調(diào)用。當(dāng)然你也可以不這么做,反正最終都是使用 sendEventWithName 來進行真正的調(diào)用的。

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

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

      //在組件的生命周期中綁定與解綁
      componentWillMount() {
      //對應(yīng)原生端的名字
      this.listener = myNativeEvt.addListener('callRn', this.callRn.bind(this));  
      }
    
      componentWillUnmount() {
      this.listener && this.listener.remove();  //記得remove哦
      this.listener = null;
      }
    
  • 7.在 JS 端實現(xiàn)綁定的方法

      //接受原生傳過來的數(shù)據(jù) data={code:,result:}
      callRn(data) {
          console.warn(data.code, data.result);
      }
    
  • 8.在 Native 端合適的時機調(diào)用,結(jié)束啦~

      [self nativeCallRn:@"200" result:@"OC call Rn"];
    

4.0 Demo Project

寫了一個 Demo Project:

https://github.com/xzr123/LittleReactNativeDemo

如果你想試一試運行工程并且還沒有安裝好 React Native 開發(fā)環(huán)境,先看這個官方文檔配置環(huán)境是個不錯的選擇。

之后,用別忘了啟動 RN 本地調(diào)試服務(wù)器

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

接著用Xcode打開項目工程看看運行效果吧。該Demo是基于 React Native 0.45 版本環(huán)境下的。

參考

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容