項目接入使用React Native

有一段時間沒有寫東西了,因為最近項目開始嘗試使用React Native(以下簡稱RN)來開發(fā),所以這段時間一直在研究,目前為止開發(fā)的內(nèi)容不多,所以使用過的東西也不算多,這里也只是做個簡單的記錄
這里我打算從以下幾個方面來講:
1.背景介紹
2.環(huán)境的配置
3.RN所需要知道的知識
4.RN與原生的交互
5.本地調(diào)試與本地打包調(diào)試
6.遠程熱更新
7.踩坑記錄
8.相關資料


0x00 背景介紹

RN是Facebook在React.js 2015大會上公布開源的,它是基于開源框架React.js來實現(xiàn)的,它支持了iOS和Android兩大平臺,解決開發(fā)者們編寫重復代碼的痛點,實現(xiàn)了所謂的跨平臺開發(fā),Write Once , Run Anywhere,這是目前很多開發(fā)者所追求的,特別是一些獨立開發(fā)者或者項目快速迭代的團隊,可以嘗試使用RN來開發(fā),另外包括方便的npm管理,快速的調(diào)試等等
那么既然優(yōu)點這么明顯,為什么大部分的團隊還是采用傳統(tǒng)的iOS、Android開發(fā)呢,踩過坑的同學都知道,首先在支持上還做得不夠完善,在使用組件時,RN原有提供的組件往往不能很好的支持,與原生組件多少存在著差異,而且在使用第三方組件時,又會因為長期不更新的原因,存在很多坑,對于新手來說,根本不知道坑在哪,完全無從下手。另外RN的性能也不能和原生的相提并論,特別是列表組件在渲染大量數(shù)據(jù)時,流暢性方面還是原生更加優(yōu)越,而且并非所以代碼iOS和Android都能公用,如果某個組件只支持某一個平臺,那你必須分開編寫代碼,實際上還是存在重復代碼,除此之外學習的成本以及團隊RN推廣等等原因都需要考量,但是我相信,跨平臺開發(fā)始終是一個趨勢,RN整個社區(qū)也在不斷的發(fā)展,相信未來我們會實現(xiàn)真正意義上的跨平臺開發(fā)~

0x01 環(huán)境配置

相對于Android的環(huán)境配置過程來說,iOS可以說是簡單輕松…出現(xiàn)的問題要少很多
首先我們需要安裝Homebrew

$ ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"

然后安裝nodewatchman(用于監(jiān)測文件系統(tǒng)的變更)

brew install node
brew install watchman

RN的命令行工具react-native-cli

npm install -g react-native-cli

如果遇到權限問題,只要前面加個sudo即可

sudo npm install -g react-native-cli

yeah~that's all~我在配置的過程中,基本沒有報錯,如果有出現(xiàn)配置問題的話,請自行Google一下,看看大家的解決方法

0x02 RN所需要知道的知識

RN的運行機制

在開始寫代碼之前,我們需要了解RN的運行機制是怎么樣的,這樣寫起來思路會更加清晰
首先,程序需要有個入口,我們可以創(chuàng)建很多的組件,但是有且只有一個組件用來做為程序的入口,RN的入口則類似于iOS的main.m,在iOS里我們會在main函數(shù)里設置應用程序類的代理類

return UIApplicationMain(argc, argv, nil, NSStringFromClass([KDAppDelegate class]));

同樣,RN里我們需要注冊入口的名稱,并且這個名稱要和原生的初始化RN界面時的入口名稱保持一致

// 引用navigation使用的組件

import React, { Component } from 'react';
import {
 AppRegistry,
 ...
} from 'react-native';
?
// 創(chuàng)建navigation類
class navigation extends Component {
 // set compnent
}
?
// 注冊navigation為程序的入口
AppRegistry.registerComponent('navigation', () => navigation);

在iOS原生這邊需要用到RN的地方,我們需要初始化它

NSURL *jsCodeLocation =

[[NSBundle mainBundle] URLForResource:@"bundle/index.ios" withExtension:@"jsbundle"];
//       [NSURL URLWithString:@"http://172.17.9.188:8081/index.ios.bundle?platform=ios"];
RCTRootView *rootView = [[RCTRootView alloc] initWithBundleURL:jsCodeLocation
                                                   moduleName:@"navigation"
                                           initialProperties:nil
                                                 launchOptions:nil];
self.view = rootView;

Tip:

jsCodeLocation 是RN資源加載的路徑,我們有兩種方式去加載,一種是加載本地的js文件及其他資源文件,一種是我們將其打包成bundle文件,前者的優(yōu)勢在于方便調(diào)試,后者是用來打包發(fā)布上線用

moduleName
是對應于RN的入口名字,且這個是唯一的,那我們?nèi)绻卸鄠€入口需要初始化不同的RN界面,那該怎么辦呢?這就用到了initialProperties
,它是字典類型,我們可以將入口作為路由,在initialProperties
里傳入我們需要初始化的界面名稱,入口獲取到名稱之后,渲染對應的界面即可

RN組件的生命周期

在RN里面,所謂的界面應該稱作類或者組件更為合適并且組件也有它的生命周期,和iOS里的viewWillAppear、viewDidDisappear等等很像,下面生命周期內(nèi)容取自于http://www.race604.com/react-native-component-lifecycle/


我們可以把組件生命周期大致分為三個階段:
第一階段:是組件第一次繪制階段,如圖中的上面虛線框內(nèi),在這里完成了組件的加載和初始化;

第二階段:是組件在運行和交互階段,如圖中左下角虛線框,這個階段組件可以處理用戶交互,或者接收事件更新界面;

第三階段:是組件卸載消亡的階段,如圖中右下角的虛線框中,這里做一些組件的清理工作。

下面來詳細介紹生命周期中的各回調(diào)函數(shù)。

getDefaultProps

在組件創(chuàng)建之前,會先調(diào)用 getDefaultProps(),這是全局調(diào)用一次,嚴格地來說,這不是組件的生命周期的一部分。在組件被創(chuàng)建并加載候,首先調(diào)用 getInitialState(),來初始化組件的狀態(tài)。

componentWillMount

然后,準備加載組件,會調(diào)用componentWillMount(),其原型如下:

void componentWillMount() 

這個函數(shù)調(diào)用時機是在組件創(chuàng)建,并初始化了狀態(tài)之后,在第一次繪制 render() 之前??梢栽谶@里做一些業(yè)務初始化操作,也可以設置組件狀態(tài)。這個函數(shù)在整個生命周期中只被調(diào)用一次。

componentDidMount

在組件第一次繪制之后,會調(diào)用 componentDidMount(),通知組件已經(jīng)加載完成。函數(shù)原型如下:

void componentDidMount() 

這個函數(shù)調(diào)用的時候,其虛擬 DOM 已經(jīng)構建完成,你可以在這個函數(shù)開始獲取其中的元素或者子組件了。需要注意的是,RN 框架是先調(diào)用子組件的 componentDidMount(),然后調(diào)用父組件的函數(shù)。從這個函數(shù)開始,就可以和 JS 其他框架交互了,例如設置計時 setTimeout或者 setInterval,或者發(fā)起網(wǎng)絡請求。這個函數(shù)也是只被調(diào)用一次。這個函數(shù)之后,就進入了穩(wěn)定運行狀態(tài),等待事件觸發(fā)。

componentWillReceiveProps

如果組件收到新的屬性(props),就會調(diào)用componentWillReceiveProps()
,其原型如下:

void componentWillReceiveProps( 

 object nextProps
)

輸入?yún)?shù) nextProps是即將被設置的屬性,舊的屬性還是可以通過 this.props 來獲取。在這個回調(diào)函數(shù)里面,你可以根據(jù)屬性的變化,通過調(diào)用 this.setState()來更新你的組件狀態(tài),這里調(diào)用更新狀態(tài)是安全的,并不會觸發(fā)額外的 render()調(diào)用。如下:

componentWillReceiveProps: function(nextProps) { 

 this.setState({
   likesIncreasing: nextProps.likeCount > this.props.likeCount
 });
}

shouldComponentUpdate

當組件接收到新的屬性和狀態(tài)改變的話,都會觸發(fā)調(diào)用shouldComponentUpdate(...)
,函數(shù)原型如下:

boolean shouldComponentUpdate( 

 object nextProps, object nextState
)

輸入?yún)?shù)nextProps 和上面的 componentWillReceiveProps函數(shù)一樣,nextState表示組件即將更新的狀態(tài)值。這個函數(shù)的返回值決定是否需要更新組件,如果 true表示需要更新,繼續(xù)走后面的更新流程。否者,則不更新,直接進入等待狀態(tài)。
默認情況下,這個函數(shù)永遠返回 true用來保證數(shù)據(jù)變化的時候 UI 能夠同步更新。在大型項目中,你可以自己重載這個函數(shù),通過檢查變化前后屬性和狀態(tài),來決定 UI 是否需要更新,能有效提高應用性能。

componentWillUpdate

如果組件狀態(tài)或者屬性改變,并且上面的 shouldComponentUpdate(...)返回為 true,就會開始準更新組件,并調(diào)用 componentWillUpdate(),其函數(shù)原型如下:

void componentWillUpdate( 

 object nextProps, object nextState
)

輸入?yún)?shù)與 shouldComponentUpdate 一樣,在這個回調(diào)中,可以做一些在更新界面之前要做的事情。需要特別注意的是,在這個函數(shù)里面,你就不能使用 this.setState來修改狀態(tài)。這個函數(shù)調(diào)用之后,就會把 nextPropsnextState 分別設置到 this.propsthis.state中。緊接著這個函數(shù),就會調(diào)用 render()來更新界面了。

componentDidUpdate

調(diào)用了 render() 更新完成界面之后,會調(diào)用 componentDidUpdate()來得到通知,其函數(shù)原型如下:

void componentDidUpdate( 

 object prevProps, object prevState
)

因為到這里已經(jīng)完成了屬性和狀態(tài)的更新了,此函數(shù)的輸入?yún)?shù)變成了 prevPropsprevState。

componentWillUnmount

當組件要被從界面上移除的時候,就會調(diào)用 componentWillUnmount(),其函數(shù)原型如下:

void componentWillUnmount() 

在這個函數(shù)中,可以做一些組件相關的清理工作,例如取消計時器、網(wǎng)絡請求等。
下表是生命周期函數(shù)的調(diào)用次數(shù),以及能否使用setSate():

RN的設計模式

目前設計模式也非常多,如Flux,Reflux,Redux,Relay,Marty,不過以上都不是很了解,可以參考ReactNative的組件架構設計學習了解一下,由于做客戶端的同學接觸的最多的是MVC,MVVM、MVCS等等,所以我覺得選用類似MVCS的模式可能更加適合新手的學習,比如寫組件時,通常我們會創(chuàng)建一個組件,里面會包含數(shù)據(jù)的處理,頁面的渲染,樣式的設置,網(wǎng)絡請求,當這些內(nèi)容過多時,組件就會顯得特別臃腫,所以我們需要將其拆分開為數(shù)據(jù)模型(Model),頁面渲染,樣式設置,網(wǎng)路請求(Service),這里的頁面渲染和樣式設置,不能算是稱作為iOS里的Controller和View,應該跟前端一樣,在html文件里面寫布局,css文件里面寫樣式,感覺像是MVCS和前端的融合

0x03 RN與原生的交互

在寫RN時不免會遇到與原生交互,下面我分JS調(diào)用原生、原生調(diào)用JS來講

JS調(diào)用原生

在調(diào)用原生時,我們需要實現(xiàn)RCTBridgeModuleRCT_EXPORT_MODULE();
RCT_EXPORT_MODULE();則是一個宏定義,返回moduleName,并且調(diào)用+ load方法注冊

#define RCT_EXPORT_MODULE(js_name) \
RCT_EXTERN void RCTRegisterModule(Class); \
+ (NSString *)moduleName { return @#js_name; } \
+ (void)load { RCTRegisterModule(self); }

例如我們增加一個bridge方法,獲取版本號,getVersion為方法名,callback是原生回調(diào)給JS的內(nèi)容

RCT_EXPORT_METHOD(getVersion : (RCTResponseSenderBlock)callback) {
   NSString *version =
   [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleShortVersionString"];
   callback(@[[NSNull null], @[version]]);
}

然后返回方法的隊列為主隊列

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

在JS文件里,我們可以定義一個全局變量

var ZanIntentModule = NativeModules.ZanIntentModule;

然后在使用的時候調(diào)用我們在原生時定義方法

ZanIntentModule.getVersion(
(callback) => {
   // do some thing
})

原生調(diào)用JS

老版本的調(diào)用方式為,但是接口被標記為deprecated:__deprecated_msg("Subclass RCTEventEmitter instead");

[self.bridge.eventDispatcher sendAppEventWithName:kGiftReloadData body:nil];

新版本的調(diào)用方式為

ZanEventEmitter *emitter = [[ZanEventEmitter alloc] init];
emitter.bridge = self.bridge;
[emitter sendEventWithName:kGiftReloadData body:nil];

但是新版本坑的是,直接這樣調(diào)用時bridge居然是nil,網(wǎng)上說用單例,但是也不行...所以我還是用老版本的調(diào)用方法,有哪個大神知道怎么用新版本接口調(diào)用的正確姿勢,請留言交流哈
然后在實現(xiàn)RCTBridgeDelegate

- (NSURL *)sourceURLForBridge:(RCTBridge *)bridge {
   return [[NSBundle mainBundle] URLForResource:@"bundle/index.ios" withExtension:@"jsbundle"];
//   return [NSURL URLWithString:@"http://172.17.9.94:8081/index.ios.bundle?platform=ios"];
}

在對應的組件里,需要在componentWillMount增加監(jiān)聽

componentWillMount() {
  this.eventEmitter = NativeAppEventEmitter.addListener(
   'GiftReloadData',
   () => this._reloadData()
 );
}

對應的也需要移除掉監(jiān)聽

componentWillUnmount() {
  subscription.remove();
}

然后原生發(fā)送action之后,會觸發(fā)我們設定好的reloadData()方法

0x04 本地調(diào)試與打包調(diào)試

在編寫的過程中,也需要進行調(diào)試,調(diào)試有兩種方法:一種是本地調(diào)試,一種是打包調(diào)試

本地調(diào)試

我們在加載bundle時,需要替換成你的ip地址,端口號不要變

[NSURL URLWithString:@"http://172.17.9.94:8081/index.ios.bundle?platform=ios"]

如果你是在真機上調(diào)試,你需要開啟HTTP代理,填寫你的ip地址和端口號
在終端上,先進入到你的項目目錄(與node_modules目錄同級),然后開啟服務

yzydeMacBook-Pro:shangjiaban-ios yzy$ npm start

你修改了某處之后,在模擬器上點擊Shake Gesture或者快捷鍵,在真機上只要搖一搖就可以


在模擬器彈出框里選擇Roload,這樣就會重新加載你本地的JS文件

如果你想查看JS里面的log日志,你可以選擇Start Remote JS Debugging,在chrome瀏覽器里就能看到輸出的日志了

打包調(diào)試

另外一種就是打包調(diào)試,但是比較麻煩,首先我們要講bundle加載方式改為

[[NSBundle mainBundle] URLForResource:@"bundle/index.ios" withExtension:@"jsbundle"];

然后在終端里面,輸入

yzydeMacBook-Pro:shangjiaban-ios yzy$ react-native bundle --entry-file index.ios.js --platform ios --dev false --bundle-output ./xxx/bundle/index.ios.jsbundle --assets-dest ./xxx/bundle

--bundle-output ./xxx/bundle/index.ios.jsbundle指的是輸出的bundle文件路徑

[20:54:43] <START> Building Dependency Graph

[20:54:43] <START> Crawling File System
[20:54:43] <START> find dependencies
[20:54:48] <END>   Crawling File System (4712ms)
[20:54:48] <START> Building in-memory fs for JavaScript
[20:54:48] <END>   Building in-memory fs for JavaScript (230ms)
[20:54:48] <START> Building in-memory fs for Assets
[20:54:48] <END>   Building in-memory fs for Assets (154ms)
[20:54:48] <START> Building Haste Map
[20:54:48] <START> Building (deprecated) Asset Map
[20:54:48] <END>   Building (deprecated) Asset Map (66ms)
[20:54:48] <END>   Building Haste Map (154ms)
[20:54:48] <END>   Building Dependency Graph (5261ms)
transformed 372/372 (100%)
[20:54:49] <END>   find dependencies (6402ms)
bundle: start
bundle: finish
bundle: Writing bundle output to: ./Koudaitong/bundle/index.ios.jsbundle
bundle: Copying 5 asset files
bundle: Done writing bundle output
bundle: Done copying assets

當看到這樣的信息的時候,說明已經(jīng)打包成功了,再將生成的bundle文件夾以Create folder references形式加到工程里,然后就可以run了
Tip:

在真機調(diào)試時,需要在Edit Scheme里在Run模式里,將Build Configuration改為Release模式

0x05 遠程熱更新

這塊網(wǎng)上的方案大同小異,因為目前我們還是采取本地打包加載的方式,還未上熱更新,所以在這不好多做說明,等上了熱更新之后,我再來補充~

0x06 踩坑記錄

踩坑最多是應該是使用上的
1.RN系統(tǒng)的組件并不是所有都是共用的,比如segment支持iOS,不支持Android,Alert分為iOS和Android等等,所以還是要寫重復的代碼
2.ListView不支持iOS原生的滑動操作,需要使用第三方庫,但是第三方庫不能控制只編輯一個Cell
3.由于原先iOS和Android的代碼倉庫是分開的,所以接入RN時,JS文件也是跟著倉庫走的,這樣iOS和Android會存在重復代碼,并且目前兩個人分別接iOS和Android,寫JS時,有時并不共享,容易代碼寫著寫著就有差異了,偏離了Write Once , Run Anywhere的初衷

0x07 相關資料

React Native
React Native 中文網(wǎng)
匯集了各類react-native學習資源、開源App和組件
寫給 iOS 開發(fā)者的 React Native 學習路線
江清清的技術專欄

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

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

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