混合開發(fā)有一些使用場(chǎng)景:
- 在 Native 項(xiàng)目中加入 React Native 界面. 比如詳情頁采用 RN 實(shí)現(xiàn).
- 在 React Native 項(xiàng)目中加入 Native 界面. 比如詳情頁采用 Native 實(shí)現(xiàn).
- 在 Native 項(xiàng)目中加入 React Native 模塊. 比如列表中某個(gè) cell 采用 RN 模塊實(shí)現(xiàn).
- 在 React Native 項(xiàng)目中加入 Native 模塊. 比如地圖模塊
在 Native 應(yīng)用中添加 React Native 界面(模塊)
主要步驟如下:
- 創(chuàng)建一個(gè) React Native 的空項(xiàng)目(不包含 iOS 模塊和 Android 模塊).
- 為已存在的 iOS 項(xiàng)目配置 React Native 所需的依賴.
- 創(chuàng)建 index.js 文件, 并添加 React Native 代碼. 用于 Native 應(yīng)用加載 React Native 界面(模塊).
- 通過 RCTRootView 作為容器, 加載 React Native 組件.
- 運(yùn)行混編項(xiàng)目.
- 添加更多 React Native 的組件.
- 打包 iOS 項(xiàng)目.
1. 創(chuàng)建一個(gè) React Native 的空項(xiàng)目
有兩種方式
- 創(chuàng)建并配置 package.json 文件, 通過 yarn 安裝 react-native, react 等依賴的方式創(chuàng)建項(xiàng)目
- 直接通過
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è)問題:
- 由于 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>
- 對(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)目中使用.
- 生成 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/ 文件夾中.
-
將 js bundle 和 assets 直接拖到項(xiàng)目根目錄.
- 在 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ù)的流通原理, 都是一樣的.