那些攜程火車票業務在RN實踐中踩過的坑

aqzp0628 8年前發布 | 10K 次閱讀 ReactNative 移動開發 React Native

火車票作為攜程體系下的重要環節,要兼顧良好的App用戶體驗及迅速的業務迭代,一個月左右一次App版本的節奏很難滿足,而React Native跨平臺、媲美原生App的用戶體驗以及無需發版的升級模式等等優勢無疑使人眼前一亮。

加上基礎團隊的Ctrip React Native框架對RN的性能優化、業務封裝以及拆包發布等的大力支持,火車票現已上線將近20個RN頁面,經歷了攜程App三個大版本的迭代與考驗。

本文將著重介紹React Native在攜程火車票產品中的應用,以及在RN實踐過程中遇到過的一些實際問題與解決方案。

本文大致分以下幾塊內容:

  1. 為什么選擇React Native
  2. React Native的現狀
  3. Ctrip React Native
  4. 攜程火車票的RN應用
  5. 踩過的坑及解決方案
  6. 各種問題及優化步驟

一、為什么攜程火車票要選擇React Native

作為目前攜程App為數不多的主要以原生開發方式為主的BU,我們也曾在Native跟Hybrid兩種方案中糾結過,一方面,原生的交互性能跟用戶體驗都是最優的,Hybrid則始終還未能突破性能瓶頸;但是另一方面,原生應用的更新又是一個很大的問題,尤其是iOS的AppStore,每次發布更新都需要漫長的審核周期,無法做到及時更新,并且上線之后的維護也非常麻煩。

對于業務高速發展、更新比較頻繁的火車票業務來說,攜程App一個月左右一次的大版本發布已然無法滿足需求。所以這個時候,基本兼顧到體驗與更新兩方面優勢的React Native的出現,無疑非常值得我們一試。

從性能體驗來說,Native最為優秀,RN基本接近,Hybrid則有些硬傷,因為Hybrid的View層實際上還是前端開發人員所熟知的DOM,而React Native則是以Virtual DOM的方式操作跟渲染相應平臺的UI組件,所以性能要高于Hybrid而不遜色于原生;而更新復雜度上,Hybrid跟RN都比較低,可以進行無需發版的bundle包更新,而Native則受限于應用商店的發布更新,復雜度最高;另外,從開發成本角度來看,Native開發周期相對較長,編譯調試也相對復雜,并且不能跨平臺,Android跟iOS需要維護兩套完全不同的代碼,RN雖然比Hybrid成本稍高,但是遠小于Native,可以做到大部分代碼的跨平臺復用。

二、 React Native 的現狀

從這上述的幾個方面來看,RN的各項表現都是非常優秀的。然而,目前React Native仍以每兩周一個版本的更新頻率快速變化中,到現在最新的0.35,仍舊是以零點幾的版本在定義,還不能算是一個完全成熟的框架,所以在實際應用過程中還有許多坑要趟。比如版本升級問題,有時候可以平滑過渡,有時候就沒那么輕松了。

就拿我們年初實踐的汽車票獨立版來說,1月份剛開始使用的時候,React Native剛開源Android版本不久,在Android上的兼容性還不是很樂觀,所以只在iOS上做了嘗試,我們最開始使用的是0.18的RN版本,從如何集成到現有的App里、怎么打全量包或增量包、以及bundle包的發布等等問題,當時都是組里的小伙伴跟iOS開發小伙伴自己一步步摸索過來的,但是在RN的快速更迭下,等嘗試升級到零點二幾的RN版本時,一言不合某些組件跟API就不能用了。

三、 Ctrip React Native

在攜程基礎團隊向我們各個業務團隊提出Ctrip React Native的支持時,我們幾乎毫不猶豫就確定要在攜程火車票里接入了,算是公司里RN應用比較早的BU,也是第一個使用RN進行完整的購票下單流程的BU。

CRN抹平了很多iOS跟Android組件的差距,比如DatePicker、SegmentedControl,提供各種攜程風格的組件和API,如HeaderView、HtmlText、Storage、Fetch等,使業務開發更專注于業務邏輯,不需要過多費精力在跨平臺的代碼實現上,也避免了各個BU的大量重復勞動;

此外,CRN對首屏渲染速度的提升,使iOS能在200ms,Android在400ms左右完成首屏渲染,以及對ListView的優化等都讓React Native向Native靠近了一大步;另外,包括對打包拆包、業務代碼發布及諸多工具比如crn-cli的提供和支持,都讓業務方更無壓力的接入RN應用。

可以說,CRN實現了把React Native作為一個純技術框架像業務框架的轉變。

四、攜程火車票的 React Native 應用

攜程App從6.17版本開始有業務試用React Native,到6.18也只有2個BU嘗試了3個RN頁面的上線。到6.19版本,火車票RN強勢加盟,7個BU,21個RN頁面里,單火車票一家就占了8個。再到6.20版本,在CRN的加持下,15個BU接入了50+的頁面,我們到達了17個,包括云搶票、改簽搶票、接送站等業務,并且這些頁面大部分都比較重要且有較為復雜的交互邏輯。

五、踩過的坑及解決方案

從一個火車票購票流程里粗略提取一下具體實現就有如下幾點:

1、從幾千個城市站點里選擇目標城市

2、在各種車次、座席、出發時間里篩選出合適的車次

3、乘客信息的填寫或者選擇

首先,購票流程第一步的站點選擇,就遇到了一個大坑:大家應該知道全國大大小小的火車站至少有數千個。而接觸過RN社區的小伙伴應該知道,React native的GitHub上有條臭名昭著的issue,官方至今還沒有完美的解決方案,就是ListView的性能問題。RN自帶的ListView是沒有回收機制的,這樣就使得RN在加載較多個數據的列表,App會非常吃內存。

我們一開始也嘗試用自帶的ListView來加載城市站點列表,幾千條純Text渲染下來時感覺還能勉強接受,但在加上了View布局、Touchable事件之后,當時連在iPhone 6,iOS 8.2的系統下也非常吃力,越滑越卡,甚至在較低配置的設備上還出現卡死甚至crash的現象。

實際上,城市站點選擇是一個變更頻率很低但是使用頻率很高的頁面,考慮到RN ListView的優化空間有限,一旦出現卡死,對火車票來說,結果基本是災難性的,所以我們最終選擇了復用原生已有的城市選擇頁面,由封裝成一個station component供RN使用。

現在我們考慮下另外一個重要場景的實現,從賬號里的常用乘客列表里勾選乘車人,同樣作為一個列表,是不是也可以像站點列表一樣復用native組件呢?我們也確實這樣考慮過,看起來好像省時省力、皆大歡喜。但其實正常情況下,個人賬號里的乘客數量遠小于上千條的站點數據,RN實現是可以負荷下來的。而且station component只需要傳給native一個已選的車站,然后native組件里操作完成后返回一個重新選擇的車站就可以了。

乘客列表并非簡單的單選,而且除了點選,還涉及到新增編輯等等更多的交互。由native封裝的話,涉及到的參數傳值未免太多。那么是不是可以跳轉到一個新的頁面,加載跟渲染數量較少的乘客列表比較方便實現呢?

從產品層面來說,火車票購買作為一個購票流程,每多跳轉一個頁面就有可能損失一部分轉化率,所以為了盡可能減少頁面的跳轉,我們采用了浮層形式在訂單填寫頁面里進行乘車人的選擇。然后問題又來了,在浮層彈出的動畫過程中加載并渲染乘客列表,很容易出現失幀卡頓的現象。如何解決?

我們是這樣考慮的,列表的加載并不是非要在浮層彈出的同時進行的,在進到訂單填寫頁時就可以預先加載好乘客列表數據,而只在浮層里做渲染即可。而且可以在不影響用戶視覺體驗的前提下,增加一些短時間的延遲。先完成浮層的彈出動畫,使用RN InteractionManager的runAfterInteractions等動畫結束之后渲染數據,并且設置了ListView的initialListSize跟pageSize均為1來逐條進行渲染。

前面也提到了,我們是希望盡可能減少頁面跳轉的,所以像車次類型、時間段篩選、座席選擇以及前面提到的乘客選擇,都是在各個頁面采用浮層形式來實現的。但其實浮層涉及到的問題相當多,這里仍舊以乘客選擇浮層為例。乘客浮層要實現的功能非常之多,首先,內部的列表是可以滑動的,上部分的陰影可以點擊消散浮層,并且內部的Item又要響應各種點擊操作。

拿到這么一個復雜的需求,最開始的做法是先給整個Modal加個TouchableHighlight事件來處理消散動作,然后內層加上TouchableWithoutFeedback來避免觸發外面的hideModal動作,

然后再在每個Item上加各自響應的Touchable事件響應勾選或者取消。然而,各種Touchable事件嵌套之后,實際效果就不在預期范圍內了:滑動內層列表的時候突然劃不動,點擊Item卻沒有反應等等,經過一番調試跟定位,終于確定,ScrollView滑動過程中很容易觸發到外層的TouchableWithoutFeedback,所以也就without feedback了。

問題一定位,解決方案自然也就出來了,Touchable的過多嵌套導致了問題的產生,那么就應該重新進行層級的布局,避免這些不應該的嵌套,不在整個Modal上加hideModal事件,而是抽出與浮層同級的View來做消散動作,內層只專注于List的渲染跟Item的點擊事件監聽。

六、各種問題及優化步驟

除了上述那些復雜的需求導致的復雜實現,還遇到過很多比較low的問題。大多數時候如果知道了一些屬性就完全可以避免很多問題的產生。

不知道大家有沒有遇到過setState方法剛設置完一個狀態,取這個狀態卻發現沒有生效的情況。這個異步方法讓我寫出過很多丑陋的setTimeout來嘗試解決。結果查閱React文檔后發現setState是有第二個參數的,這個參數就是設置完state之后需要立即調用的函數。

另外,合理使用key屬性跟各種React生命周期鉤子函數,如shouldComponentUpdate,可以優化很多性能問題。

再比如長按累加累減這樣的需求,單純的onPress跟onLongPress是不能實現的,需要結合delayLongPress直接觸發onLongPress,并且在onLongPress里進行setInterval的遞增或者遞減操作,然后在onPressOut解除press動作的同時clearInterval。

伴隨著業務的高速發展,邏輯越來越復雜,我們也逐漸發現了很多做得不好,值得優化的地方。像前面提到過的View層級過多導致的內存消耗、頁面卡頓。頁面邏輯復雜導致state設置過多,出現切換卡頓現象,并且狀態管理越來越混亂。而且現在很容易出現單個頁面動不動就一兩千,甚至幾千行代碼,維護起來非常困難,還有很多重復的代碼實現等等。

這些問題我們也在考慮從很多方面優化,像布局上盡可能減少層級嵌套,盡可能抽取能夠復用的組件,都是大家需要注意的點,狀態管理上我們也在考慮如Redux等一些好的解決方案的引入。

 

 

來自:http://techshow.ctrip.com/archives/1448.html

 

 本文由用戶 aqzp0628 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!