標籤:react native ios
Facebook三月份開源了React Native iOS平台的架構,讓移動開發人員和web開發人員都各自興奮了一把:native的移動開發人員想的比較多的估計是Facebook的那句:“learn once, write everywhere”,而web開發人員興奮的估計是,不需要學習iOS那陌生的OC或者swift語言,用自己熟悉的javascript語言就可以開發原生的移動APP了。那麼新推出的react native 能否承載的了兩大陣營的開發人員的期待了。本人及同事對react native做了一段時間的調研,心中漸漸有了自己的答案:
本文假定讀者熟悉iOS APP開發,但對web前端開發的知識匱乏(如本人一樣),在此基礎上試圖講清楚react native 到底是什麼;怎麼用;以及當前是否值得使用。
1.React Native 是什嗎?
首先,react native 到底是個什麼東東:它是Facebook開源的一套架構,其目的在於使用JavaScript語言編寫iOS native的控制項。更直白的講就是,你用JS(JavaScript,下同)寫的代碼通過react-native lib橋接到Xcode中寫的標準iOS程式中。在JS程式中,開發人員可以使用react native定義的一套和cocoa touch中UI控制項類等價的類,來完成UI層的開發工作,Xcode 編譯器 會利用 react-native lib將JS寫的代碼編譯成iOS原生的UI組件,展示了利用Xcode的視圖調試功能展示了JS代碼編譯的結果,可以看到,這些JS語言最終確實是被編譯成了UIView等對象,而不是H5介面經常使用的webview,有了這個認識之後,我們對react native就不在那麼陌生了。
綜上,你可以認為react native是一套能夠讓你使用Javascript而不是傳統的Objective-C或者Swift,編寫iOS APP 介面邏輯(MVC架構中的V層)的架構。使用過swift得同學甚至還會覺得JS和swift的文法上又不少雷同的地方(好吧,swift五仁月餅果然名不虛傳),比如定義一個變數都是用關鍵字var。這套架構最大的亮點有倆:
- 使用Javascript編寫應用邏輯,保持APP的原生性,而不像HTML5那樣對UI做出妥協,互動上有著優質的使用者體驗;
基於狀態驅動的介面更新機制,而不是傳統意義上通過MVC的Controller來集中控制介面的更新工作,將UI和自訂的某一個或者某幾個狀態變數函數綁定,當這些變數發生變化時,這些狀態變數便會驅動相應的UI模組重繪;
2.React Native 的開發環境使用React Native 首先要搭建環境,使用React Native 進行iOS 開發,其環境如所示:
可以直接按照Facebook 官網搭建文檔的指導來搭建React Native 開發環境,基本上沒有坑,能夠很迅速的完成。
除了運行環境的建立,還需要編寫 JavaScript 代碼的環境,Xcode 並非是最好的工具!Javascript 好用的編輯器網上很多,比如Sublime Text、atom等。
3. React Native 技術構成3.1基本構成元素
React Native 的庫同時包含了OC代碼和javascript 代碼,由這兩種代碼共同提供了一套用於構建介面UI系統的元素,包括但不限於:
基本上我們用JS寫代碼也是使用這些基本組件來構建我們的UI介面的。除此之外,你也可以在OC中,定製自己的模組,通過橋接的方式來在React Native 中使用。
3.2 React Native 中的事件響應系統
本地APP 和web 端的最大區別就是本地APP有著完美的事件響應系統,使用者能夠獲得更好的使用者體驗。在React Native中也提供了一套事件響應系統,扒一扒React Native 的源碼(在 ResponderEventPlugin.js 檔案中),能夠窺到React native事件響應的基本流程:
可見React native 的響應系統和Cocoa touch類似,一個view 如果想要對事件作出響應,它只需要實現函數:
- View.props.onStartShouldSetResponder: (evt) => true, - 當前view是否想作為touch 的響應者?
- View.props.onMoveShouldSetResponder: (evt) => true, - 當前view是否想作為move 事件 的響應者?
如果返回true,嘗試要變成第一響應者,那麼下面兩個函數中的一個會被調用:
- View.props.onResponderGrant: (evt) => {} -當前view是第一響應者,在這裡展示響應的互動效果(如背景色變化等)和事件觸發的其他邏輯;
View.props.onResponderReject: (evt) => {} - 其他view是第一響應者;
考慮到響應系統的複雜性,React Native 在對事件響應封裝的基礎上實現了一些抽象類別,如類似Cocoa Touch 中UIButton 的 TouchbleHighlight,你可以向使用view一樣將它放到你希望有互動效果的地方。我們通過來看看如何使用TouchbleHighlight:
3.3 React native UI更新邏輯
在Cocoa Touch 系統中,UI更新是典型的MVC模式:Controller 通過資料的變更,來更新view層的展示,但在React Native 中卻大相徑庭:React Native 通過狀態機器的機制來驅動整個view層的更新。在開始介紹這一塊之前不得不得先說一下React Native 的渲染方式:
從之前圖片中給出的程式碼片段中讀者也能窺出這種構建頁面的方法和HTML語言很像:通過標籤系統構建出分層的頁面邏輯(父子關係),布局代碼則採用CSS 的方式通過單獨的代碼來控制,這樣顯示的將商務邏輯和布局邏輯分開,使得整個代碼層次邏輯更清晰。
在React Native中,整個UI都是一個component樹:前面我們提到的ListView等UI組建都是一種具體的component,React Native通過將component樹編譯成一個virtual-DOM:虛擬文件物件模型,熟悉HTML的讀者可能對於這個DOM非常熟悉,沒錯,就是那個DOM,整個的UI的關係可以通過這個virtual-DOM很清晰的體現,而且更重要的是,React Native的UI更新邏輯也是依賴於這個樹來實現的:我們知道,籠統的講,一個頁面的更新,肯定是由資料的變更來驅動的,比如網路資料的更新或者是使用者觸摸導致的touch事件的發生(以及後續商務邏輯的跟進),那麼如何將這些資料的變更和介面的重新整理相綁定呢?如何知道哪一塊的資料變更後需要重新整理哪一塊的UI呢?要知道每次資料更新都重繪整個介面實在是一個吃力不討好的事情:不僅你的APP處於一種高負載運行狀態,而且使用者體驗也不好。React Native很巧妙的通過使用者提供的state變數維著一個狀態機器,通過將這個狀態機器來驅動virtual DOM樹的UI更新,如所示:
設計好了state資訊之後,React Native會根據代碼邏輯計算出那一塊的DOM組件需要進行更新,整個過程不需要開發人員來主動的幹預,開發人員只需要建立好state系統,並根據資料變化來維護state資訊即可,React Native會在後台為你做好這一切。
那麼一個component 中的state 是什麼呢?其實就是一個屬性,比如一個bool值或者數組或者任何其他JS支援的類型。一個component對象其實是包含了兩種類型的屬性的:property和state,前者主要是一些固定值的屬性,後者則是那些資料會發生變化,並且這種變化會導致介面某一部分重繪的屬性,比如列表頁裡面的資料來源等,如何區分一個屬性是應該被歸類為property還是state,Facebook 的 React 官網文檔:think in react上有詳細的介紹,這裡就不再贅述。
3.4 React Native的通訊機制
在講訴具體的通訊原理之前,我們首先來看一下,在代碼實現上是怎樣的。我們這裡說的通訊,很大程度上指的是我們用javascript寫的模組和OC寫的本地模組直接能否互相調用,如所示:
在具體深入細節之前,先想一下在純OC代碼中,如果一個類對象想直調用另一個類對象的簡單情況,那麼主調用模組必須要知道的是:
- 被調用模組的地址在哪兒?
- 被調用模組的方法名是什嗎?
需要傳入哪些參數進去?
也即一個完整的可執行檔調用地址必須有三個單元組成:(模組地址、方法名、參數),這三者缺一不可,這些資訊的獲得主要通過標頭檔機制和Cocoa Touch 運行時系統來提供。而React Native的通訊原理也是如此:
我們可以將React Native裡用JS代碼寫的模組和OC寫的本地模組看作是兩個互相陌生的城市,那麼很顯然,這兩個城市之間的人要想有效溝通,必須要彼此有一張對方城市的地圖才行。那麼在React Native世界裡,這兩張地圖就是模組配置表,它看上去大概是醬紫滴(一下部分資料來源於Bang’s blog):
既然雙方的通訊可以通過模組配置表來解決,那麼現在問題就簡化為:如何向編譯和運行時系統提供這張表了,為方便進一步分析,這兩我們將通訊氛圍JS模組調用OC本地模組和OC本地模組調用JS模組兩部分進行討論:
3.4.1 JS模組調用OC本地模組
一個OC模組也即OC寫的一個普通的類,在預設情況下是無法被Javascript 運行時系統捕獲並進而被調用的,它必須要向編譯及運行時系統提供or註冊它自己,並暴露出自己哪些屬性想被暴露出去,哪些方法可以被調用。訣竅就在於:
- 在聲明你的類的時候聲明自己遵循React Native提供的RCTBridgeModule協議(RCT是ReaCT的簡寫);
- 在實現的檔案中添加宏
RCT_EXPORT_MODULE();
對於你想暴露的方法使用RCT_EXPORT_METHOD()
宏進行封裝;
那麼在被調用的OC模組裡需要添加的代碼如下(代碼來自Facebook官網):
// CalendarManager.h#import "RCTBridgeModule.h"@interface CalendarManager : NSObject <RCTBridgeModule>@end
// CalendarManager.m@implementation CalendarManagerRCT_EXPORT_MODULE();......RCT_EXPORT_METHOD(addEvent:(NSString *)name location:(NSString *)location){ RCTLogInfo(@"Pretending to create an event %@ at %@", name, location);}@end
主調模組JS中得調用方式如下:
var CalendarManager = require(‘NativeModules‘).CalendarManager;CalendarManager.addEvent(‘Birthday Party‘, ‘4 Privet Drive, Surrey‘);
暴露出去的方法支援一下參數類型:
- string (NSString)
- number (NSInteger, float, double, CGFloat, NSNumber)
- boolean (BOOL, NSNumber)
- array (NSArray) of any types from this list
- map (NSDictionary) with string keys and values of any type from this list
- function (RCTResponseSenderBlock)
沒錯!就是這麼簡單! Awesome, isn’t it ? ^_^
除此之外,對於OC模組想暴露給JS模組的參數,可以通過constantsToExport
方法提供,該方法返回的是一個字典,範例程式碼如下:
In OC模組:
- (NSDictionary *)constantsToExport{ return @{ @"firstDayOfTheWeek": @"Monday" };}
在JS模組中可以直接擷取該函數返回的參數:
console.log(CalendarManager.firstDayOfTheWeek);
3.4.2 OC本地模組調用JS模組
OB本地的類對象要想調用JS模組裡面的方法,也必須首先遵循3.4.1中提到的RCTBridgeModel
協議,編譯器建立的模組配置表除了有上述OC的模組remoteModules
外,還儲存了JS模組localModules
。RCTBridgeModel
協議中提供了一個RCTBridge
屬性對象,該對象提供了訪問JS模組的方法,代碼如下:
/** * This method is used to call functions in the JavaScript application context. * It is primarily intended for use by modules that require two-way communication * with the JavaScript code. */- (void)enqueueJSCall:(NSString *)moduleDotMethod args:(NSArray *)args;
除了這種直接的調用方式之外,FacebookFacebook React Native 官網還提供了一種間接實現JS模組調用的方法,即通過RCTEventDispatcher
,以發送和接收訊息的方式實現調用,其原理圖如下:
具體實現代碼如下:
首先,在OC本地代碼中發送通知EventReminder
:
#import "RCTBridge.h"#import "RCTEventDispatcher.h"@implementation CalendarManager@synthesize bridge = _bridge;- (void)calendarEventReminderReceived:(NSNotification *)notification{ NSString *eventName = notification.userInfo[@"name"]; [self.bridge.eventDispatcher sendAppEventWithName:@"EventReminder" body:@{@"name": eventName}];}@end
其次,在JS模組中監聽EventReminder
通知,並添加相應的通知響應函數:
var subscription = DeviceEventEmitter.addListener( ‘EventReminder‘, (reminder) => console.log(reminder.name));...// Don‘t forget to unsubscribe, typically in componentWillUnmountsubscription.remove();
通過接收、發送通知的方式可以降低OC模組和JS模組的耦合度,而這種方式的實現同樣是通過RCTBridge
的直接調用方式來實現的,通過查看RCTEventDispatcher
中發送通知的sendDeviceEventWithName
的源碼實現即可發現:
- (void)sendDeviceEventWithName:(NSString *)name body:(id)body{ [_bridge enqueueJSCall:@"RCTDeviceEventEmitter.emit" args:body ? @[name, body] : @[name]];}
綜上,我們可以認為,OC模組直接調用JS模組的通訊方式主要通過RCTBridgeModel
協議中的RCTBridge
的sendDeviceEventWithName
對象來實現,除了直接調用該方法的方式,還可以採用通知的方式間接調用(從FacebookFacebook React Native 官網上僅介紹了通知的方式,也許能看出這是Facebook推薦的方式,至於具體的使用,還需開發人員依情境便宜行事)。
3.4.3 React Native 的通訊機制總結
通過以上兩節代碼示範,我們能夠很快的實現OC模組和JS模組的雙路通訊,基本上,無論是OC調JS還是JS調OC,其依賴的核心就是雙側模組提供的模組配置表(remote and local),至於其詳細實現原理,其參看Bang’s blog,這裡不再詳述。
3.5. React Native 的UI布局機制
無論是web頁面還是Native的本地頁面,在開發中UI布局都是很重要的一環,能否相容多重尺寸的頁面、裝置,是開發人員面臨的首要問題。隨著蘋果iPhone手機螢幕尺寸的越來越多樣化(目前至少有iphone4s/5s/6/6plus四種尺寸了),蘋果也越來越趨向於將UI布局重任放在了autolayout上面了,autolayout是一種典型的相對布局方法,開發人員通過可視化編輯環境xib或者storyboard對UI組件添加約束,運行時系統通過自動布局引擎,根據實際的螢幕尺寸,計算出UI中每個控制項的Frame資訊,從而實現UI的布局。
和Cocoa Touch 不同的是,React Native 在UI布局上採用了一個完全不同的系統: HTML CSS,也就目前主流網頁的流式布局方式,開發人員可以將每一個的布局資訊寫入單獨的style表中,將布局和商務邏輯分析,開發人員使用HTML CSS的文法完成布局資訊:
var styles = StyleSheet.create({ scrollView: { backgroundColor: ‘#6A85B1‘, height: 300, }, button: { margin: 7, padding: 5, alignItems: ‘center‘, backgroundColor: ‘#eaeaea‘, borderRadius: 3, }, buttonContents: { flexDirection: ‘row‘, width: 64, height: 64, }, img: { width: 64, height: 64, }});
除了使用標準的HTML CSS方式進行布局,React Native還支援Flexbox模組的布局方式,根據其官網說明,Flexbox Layout module旨在提供一種更高效的方式來布局,以動態決定在一個container中的子項的對其、置中、間隔甚至是尺寸大小的方式。
FlexBox布局對象只有兩類:容器(container)和容器內的子項(item),如所示:
對於容器和子項分別有六七個布局屬性關鍵字,羅列如下:
應用於Container的屬性: display flex-direction flex-wrap flex-flow // = flex-direction + flex-wrap justify-content align-items align-content應用於Item的屬性: order flex-grow flex-shrink flex-basis flex // = flex-grow + flex-shrink + flex-basis align-self
CSS中使用FlexBox只需要直接添加相應的關鍵字即可,如下代碼所示,具體每一種布局關鍵字的意義可以通過這篇文章擷取:
.flex-container { /* We first create a flex layout context */ display: flex; /* Then we define the flow direction and if we allow the items to wrap * Remember this is the same as: * flex-direction: row; * flex-wrap: wrap; */ flex-flow: row wrap; /* Then we define how is distributed the remaining space */ justify-content: space-around;}
HTML CSS Style的布局方式相對於iOS 的自動布局方式,其動態性更好,但只能通過純程式碼的方式來寫布局,著實讓人有些痛苦,而且對於廣大沒有web前端開發經驗的iOS移動端猿猿們來說,CSS的布局方式初一上手,還是覺得有些陌生:基本上你要換一種思維方式才能考慮清楚具體的布局細節,而且對於更複雜的動態情境,這種布局方式可能更難以實現和維護。
4. 目前,使用React Native的時機是否成熟
在React Native大熱的同時,我們要謹慎的探討一下使用React Native 的時機是否成熟這個問題。調研的這一段時間,我們發現有一下幾點值得注意:
4.1 JS模組和OC模組的資料互動只能通過字典(dictionary)傳遞
字典在OC模組中是一種比較鬆散的資料結構,如果考慮使用React Native負責UI介面的繪製工作,OC模組負責資料的處理,那麼二者的互動載體只能是字典。OC定義的model類對象(如使用core data時建立的model 對象)無法直接傳遞給JS模組使用,還必須要提前轉為字典才行,這無疑多了一層處理邏輯,勢必會帶來一些潛在的風險。
以使用CoreData儲存資料為例,我們的整個資料層的互動將是這樣子的:
這種只能通過字典來傳值的限制,就使得我們沒法直接將OC模組中的資料對象直接作為JS模組裡面驅動頁面更新的state屬性。我們將不得不添加一個中介層來轉換資料的這種變化已映射到JS模組裡驅動UI層的更新。
4.2 React Native的learn once,write everywhere 的實現還有待時日
Facebook在力推React Native的時候強調它的最大特點是:Learn once, write where. 但目前的實際情況是,React Native Android 預計2015年10月才發布,這對希望三端(Web/iOS/Android)架構一致的使用者而言也算個風險。而且細看React Native iOS 架構裡面,還有很多和iOS本地模組緊密耦合的模組,比如以iOS結尾的若干component都是iOS才有的,使用了這些模組的代碼,將來想直接在Android上運行恐怕是不可能的事情,那麼React Native在這一點上離真正的跨平台還有不少路要走:
**COMPONENTS**ActivityIndicatorIOSDatePickerIOSImageListViewMapViewNavigatorNavigatorIOSPickerIOSScrollViewSliderIOSSwitchIOSTabBarIOSTabBarIOS.Item
4.3 React Native 中Listview 效能問題
在github的React Native有一個issue格外讓人擔憂: ListView renders all rows? 其中有幾個評論揭示了這樣一個事實:React Native的ListView可能一次性的渲染了所有的rows(cells):
@ide I‘m a noob at instruments profiler... So here‘s brief summary from me taking a look at it.cpu profile looks like most of the time is spent here (recursing through subviews), in RCTView.m :- (void)react_updateClippedSubviewsWithClipRect:(CGRect)clipRect relativeToView:(UIView *)clipViewIn memory profile major causes of persisted memory (645 MB total) are:VM: CG Graphics Data (410 MB)VM: CoreAnimation (141 MB)VM: JS Garbage Collector (61 MB)...Unmount the ListView component, and total persisted memory drops to 74 MB total:VM: JS Garbage Collector 58 MBVM: CoreUI image data...
Kureev commented 24 days agoIt‘s totally insane: my iPhone 5c crashes after 700 list items. If I‘m going to write a chat - it‘s blocking for me.Also I got a lot of "Cannot find single active touch"
samfriend commented 14 days agomy < ListView pagingEnabled={true} onEndReached={this.loadAnotherFiftyArticles} >50 rows/pages of < Image / > < Title/ > < Description/ >3rd Load append (total 150)Received memory warningReceived memory warningReceived memory warningCrash Physical iPhone 6 PlusAlso I got a lot of "Cannot find single active touch" time to time
為了證明網友們的擔憂,這裡我們用Xcode的view透視工具看了一下React Native 官網提供的Demo: UIExplorer的ListView,看看到底是否是一次性重繪了所有的rows,展示了這個Demo運行時的介面:
然後我們使用Xcode 的 Debug View Hierarchy來查看這個試圖的介面層級結構,結果讓我們觸目驚心:它果然對所有的row進行了繪製!
再看一個scrollView的情況,好吧,看完我整個人都不好了:
如果情況真是這樣,那就這一條就足有讓我們有理由選擇放棄使用React Native了,至少暫時是!
4.4 React Native 的UI布局系統不盡如人意
我們知道,React Native採用的web前端的HTML CSS 也即流式布局。整個UI介面都是通過樹形結構構建起來,布局也是基於此,而且需要全手動打造。使用過純程式碼的方式寫iOS 上的autolayout 布局的童鞋相比一定被這種非可視化的布局方式深深刺痛吧:你必須要在腦海裡將所有的UI展示效果轉換成為紛繁複雜的布局約束。
autolayout可視化的布局是React Native所最欠缺的
採用CSS布局的另一個劣勢是,相比較於傳統的Native 布局方式,精確性控制的不是很好,最終布局效果可能和設計師的初衷相差甚遠。在自動布局autolayout 中我們可以通過sizeclass針對橫豎屏做定製化的布局工作,但是使用CSS目前來看還沒法實現這種方式。
4.4 總結
通過上訴的分析,我們發現React Native在效能、開發便利性等方面還存在很大的不足。目前來看,它還沒實現它所倡導的“Learn Once, Write Everywhere”的目標,但帶來的問題卻不少,別的不說,單就ListView的效能問題就是一個最大的瓶頸。React Native還處在一個初期摸索階段,它的下一階段發展如何,還要看它的老東家Facebook接下來的動作。因此,筆者建議目前已有的開發項目中不要冒險採用React Native 技術,保持技術跟進即可。
React Native 調研報告