前言
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)建相應的ViewController和RCTRootView來包裹一下RN那邊的界面和組件,這樣原生就可以成功調用RN那邊的界面和組件了。當然這中間要用CocoaPods來為原生項目添加并管理一些RN的依賴庫——這樣原生項目和RN項目之間才能建立連接。
第一步:創(chuàng)建一個原生項目
我們這里創(chuàng)建一個原生項目,名字叫作HybridApp。
第二步:創(chuàng)建一個RN項目
我們這里創(chuàng)建一個RN項目,名字叫作 HybridApp_RN,用來編寫HybridAppRN部分的代碼。
創(chuàng)建好后,項目的目錄結構如下。

但因為這個RN項目是用來輔助我們自己的HybridApp項目開發(fā)的,所以它里面不能是默認的那個android和ios項目,我們要刪掉它倆,替換成我們自己的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了。