React Native 混合開發(fā) - iOS篇(一)

混合開發(fā)有一些使用場(chǎng)景:

  1. 在 Native 項(xiàng)目中加入 React Native 界面. 比如詳情頁采用 RN 實(shí)現(xiàn).
  2. 在 React Native 項(xiàng)目中加入 Native 界面. 比如詳情頁采用 Native 實(shí)現(xiàn).
  3. 在 Native 項(xiàng)目中加入 React Native 模塊. 比如列表中某個(gè) cell 采用 RN 模塊實(shí)現(xiàn).
  4. 在 React Native 項(xiàng)目中加入 Native 模塊. 比如地圖模塊

在 Native 應(yīng)用中添加 React Native 界面(模塊)

主要步驟如下:

  1. 創(chuàng)建一個(gè) React Native 的空項(xiàng)目(不包含 iOS 模塊和 Android 模塊).
  2. 為已存在的 iOS 項(xiàng)目配置 React Native 所需的依賴.
  3. 創(chuàng)建 index.js 文件, 并添加 React Native 代碼. 用于 Native 應(yīng)用加載 React Native 界面(模塊).
  4. 通過 RCTRootView 作為容器, 加載 React Native 組件.
  5. 運(yùn)行混編項(xiàng)目.
  6. 添加更多 React Native 的組件.
  7. 打包 iOS 項(xiàng)目.

1. 創(chuàng)建一個(gè) React Native 的空項(xiàng)目

有兩種方式

    1. 創(chuàng)建并配置 package.json 文件, 通過 yarn 安裝 react-native, react 等依賴的方式創(chuàng)建項(xiàng)目
    1. 直接通過 react-native init ProjectName 創(chuàng)建項(xiàng)目, 然后刪除 iOS 和 Android 文件內(nèi)容.

對(duì)于方式一, 我們需要?jiǎng)?chuàng)建一個(gè)空目錄存放所有的項(xiàng)目文件, 然后創(chuàng)建并配置 package.json 文件, 內(nèi)容如下:

{
  "name": "MyReactNativeApp",
  "version": "0.0.1",
  "private": true,
  "scripts": {
    "start": "node node_modules/react-native/local-cli/cli.js start"
  }
}

然后, 在項(xiàng)目根目錄執(zhí)行 yarn add react-native 添加模塊.

此時(shí)會(huì)有警告信息, 需要我們安裝對(duì)應(yīng)版本的 React 模塊

yarn add react@16.6.3

yarn 在添加依賴的時(shí)候都會(huì)將其安裝到項(xiàng)目根目錄下的 node_modules 文件夾中, 這個(gè)目錄一般比較大.

我們應(yīng)該將其添加到 .gitignore 文件中(如果有的話), 保證這個(gè)文件夾只保留在本地, 不上傳到版本控制系統(tǒng).

最后結(jié)果如下:


另外一種方式創(chuàng)建 React Native 項(xiàng)目, 就比較簡(jiǎn)單了. 通過如下創(chuàng)建

react-native init ProjectName

不過此方法會(huì)產(chǎn)生多余的文件, 需要?jiǎng)h除. 下面是創(chuàng)建的 package.json 文件.

2. 為已存在的 iOS 項(xiàng)目配置 React Native 所需的依賴

這一步驟主要用來介紹如何將 React Native 項(xiàng)目與 Native 項(xiàng)目融合.

比如我們有一個(gè) RNHybridiOS 項(xiàng)目, 我們直接將其復(fù)制到 RNHybrid 文件夾中, 現(xiàn)在項(xiàng)目的根目錄中, 文件結(jié)構(gòu)如下:

在 iOS 項(xiàng)目中我們一般使用 CocoaPods 來管理項(xiàng)目依賴.

RNHybridiOS 文件夾中創(chuàng)建 Podfile 文件

pod init

Podfile 文件中配置依賴

# 對(duì)于Swift應(yīng)用來說下面兩句是必須的
platform :ios, '9.0'
use_frameworks!

# target的名字一般與你的項(xiàng)目名字相同
target 'RNHybridiOS' do

  # 'node_modules'目錄一般位于根目錄中
  # 但是如果你的結(jié)構(gòu)不同麻顶,那你就要根據(jù)實(shí)際路徑修改下面的`:path`
  pod 'React', :path => '../node_modules/react-native', :subspecs => [
    'Core',
    'CxxBridge', # 如果RN版本 >= 0.47則加入此行
    'DevSupport', # 如果RN版本 >= 0.43颂砸,則需要加入此行才能開啟開發(fā)者菜單
    'RCTText',
    'RCTNetwork',
    'RCTWebSocket', # 調(diào)試功能需要此模塊
    'RCTAnimation', # FlatList和原生動(dòng)畫功能需要此模塊
    # 在這里繼續(xù)添加你所需要的其他RN模塊
  ]
  # 如果你的RN版本 >= 0.42.0虫腋,則加入下面這行
  pod "yoga", :path => "../node_modules/react-native/ReactCommon/yoga"

  # 如果RN版本 >= 0.45則加入下面三個(gè)第三方編譯依賴
  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

依賴的內(nèi)容參考自官方文檔

這里簡(jiǎn)單講一下 Podfile 文件中這些代碼的意思. 后續(xù)可能會(huì)做源碼分析.

  • React Native 框架整體是作為 node 模塊安裝到項(xiàng)目中的, 我們能在 /node_modules/react-native 目錄中找到.
  • Podfile 里面關(guān)于 React 庫這一部分的操作主要就是將相關(guān)的庫文件的 引用 添加到 React 目錄下(原始文件還是在 /node_modules/react-native 目錄下). 以供使用

接下來在 iOS 項(xiàng)目根目錄執(zhí)行以下命令, 安裝 CocoaPods 依賴.

pod install

安裝完依賴, 就需要?jiǎng)?chuàng)建 React Native 代碼以供 iOS 項(xiàng)目使用.

3. 創(chuàng)建 index.js 文件, 并添加 React Native 代碼

RNHybrid 目錄下創(chuàng)建一個(gè) index.js 文件并添加如下代碼:

import { AppRegistry } from 'react-native';
import App from './App';

AppRegistry.registerComponent('Welcome', () => App);

向 React Native 注冊(cè)一個(gè)名為 Welcome 的組件.

上述代碼引入了一個(gè) App.js 文件. 內(nèi)容可以如下:

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

const instructions = Platform.select({
  ios: 'Press Cmd+R to reload,\n' + 'Cmd+D or shake for dev menu',
  android:
    'Double tap R on your keyboard to reload,\n' +
    'Shake or press menu button for dev menu',
});

export default class App extends Component {
  constructor(props) {
    super(props);
  }

  render() {
    return (
      <View style={styles.container}>
        <Text style={styles.welcome}>Welcome to React Native!</Text>
        <Text style={styles.instructions}>To get started, edit App.js</Text>
        <Text style={styles.instructions}>{instructions}</Text>

      </View>
    );
  }
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
    backgroundColor: '#F5FCFF',
  },
  welcome: {
    fontSize: 20,
    textAlign: 'center',
    margin: 10,
  },
  instructions: {
    textAlign: 'center',
    color: '#333333',
    marginBottom: 5,
  },
});

這是默認(rèn)初始化項(xiàng)目的一個(gè)初始頁面. 顯示簡(jiǎn)單的文本數(shù)據(jù).

4. 通過 RCTRootView 作為容器, 加載 React Native 組件.

在上面我們創(chuàng)建了一個(gè) Welcome 組件, 接下來是如何使用這個(gè)組件.

class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()

        
        if let jsCodeLocation = RCTBundleURLProvider.sharedSettings()?.jsBundleURL(forBundleRoot: "index", fallbackResource: nil) {

            let welcomeView = RCTRootView(bundleURL: jsCodeLocation, moduleName: "Welcome", initialProperties: nil, launchOptions: nil)
            // 一定要設(shè)置 frame, 默認(rèn)是 0
            welcomeView?.frame = view.bounds
            
            view.addSubview(welcomeView!)
        }
    }
}

有幾點(diǎn)需要注意:

  • initWithBundleURL: 用于指示 js 代碼位置, 在開發(fā)階段可以使用RCTBundleURLProvider 的形式生成 jsCodeLocation, 也可以直接指定
let jsCodeLocation = URL(string: "http://localhost:8081/index.bundle?platform=ios")

在發(fā)布版本只會(huì)使用靜態(tài)js bundle. 而不是像這樣通過本地服務(wù)器加載.

  • moduleName: 用于指定 React Native 要加載的 JS 模塊名, 也就是上文中所講的在index.js 中注冊(cè)的模塊名.
  • launchOptions: 主要在 AppDelegate 加載 JS Bundle 時(shí)使用楼咳,這里傳nil就行仙粱;
  • initialProperties: 接受一個(gè)字典類型的參數(shù)來作為 RN 初始化時(shí)傳遞給 JS 的初始化數(shù)據(jù).

5. 運(yùn)行混編項(xiàng)目

在上一步中我們已經(jīng)加載了在 JS 中注冊(cè)的 React Native 組件. 下面我們需要啟動(dòng)開發(fā)服務(wù)器(即 Packager, 它負(fù)責(zé)實(shí)時(shí)監(jiān)測(cè) js 文件的變動(dòng)并實(shí)時(shí)打包, 輸出給客戶端運(yùn)行), 通過這加載 js 代碼.

在混編項(xiàng)目的根目錄執(zhí)行以下命令

npm start

隨即可以直接用 Xcode 運(yùn)行項(xiàng)目, 或者在項(xiàng)目的根目錄執(zhí)行以下

react-native run-ios

第一次運(yùn)行可能會(huì)遇到幾個(gè)問題:

  1. 由于 React Native 部分的代碼是通過本地服務(wù)器進(jìn)行加載的, 并且它是 http 協(xié)議傳輸?shù)? 為了能在 iOS 原生項(xiàng)目中能使用, 我們需要設(shè)置 App Transport Security Settings, 讓其支持 http 傳輸.
    在 iOS 項(xiàng)目根目錄下, 找到 info.plist 文件.
<key>NSAppTransportSecurity</key>
<dict>
   // 這個(gè)是允許所有 http 格式加載
   <key>NSAllowsArbitraryLoads</key>
   <true/>
      
    // 下面是為 localhost 添加白名單,  兩種方式任選其一
    <key>NSExceptionDomains</key>
    <dict>
        <key>localhost</key>
        <dict>
            <key>NSTemporaryExceptionAllowsInsecureHTTPLoads</key>
            <true/>
        </dict>
    </dict>
</dict>
  1. 對(duì)于 iOS 項(xiàng)目中的 RCTRootView, 默認(rèn)加載出來的視圖控件的 frame 是 0, 所以我們需要為其設(shè)置大小, 否則將不會(huì)顯示.

6. 添加更多 React Native 的組件

在 index.js 文件中, 我們可以添加多個(gè)組件以供 iOS 項(xiàng)目調(diào)用.

import {AppRegistry} from 'react-native';
import App from './App';
import App2 from './App2';
import App3 from './App3';

AppRegistry.registerComponent("Welcome", () => App);
AppRegistry.registerComponent("Welcome2", () => App2);
AppRegistry.registerComponent("Welcome3", () => App3);

在 iOS 項(xiàng)目中指定需要加載的組件名稱即可.

7. 打包 iOS 項(xiàng)目.

對(duì)于發(fā)布版本我們不能使用本地服務(wù)器加載 js 代碼, 所以我們需要將 js 代碼打成 bundle, 在 iOS 項(xiàng)目中使用.

  1. 生成 js bundle
react-native bundle --entry-file index.js --platform ios --dev false --bundle-output release_ios/main.jsbundle --assets-dest release_ios/

說明一下:

react-native, 執(zhí)行命令
參數(shù)以下
bundle, 命令類型
--entry-file 文件入口, 這里指定為 index.js
--platform, 平臺(tái), 這里指定 ios
--dev, 是否為開發(fā)版本, 這里指定 false
 --bundle-output, bundle 輸出路徑, 這里指定release_ios/main.jsbundle, 如果沒有 release_ios 文件夾需要手動(dòng)創(chuàng)建
 --assets-dest, 如果有圖片資源, 也需要打包, 這里指定在 release_ios/ 文件夾中.
  1. 將 js bundle 和 assets 直接拖到項(xiàng)目根目錄.


  2. 在 iOS 項(xiàng)目代碼中, 指定 js code 路徑.
    在下面代碼中, 我們獲取到了 react native 界面, 將其用在 App 的根控制器中.
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        
        #if DEBUG         // 調(diào)試版本
        
        let jsCodeLocation = RCTBundleURLProvider.sharedSettings()?.jsBundleURL(forBundleRoot: "index", fallbackResource: nil)
        
        #else             // 發(fā)布版本, 本地加載 js 代碼
        
        let jsCodeLocation = Bundle.main.url(forResource: "main", withExtension: "jsbundle")
        
        #endif
        
        
        if let jsCodeLocation = jsCodeLocation {
            
            let rootView = RCTRootView(bundleURL: jsCodeLocation, moduleName: "NativeDemo_Swift", initialProperties: nil, launchOptions: launchOptions)
            
            window = UIWindow(frame: UIScreen.main.bounds)
            
            let rootVC = UIViewController()
            rootVC.view = rootView
            window?.rootViewController = rootVC
            window?.makeKeyAndVisible()
        }
        
        return true
}

代碼里面的 DEBUG 它只是我們自定義的一個(gè)標(biāo)記


我在測(cè)試的時(shí)候發(fā)現(xiàn), 在項(xiàng)目中導(dǎo)入 main.jsbundle 后, 加載 js 代碼的規(guī)律如下.


  • 開發(fā)模式下, 如果未開啟本地服務(wù)器, 那么它會(huì)默認(rèn)先去找有沒有 main.jsbundle 這個(gè)文件, 如果沒有, 屏幕會(huì)直接黑屏. 即無法加載頁面. 如果有, 會(huì)優(yōu)先加載本地的 main.jsbundle 文件.
  • 開發(fā)模式下, 本地服務(wù)器肯定沒有開啟, 規(guī)律和上面也一樣.

在保證 main.jsbundle 文件存在的情況下, 我們可以偷懶, 不需要分兩種版本. 直接按照 RCTBundleURLProvider.sharedSettings()?.jsBundleURL(forBundleRoot: "index", fallbackResource: nil) 這種來加載. 不過, 這種方式不推薦, 因?yàn)樾枰嘧鲆淮问欠耖_服務(wù)器的判斷, 性能有一點(diǎn)點(diǎn)損耗. 而且, 對(duì)于其他更復(fù)雜的情況, 我們可能需要 flag 來做判斷.

對(duì)于 React Native 如何加載 iOS 模塊, 在下一篇文章中講解.

其實(shí)只要明白數(shù)據(jù)的流通原理, 都是一樣的.

參考

React Native 中文網(wǎng) - 在原生項(xiàng)目中集成 RN

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末医吊,一起剝皮案震驚了整個(gè)濱河市设哗,隨后出現(xiàn)的幾起案子诵棵,更是在濱河造成了極大的恐慌疯潭,老刑警劉巖赊堪,帶你破解...
    沈念sama閱讀 218,755評(píng)論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異竖哩,居然都是意外死亡哭廉,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,305評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門相叁,熙熙樓的掌柜王于貴愁眉苦臉地迎上來遵绰,“玉大人,你說我怎么就攤上這事增淹〈环茫” “怎么了?”我有些...
    開封第一講書人閱讀 165,138評(píng)論 0 355
  • 文/不壞的土叔 我叫張陵虑润,是天一觀的道長(zhǎng)成玫。 經(jīng)常有香客問我,道長(zhǎng),這世上最難降的妖魔是什么哭当? 我笑而不...
    開封第一講書人閱讀 58,791評(píng)論 1 295
  • 正文 為了忘掉前任猪腕,我火速辦了婚禮,結(jié)果婚禮上钦勘,老公的妹妹穿的比我還像新娘陋葡。我一直安慰自己,他們只是感情好彻采,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,794評(píng)論 6 392
  • 文/花漫 我一把揭開白布脖岛。 她就那樣靜靜地躺著,像睡著了一般颊亮。 火紅的嫁衣襯著肌膚如雪柴梆。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,631評(píng)論 1 305
  • 那天终惑,我揣著相機(jī)與錄音绍在,去河邊找鬼。 笑死雹有,一個(gè)胖子當(dāng)著我的面吹牛偿渡,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播霸奕,決...
    沈念sama閱讀 40,362評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼溜宽,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了质帅?” 一聲冷哼從身側(cè)響起适揉,我...
    開封第一講書人閱讀 39,264評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎煤惩,沒想到半個(gè)月后嫉嘀,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,724評(píng)論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡魄揉,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,900評(píng)論 3 336
  • 正文 我和宋清朗相戀三年剪侮,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片洛退。...
    茶點(diǎn)故事閱讀 40,040評(píng)論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡瓣俯,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出兵怯,到底是詐尸還是另有隱情彩匕,我是刑警寧澤,帶...
    沈念sama閱讀 35,742評(píng)論 5 346
  • 正文 年R本政府宣布摇零,位于F島的核電站推掸,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏驻仅。R本人自食惡果不足惜谅畅,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,364評(píng)論 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望噪服。 院中可真熱鬧毡泻,春花似錦、人聲如沸粘优。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,944評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽雹顺。三九已至丹墨,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間嬉愧,已是汗流浹背贩挣。 一陣腳步聲響...
    開封第一講書人閱讀 33,060評(píng)論 1 270
  • 我被黑心中介騙來泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留没酣,地道東北人王财。 一個(gè)月前我還...
    沈念sama閱讀 48,247評(píng)論 3 371
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像裕便,于是被迫代替她去往敵國(guó)和親绒净。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,979評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容