混合開發(fā):原生調用RN頁面或組件


前言


RN和iOS混合開發(fā)的幾種場景。

  • 原生項目中,調用部分RN頁面。
  • 原生頁面中,調用部分RN組件。
  • RN項目中,調用部分原生頁面。
  • RN頁面中,調用部分原生View。
  • RN項目中,調用部分原生模塊。

場景一和場景二其實是一樣的,因為在RN看來,頁面和組件在廣義上都是組件,對應于原生里的View。

場景三和場景四是一樣的,因為無論RN要調用原生的頁面還是View,我們最終都是把原生的View交給它調用。還是那句話,RN那邊的組件對應原生里的View,而沒法對應ViewController。

場景五和場景三、場景四的區(qū)別在于,RN調用原生頁面或View是指調用原生視圖層面的東西來做UI布局的,而RN調用原生模塊是指調用原生功能層面的東西來實現某個功能(例如調用日歷、通訊錄等模塊,調用分享、三方登錄、支付等三方SDK,調用我們自己的某些功能代碼塊,等等)。

這一篇我們先講解原生調用RN頁面或組件(場景一和場景二)的詳細開發(fā)步驟,下一篇講解RN調用原生頁面或View(場景三和場景四)的詳細開發(fā)步驟,下下一篇講解RN調用原生模塊(場景五)的詳細開發(fā)步驟。


示例:原生調用RN頁面或組件(場景一和場景二)的詳細開發(fā)步驟


該示例實現的是:原生的ViewController里直接調用RN的TextAndImageView組件,點擊跳轉后調用RN的Page1界面。

其實很簡單的,無非就是RN那邊寫好界面和組件,原生這邊創(chuàng)建相應的ViewControllerRCTRootView來包裹一下RN那邊的界面和組件,這樣原生就可以成功調用RN那邊的界面和組件了。當然這中間要用CocoaPods來為原生項目添加并管理一些RN的依賴庫——這樣原生項目和RN項目之間才能建立連接。

第一步:創(chuàng)建一個原生項目

我們這里創(chuàng)建一個原生項目,名字叫作HybridApp

第二步:創(chuàng)建一個RN項目

我們這里創(chuàng)建一個RN項目,名字叫作 HybridApp_RN,用來編寫HybridAppRN部分的代碼。

創(chuàng)建好后,項目的目錄結構如下。

但因為這個RN項目是用來輔助我們自己的HybridApp項目開發(fā)的,所以它里面不能是默認的那個androidios項目,我們要刪掉它倆,替換成我們自己的iOS項目和安卓項目(比如這里我們會把上一步創(chuàng)建的HybridApp拖進去,安卓項目也是同理)。

刪除并替換后,項目的目錄結構如下。

第三步:建立原生項目和RN項目之間的連接——通過CocoaPods來為原生項目添加并管理一些RN的依賴庫

其實最終我們是把RN項目打成JS包,下載下來放到原生項目中使用的。而我們推薦的方式就是使用CocoaPods來完成RN項目代碼的下載,并且還用它來為我們的原生項目添加并管理一些RN的依賴庫(即使用RN的哪些組件)。

因此接下來,我們在原生項目中創(chuàng)建一個Podfile,為原生項目添加需要的RN依賴庫。

我們在決定要進行混合開發(fā)的時候,其實已經知道了具體要使用RN的哪些組件。因此在創(chuàng)建Podfile文件的時候,也就知道了具體要為原生項目添加哪些RN依賴庫。比如,一般來說你首先需要添加Core,這一依賴庫包含了必須的AppRegistry、StyleSheet、View以及其他的一些RN的核心庫;如果你想使用RN的Text庫(即<Text>組件),那就需要添加RCTText依賴庫;如果你想使用RN的Image庫(即<Image>組件),那就需要添加RCTImage依賴庫,等等。

我們可以按下面的格式及內容,根據自己的實際需求作調整后,復制到Podfile文件里。(注意如果出錯的話,一般是路徑問題,可以在RN項目的node_modules/react-native路徑下查看官方是不是把庫的路徑給挪動了,做下調整就可以了)

platform :ios, '10.0'

# target的名字一般與你的項目名字相同
target 'HybridApp' do
    # 如果你正在使用Swift或想要使用動態(tài)框架,請取消注釋下一行
    # use_frameworks!

    # 'node_modules'目錄一般位于根目錄中
    pod 'React', :path => '../node_modules/react-native/'
    pod 'React-Core', :path => '../node_modules/react-native/React'
    pod 'React-DevSupport', :path => '../node_modules/react-native/React'
    pod 'React-RCTActionSheet', :path => '../node_modules/react-native/Libraries/ActionSheetIOS'
    pod 'React-RCTAnimation', :path => '../node_modules/react-native/Libraries/NativeAnimation'
    pod 'React-RCTBlob', :path => '../node_modules/react-native/Libraries/Blob'
    pod 'React-RCTImage', :path => '../node_modules/react-native/Libraries/Image'
    pod 'React-RCTLinking', :path => '../node_modules/react-native/Libraries/LinkingIOS'
    pod 'React-RCTNetwork', :path => '../node_modules/react-native/Libraries/Network'
    pod 'React-RCTSettings', :path => '../node_modules/react-native/Libraries/Settings'
    pod 'React-RCTText', :path => '../node_modules/react-native/Libraries/Text'
    pod 'React-RCTVibration', :path => '../node_modules/react-native/Libraries/Vibration'
    pod 'React-RCTWebSocket', :path => '../node_modules/react-native/Libraries/WebSocket'
    
    pod 'React-cxxreact', :path => '../node_modules/react-native/ReactCommon/cxxreact'
    pod 'React-jsi', :path => '../node_modules/react-native/ReactCommon/jsi'
    pod 'React-jsiexecutor', :path => '../node_modules/react-native/ReactCommon/jsiexecutor'
    pod 'React-jsinspector', :path => '../node_modules/react-native/ReactCommon/jsinspector'
    pod 'yoga', :path => '../node_modules/react-native/ReactCommon/yoga'
    
    pod 'DoubleConversion', :podspec => '../node_modules/react-native/third-party-podspecs/DoubleConversion.podspec'
    pod 'glog', :podspec => '../node_modules/react-native/third-party-podspecs/glog.podspec'
    pod 'Folly', :podspec => '../node_modules/react-native/third-party-podspecs/Folly.podspec'

end

這樣原生項目的Podfile就創(chuàng)建好了,接下來我們在終端里cd到原生項目的目錄下,然后再執(zhí)行pod install命令(這一步會很慢,建議翻墻)。

添加依賴庫成功后,終端會有類似如下的輸出。

同時我們的文件夾下會多出幾個文件。

以后我們就通過箭頭指的那個白色文件打開原生項目。

第四步:原生項目設置App Transport Security Settings

由于在開發(fā)階段,我們的原生應用需要加載本地服務器上的RN項目JS包,它是通過http協(xié)議傳輸的,所以原生項目需要以下設置App Transport Security Settings來支持http協(xié)議的請求。

<key>NSAppTransportSecurity</key>
    <dict>
        <key>NSAllowsArbitraryLoads</key>
        <true/>
    </dict>
第五步:編寫RN部分的代碼

假設原生項目想調用RN的一個界面Page1和一個組件TextAndImageView。

// Page1.js

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

export default class Page1 extends Component {
    render() {
        return (
            <View style={styles.container}>
                <Text>{'Page1'}</Text>
            </View>
        );
    }
};

const styles = StyleSheet.create({
    container: {
        flex: 1,
        backgroundColor: 'rgba(234, 234, 234, 1)',

        justifyContent: 'center',
        alignItems: 'center',
    },
});
// TextAndImageView.js

import React, {Component} from 'react';
import {
    StyleSheet,
    View,
    Text,
    Image,
    Dimensions,
} from 'react-native';

const {width, height} = Dimensions.get('window');

export default class TextAndImageView extends Component {
    render() {
        // 原生項目會給這個組件傳遞數據過來,接收一下
        const name = this.props.name;

        return (
            <View style={styles.container}>
                <Text style={styles.text}>{'這是:' + name}</Text>
                <Image
                    style={styles.image}
                    source={require('./testImage.png')}
                />
            </View>
        );
    }
};

const styles = StyleSheet.create({
    container: {
        backgroundColor: 'pink',

        width: width,
        height: 30 + 180,

        alignItems: 'center',
    },
    text: {
        height: 30,
    },
    image: {
        width: width,
        height: 180,
    },
});

然后我們需要在index.js文件里向RN注冊這些界面和組件(其實廣義來說都是組件了),以便將來原生項目調用。

// index.js

import {AppRegistry} from 'react-native';
import Page1 from './js/Page1';
import TextAndImageView from './js/TextAndImageView';

// 注冊原生項目要使用的界面,第一個參數必須是字符串,將來原生項目要用的
AppRegistry.registerComponent('Page1', () => Page1);
// 注冊原生項目要使用的組件
AppRegistry.registerComponent('TextAndImageView', () => TextAndImageView);
第六步:在原生項目里創(chuàng)建對應的ViewController和RCTRootView來包裹一下RN那邊的界面和組件,就可以調用了
// ViewController.m

#import "ViewController.h"
#import "Page1ViewController.h"

#import <React/RCTRootView.h>
#import <React/RCTBundleURLProvider.h>

@interface ViewController ()

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    [self initRCTRootView];
}
    
// 讀取RN的組件
- (void)initRCTRootView {
    NSURL *jsBundleURL = [self getJSBundleURL];
    
    // bundleURL:RN項目JS包的地址
    // moduleName:該界面想要盛放哪個RN界面,要和index.js里注冊的名字一致
    // initialProperties:接受一個NSDictionary類型的參數,來作為RN初始化時傳遞給JS的初始化數據
    // launchOptions:主要在AppDelegate加載JS包時使用,這里傳nil就行
    RCTRootView *rootView =[[RCTRootView alloc] initWithBundleURL:jsBundleURL moduleName:@"TextAndImageView" initialProperties:@{@"name": @"Angelababy"}
                                                    launchOptions: nil];
    rootView.frame = CGRectMake(0, 200, rootView.frame.size.width, rootView.frame.size.height);
    [self.view addSubview:rootView];
}
    
    // 讀取RN項目JS包的兩種方式
- (NSURL *)getJSBundleURL
    {
#if DEBUG
        // 開發(fā)環(huán)境下,RN項目的JS包是掛在本地服務器上的,要這樣讀取
        return [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@"index" fallbackResource:nil];
#else
        // 生產環(huán)境下,RN項目的JS包是打包在我們項目里,要這樣讀取
        return [[NSBundle mainBundle] URLForResource:@"main" withExtension:@"jsbundle"];
#endif
    }

- (IBAction)goToPage1:(id)sender {
    
    Page1ViewController *vc = [Page1ViewController new];
    [self.navigationController pushViewController:vc animated:YES];
}

@end
// Page1ViewController.m

#import "Page1ViewController.h"
#import <React/RCTRootView.h>
#import <React/RCTBundleURLProvider.h>

@interface Page1ViewController ()

@end

@implementation Page1ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    [self initRCTRootView];
}

- (void)initRCTRootView {
    NSURL *jsBundleURL = [self getJSBundleURL];
    
    RCTRootView *rootView =[[RCTRootView alloc] initWithBundleURL:jsBundleURL moduleName:@"Page1" initialProperties:nil
                                                    launchOptions: nil];
    self.view = rootView;
}

- (NSURL *)getJSBundleURL
{
#if DEBUG
    return [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@"index" fallbackResource:nil];
#else
    return [[NSBundle mainBundle] URLForResource:@"main" withExtension:@"jsbundle"];
#endif
}

@end
第七步:開發(fā)階段,啟動RN項目的本地服務器,然后運行原生項目查看混合開發(fā)的效果、調試

因為我們這個項目不是純RN項目,所以沒辦法在Xcode運行項目的時候就默認調起RN的本地服務器,需要我們手動打開。

于是我們打開終端,cd到RN項目下,調用npm start命令來啟動RN項目的本地服務器。出現下圖紅框所示,就表示本地服務器已經調起來了,我們就可以去運行原生項目到模擬器或真機上看效果了。

可見已經可以在原生項目中成功調用RN的界面和組件了。

當然實際開發(fā)中我們RN部分的代碼會更多更復雜,那就需要常常改動代碼來調試。而調試這種混合開發(fā)的App和調試一個純RN開發(fā)的App是一樣的,但凡我們使用了RN界面或組件的控制器里你都可以Command + D打開RN開發(fā)者菜單,Command + R進行reload JS,其它的調試技巧也都是一樣的,當然沒有使用的控制器是不能這樣調試的。

第八步:打包上線

經過上面的一些步驟,我們就完成了原生調用RN的混合開發(fā),但是我們在打包上線的時候會發(fā)現,RN部分的代碼并不在我們的Xcode里,所以就沒法被打包進ipa包中,這就沒法使用RN的代碼了。

所以說,這里還需要把RN部分的代碼,打包成一個JS包,拖進我們的原生項目中,再打ipa包才行。

但是打出來的JS包必須放在一個名為release_ios的文件夾下,所以我們得在執(zhí)行打包命令之前先創(chuàng)建一個release_ios的文件夾。打開終端,cd到你的RN項目路徑下,執(zhí)行mkdir release_ios命令來創(chuàng)建。

release_ios文件夾創(chuàng)建完成后,執(zhí)行react-native bundle --entry-file index.js --platform ios --dev false --bundle-output release_ios/main.jsbundle --assets-dest release_ios/命令,對RN代碼進行打包。

命令參數解釋:

-entry-file index.js:代表js的入口文件為index.js
--platform ios:代表打包導出的平臺為iOS
--dev false:代表關閉JS的開發(fā)者模式
--bundle-output:后面跟的是打包后將JS包導出到的位置
--assets-dest:后面跟的是打包后的一些資源文件導出到的位置

打包完成后,會在release_ios文件夾下生成main.jsbundle文件和assets文件夾(如果RN中用到了一些圖片資源的話)。

我們把這兩個東西拖到iOS項目中就可以了,這樣原生項目就可以脫離本地服務器來加載RN部分的代碼了,我們就可以打ipa包發(fā)布到AppStore了。

注意:打包完成后,加載包中的圖片有可能加載不出來,可參看這篇文章。

當然最簡單的辦法,就是你在加載項目中圖片時也直接使用uri而非require的方式就沒問題,即:

<Image
  style={styles.image}
  source={require('./testImage.png')}
/>

替換成

<Image
  style={styles.image}
  source={{uri: 'testImage'}}
/>

且圖片不填相對路徑、不填后綴名,僅僅寫個名字就ok了。

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

相關閱讀更多精彩內容

友情鏈接更多精彩內容