React Native在特讚的應用與實踐
作者:苑永誌
作者介紹:現任特讚大前端負責人。技術涉獵比較廣泛,曾在大麥網擔任高級Java研發工程師;後以前端工程師身份加入特讚,基於React技術棧構建開發前端項目,並使用React Native開發特讚移動APP;目前正在使用Node.js開發和維護特讚服務網關,希望Node.js能夠在更輕量級的微服務架構中發揮重要作用。
一、需求緣起
特讚是在2016年末才開始著手APP開發的。記得那是距離過年還有一個月的時候,產品突然提出一個需求:咱們做一個iOS應用吧,快過年了,給設計師一個新年禮物。其實當時我的內心是拒絕的,卻也沒辦法隻能麵帶微笑說:“好啊,我們盡量吧…”iOS工程師是別指望了,既然是大前端,那就什麼都得做。
於是我們開始調研蘋果應用的審核發布流程、熱更新,以及具體的實現細節。為了趕上蘋果的審核進度,兩周時間我們發布了第一個初始版本,然後在接下來的兩周時間完成了剩餘所有功能的開發,並通過熱更新完成線上發布。也就是說,我們前後用了不到一個月的時間,完成了特讚原生iOS APP的開發。
APP包括項目列表頁、項目列表、報價列表、報價詳情、對話頁等。如圖所示是對話頁,設計師和客戶可以進行實時溝通,這也是我們APP的一個亮點功能。
二、APP開發技術的選型
前端的同學都知道,使用React的話,是通過聲明式的方式定義組件,然後通過虛擬DOM在瀏覽器環境下進行UI的渲染和數據的加載。React已經應用到了PC頁麵、移動頁麵,甚至服務端渲染等場景,隨著React Native的推出,前端同學更是通過React擁有了開發iOS和Android應用的能力。
這真的是原生應用!就像 React的官方slogan:Learn once, write anywhere。翻譯成中文就是:“一次學習,到處挖坑”。 先挖好坑,至於誰去填,這就是後話了 : )
為什麼選擇React Native呢?
首先,上文提到,通過React Native開發的應用,隻要優化得當,幾乎可以無限接近Native應用的交互操作體驗。絲滑的手感,讓人愛不釋手。
其次,React Native開發出來的應用,其功能和性能都很強。
第三,React Native可以直接通過Chrome進行調試,這對前端開發人員來說簡直就是一個福音。
第四,我們團隊本身使用的就是React技術棧,所以React Native是一個很自然的選擇,適應的過程也非常短。
最後,React Native除了可以像WEB一樣進行開發,還擁有WEB一樣的發布能力,隻要通過熱更新就可以簡單做到。這一點也是影響我們抉擇的一個因素。
三、React Native開發過程中的主要問題
本次分享主要圍繞React Native開發前後涉及到的方方麵麵進行探討。包括
- 開發前我們會重點考慮的調試、路由、數據管理、組件選型等問題;
- 開發過程中,要解決的動畫、緩存、手勢、支付等問題;
- 業務功能開發完畢後,要關注的消息推送、異常監控、熱更新、性能消化等話題。
3.1 調試
如圖,當調試工作能夠通過Chrome的DevTools進行時,一切似乎都變得簡單起來了。我們可以進行熟悉的斷點調試、變量審查;還可以結合React、Redux的Chrome插件直觀地查看組件結構和整個工程的數據變化。
這裏有兩個坑值得一提:
- 一是一旦應用live reload之後,斷點調試就會失效,要重新reload應用才能恢複;
- 二是調試過程中保證DevTools所在TAB在最前麵,否則APP會瞬間變得卡頓。
3.2 路由
React在WEB上可以通過react-router來管理路由,而在React Native中,路由管理變得更簡單。利用Navigator組件,我們把所有的Scene、場景或頁麵通過一個堆棧管理起來,頁麵的操作就是簡單的出棧入棧操作。
比如我們最初處於home頁,接著push到對話列表頁,再push到對話詳情、項目列表頁,然後又可以pop回對話詳情頁。當然實際情況可能還要複雜一點。比如往回跳多個頁麵、跳回指定頁麵等,這一切都是針對一個堆棧來進行操作的,所有這一切都可以用類似下圖的這一行代碼來實現。
通過Navigator組件對象的引用,我們可以跳轉到對話列表(chat)頁麵,與此同時,我們帶上項目ID、設計師ID等參數,這些參數在chat頁麵中很容易獲得。
3.3 數據管理
使用React做過Web開發的同學知道,我們往往通過Redux把數據集中管理起來。在React Native中不同的點在於:我們希望數據能夠被持久化,以免每次應用重啟之後,所有數據又要重新加載。AsyncStorage能夠很方便地跟Redux集成到一起,下文會介紹AsyncStorage的具體應用。
3.4 組件選型
組件也是我們是否選擇一個前端框架的重要考慮因素。React Native框架本身給我們提供了很多實用的組件,比如列表、觸摸操作、導航、圖片等,但這些還遠遠不夠滿足我們的使用需求,所幸的是React Native社區非常活躍,有很多組件可供選擇和直接使用,比如輪播、側滑、文件上傳……
3.5 動畫
以上四部分都是一些準備調研,真正的挑戰才剛剛開始。
如圖所示是一個報價列表的頁麵,在使用中動畫效果沒有卡頓和掉幀的現象。在對話列表頁內部,還可以通過上滑直接將列表中的一項放大成全屏,繼續上滑TabBar還可以置頂,並且可以進行滑動切換操作,下滑又可以退出全屏。
在WEB頁麵中,我們通過CSS3動畫(比如transition、animation等),可以方便地實現很多過渡和動畫效果。JS層麵,我們也可以使用requestAnimationFrame來進行動畫操作,實際上很多動畫庫就是基於它來進行封裝的。在React Native中沒辦法通過過渡、動畫幀來實現動畫,但React Native框架給我們提供了更為精細的動畫支持。如下圖所示:
Animated組件能夠用於實現精細體驗、友好的交互動畫。我們可以通過定義特定的Value作為動畫變化的參數,而這些動畫可以是隨時間漸變、彈跳或者有加速度的。動畫可以是單個的,也可以是多個通過並行、順序、交錯的方式進行組合的。
這些動畫都必須應用在特定的動畫組件之上,除了內置一些動畫組件,我們還可以根據需要自定義動畫組件。在動畫的過程中,我們還可以進行跟蹤,根據Animated.event對象獲得動畫過程中的相關變量。
實際上,上文提到的這些用以實現一些常規的動畫效果已經綽綽有餘了。下麵我們通過幾個代碼片段,直觀地感受一下。
首先我們定義一個動畫的值opacityValue用於記錄透明度的變化,然後將這個值應用於Animated.Image組件的style屬性之上,這跟我們書寫內聯樣式沒有什麼區別,隻不過opacity的值是我們定義的特定類型的動畫值。
那我們如何觸發這個圖片的透明度動畫呢?
我們使用Animated.timing對透明度進行一個線性的操作,第一個參數是我們定義的值,第二個參數是指定動畫完結時的值,持續時間,變化虛線。
綜合起來就是說,在4秒鍾之內,圖片的透明度將會由0線性變化成1。
在動畫完成之後,我們還可以在回調中做一些事情。這是一個很有用的操作,我們可以把動畫和業務操作錯開來,避免動畫和數據操作同時占用資源,造成卡頓。
除了上文介紹的精細控製,React Native也提供了粗粒度的動畫控製 LayoutAnimation,我們可以把多個動畫值一次變化到另一個狀態,具體的動畫效果交由框架去完成。簡單的動畫可以這麼做,但是一旦複雜起來,我們還是會更加傾向於使用Animated去做控製。
React Native的組件還為我們提供了一個很有意思的接口:setNativeProps,顧名思義就是設置原生組件的屬性,類似於我們在WEB中直接操作DOM。結合requestAnimationFrame會有意想不到的驚喜,但一般情況下我們不建議大家這麼使用,因為脫離了框架的操作會讓程序變得失控,除非你自己知道自己在幹什麼,還有別忘了寫上顯著的注釋!
3.6 手勢
一般情況下,動畫都是伴隨著手勢產生的。React Native中很多組件都對手勢操作進行了封裝。比如Touch打頭的組件,對觸摸操作進行了處理,ScrollView中的onScroll對滑動操作進行了封裝。
React Native中的事件也是分為捕獲、目標和冒泡三個階段,在各個階段都可以進行一些操作,比如判斷是否需要響應事件,我們可以在子組件之前響應事件,也可以在子組件之後響應事件;還可以處理簡單的觸摸事件和滑動操作。
有的情況下,我們需要把多個手指的操作協調成一個單點操作,這時我們需要使用PanResponser。這與前文提到的事件處理非常類似,因此不再贅述。
通過一個例子直觀感受手勢操作的處理。
我們不區分單個或多個手指,因此我們使用PanRespnsor來處理,在onMoveShouldSetPanResponser中,我們判斷手指(可能是一個,也可能是多個) 滑動時,組件是否需要響應該手勢。
接下來是一些業務判斷,比如正在加載數據、水平方向上有滾動、正處於加載完畢提示頁、水平位移大於豎直位移時,不需要處理;如果是全屏,向下滑動時,需要響應;如果是列表狀態,向上滑動需要響應。當然,這僅僅是手勢響應的一部分,還需要其他很多配合才能將手勢和動畫組合起來。
前文提到AsyncStore可以和Redux配合起來使用,實際上,所有需要進行持久化緩存的數據都可以使用AsyncStorage來進行操作。
為什麼是AsyncStorage?雖然localStorage也能進行持久化緩存,但它的接口是同步的,在JS單線程模型中,耗時的阻塞IO操作令人非常鬱悶。所以AsyncStorage正是為了取代localStorage而出現的,使用異步在JS中是最佳實踐。
不過,我們一般不建議直接使用AsyncStorage,因為它存儲的內容都是字符串,每次操作的時候都要進行序列化、反序列化,同時還要捕捉異常。所以我們通常把存取操作封裝起來,如果有必要,也可以加上命名空間來區分不同的數據資源。
APP中,除了數據緩存以外,圖片等資源的還原也顯得格外重要,用戶不希望一個10M的APP在多次使用後莫名其妙地變成了1個G,占用了用戶的內存,也浪費了“不菲”的流量。對於圖中這樣地址不會變更的圖片,我們隻要使用一個圖片組件就可以將圖片資源緩存起來,避免重複下載。
對於七牛資源這種動態變化的地址,剛才的方案就不可行了。為了保證設計師資源的安全性,我們每次給到客戶的資源都是一個帶有token標識的鏈接,而這個鏈接很快就會過期,這就意味著同樣一張圖片也會重複進行下載,這是一件很恐怖的事情。不過我們看到每個七牛資源的key是不變的,比如圖中的“5483389ab...”部分,那就可以利用這個key去判斷是需要重新下載,還是可以從文件係統中直接讀取該圖片。
具體的實現,我們用到了react-native-fetch-blob組件,它可以配置資源下載後的存儲路徑,然後根據這個路徑從本地文件係統中直接讀取到該圖片,從而實現了對非固定路徑資源的緩存。
3.8 支付
為了實現資金的閉環,支付是必不可少的一個功能。在Web端,我們使用Ping++的支付服務,當我們知道Ping++沒有React Native的SDK時嚇了一跳,萬幸的是他們內部正在研發React Native的SDK,在經曆了各種坎坷之後我們的第一筆錢終於付出去了。
有了開發前的準備工作,也攻克了開發過程中的種種困難,一個APP的開發工作就基本完成了,可就在你以為大功告成的時候,卻發現它竟然會崩潰、卡頓,而且還不容易定位到原因。因此在業務功能開發完畢後,還要關注消息推送、異常監控、熱更新、性能消化等話題。
3.9 消息推送
消息推送是必備功能,其流程比較簡單。如果是iOS應用,首先我們需要使用蘋果開發者賬號申請一張證書,iOS APP可以獲得設備token, 後台結合token和證書可以申請消息推送的請求,獲得授權之後,就可以調用推送接口,直接推送必要的消息既可。
在開始討論異常監控之前,我們最好先了解異常發生的原因。上圖是React Native在Android和iOS兩個平台上的架構,最上層是打出的安裝包,可以運行在對應的操作係統上。中間部分是我們書寫的JS代碼,再往下是原生組件和核心類庫部分。
以上,我們可以大致看出可能的異常來源:
- 用戶業務代碼產生的異常,這部分屬於JS異常;
- JS模塊和Native相互調用可能產生的異常,我們稱之為Native異常;
- 還有組件渲染過程中產生的異常,這部分叫做UI異常。 我們分別來看各自異常的處理方式。
全局的JS異常可以通過react-native模塊中的ErrorUtils工具類來捕獲,也可以通過模塊react-native-exception-handler來統一處理,比如記錄的日誌係統。
通過React Native Android框架的源代碼,我們可以找到對應Native異常和UI異常的錯誤處理,這就意味著我們可以通過修改源碼來自定義異常的處理方式。不過一旦升級React Native版本,就需要做出相應的修改,這會帶來維護成本。
如果你認為這些方法太複雜,那也可以使用像bugly這樣的異常監控平台,它不僅可以隨時監控APP的崩潰、卡頓和錯誤等發生的情況,還可以清晰地知道用戶和手機的分布情況。
3.11 熱更新
JS可以通過應用內的JS引擎動態解釋執行。所以無論源代碼做了多大的修改,隻要無需構建,我們都可以通過熱更新動態推送到用戶的手機上。這個過程大致如下:當用戶的APP啟動或喚醒的時候,檢查APP內的bundle和圖片資源是否是最新的,如果不是,則從熱更新服務器加載最新的bundle和圖片。實際情況可能要稍微複雜一點。
在用戶的APP這邊,我們需要檢查bundle版本,進行下載、解壓、reload操作。如果是增量更新,還要進行bundle合並。對於熱更新服務器,我們要針對Android和iOS提供不同的bundle版本,每次發布或者更新時都需要打包發布對應的bundle版本;如果是增量提供bundle patch版本,還要對bundle進行拆分。
所有這些都做完的工作量可能甚至要超過APP開發本身的工作量了,那有沒有現成的解決方案呢?
解決方案顯然是有的。我們目前使用的是微軟提供CodePush熱更新服務,隻要簡單注冊配置,然後在APP端引入CodePush的客戶端插件,就可以完成上文提到的相關工作。它還提供了版本的出錯回退機製。不過訪問速度是它的缺點,如果每次更新需要幾十M甚至上百M,那就要斟酌一下了,目前來看,我們使用起來感覺還是很好的。
3.12 性能優化
最後也是最重要的一塊內容是性能優化。所謂天下武功,唯快不破,如何讓我們的應用快起來是性能優化的關鍵。下文將從加速速度、滾動速度和響應速度三個方麵來提供一些優化建議。
3.12.1 加速速度
加速速度帶給用戶的是既視體驗,如何避免白屏、把數據和頁麵第一時間呈現給用戶是關鍵。首先我們考慮的是從緩存中加載往次訪問數據,然後異步加載最新的數據。如果是對實時性要求並不是很高的數據,我們可以使用Redux中統一管理的數據,前文提到過,這一部分數據我們也做了持久化緩存。
3.12.2 滾動速度
大列表是在應用中必會出現的部分,而列表本身操作又特別複雜和頻繁,這就導致列表內的組件會重複渲染,從而帶來極大的性能消耗。通過使用shouldComponentUpdate可以判斷組件是否需要渲染,從而阻止不必要的渲染——這很簡單,也非常有效。
除此之外,ListView列表組件本身也提供了一些配置來提高渲染效率,比如首屏加載的數量、可視部分的數量。如果你剛剛開始使用React Native,恭喜你,你可以使用FlatList組件,列表的操作變得簡單,性能也非常出色。
如何提升響應速度?在Navigator頁麵切換後,如果需要通過網絡加載數據,很容易造成轉場動畫的卡頓,這是因為業務邏輯和UI渲染邏輯出現了交錯。
React Native提供了InteractionManager幫助我們去處理這樣的情況,隻要把業務邏輯放到runAfterInteractions方法的回調中去執行就可以確保轉場動畫的完整展示。按鈕點擊或其他的一些操作可能也會出現類似情況,處理的方式也是大同小異,requestAnimationFrame的回調使得交互和業務邏輯能夠錯開。實際上,卡頓丟幀的始作俑者都是JS單線程,如果使用setNativeProps就可以跳出這個模型。但還是那句話,除非你知道自己在做什麼,否則不要這麼做。
當然還會有很多其他的優化建議,沒有辦法在本文中完整列舉下來。實際上,如果你按照上麵給出的建議去做了優化,APP的體驗應該已經很不錯了。但凡事無絕對,實在快不起來的時候,別忘了把Loading效果用起來,這會讓用戶願意多等一會。
原文發布時間為:2017-09-07
本文作者: 苑永誌
本文來自雲棲社區合作夥伴“中生代技術”,了解相關信息可以關注“中生代技術”微信公眾號
最後更新:2017-09-07 13:03:03