React Native in Tezign
苑永志 特贊前端總監,技術愛好廣泛,做過Java,寫過頁面,正在搞Node.js。目前負責特贊前端團隊的人才培養和特贊服務閘道器的開發、維護。喜歡羽毛球、籃球等各種球類運動。
這是苑永志老師在7.8日iTechPlus前端大會上講的內容,整理如下:
特贊是從2016年末才開始著手APP的開發。記得那是距離過年還有一個月不到的時候,產品突然提出一個需求說,咱們要不做一個iOS應用吧,快過年了,給設計師一個新年禮物吧。當時我的內心其實是拒絕的,於是我面帶微笑著說:“好啊,我們儘量吧。。。”。iOS工程師是不指望了,既然是大前端,那就得什麼都能搞……於是,我們開始調研蘋果應用的稽核釋出流程,熱更新,具體的實現細節。為了趕上蘋果的稽核,兩週的時候,我們釋出了我們的第一個初始版本,剩下的2周時間,我們完成了剩餘所有功能的開發,並通過熱更新發布到了線上。我們用不到一個月的時間,完成了特贊原生iOS的開發。
好,來感受一下,我們做出來的一些成果。第一幅是專案列表頁,然後是專案列表,報價列表,報價詳情(看到上面的金額沒有,在特贊做設計師還是很是賺頭的哦 :) )。這個是對話也,通過這個,設計師和客戶可以進行實時的溝通,這也是我們APP的一個亮點功能。
可能前端的同學都知道,使用React的話,我們是通過宣告式的方式定義元件,而後通過虛擬DOM在瀏覽器環境下,進行UI的渲染和資料的載入。React的使用已經應用到了PC頁面,移動頁面,甚至服務端渲染等場景下。隨著React Native的推出,我們前端的同學通過React更是擁有了開發iOS和Android應用的能力。記住,這真的是,真的原生應用!還記的React的官方slogan嗎?Learn once, write anywhere. 翻譯成中文就是,一次學習,到處挖坑 : ) 先挖好坑,至於誰去填,這就是後話了 : )
至於為什麼選擇React Native呢?首先呢,剛才提到過,通過RN開發的應用,只要優化得當,幾乎是可以無限接近Native應用的互動操作體驗的,所以說,手感非常絲滑,讓人愛不釋手。然後就是,RN開發出來的應用,他的功能和效能都是很不錯的。還有一點,對我們前端開發人員來說,真的是一個福音,那就是,RN可以直接通過Chrome進行除錯,分分鐘讓你欲罷不能。退一步說,因為我們團隊本身是React技術棧,所以選擇RN是一個很自然的過程,適應的過程也非常的短暫。最後,也是影響我們抉擇的一個因素就是,RN除了可以像WEB一樣進行開發,還可以擁有WEB一樣的釋出能力,只要通過熱更新就可以簡單的做到,後面的部門我們會著重提到這個特性。
本次的分享呢,主要圍繞RN開發前後我們涉及到的方方面進行探討。包括開發前我們會重點考慮的除錯、路由、資料管理、元件選型等問題;開發過程中,我們則是要解決動畫、快取、手勢、支付等問題;業務功能開發完畢之後,則要關注訊息推送、異常監控、熱更新、效能優化這些可能更加重要的話題。好,那我們就一一展開來討論這些東西。
相信前端的同學看到這個圖都會感覺的很親切,當除錯工作能夠通過Chrome的DevTools進行是,一切都似乎變得簡單起來了。我們可以進行熟悉的斷點除錯,變數審查;我們還可以結合React、Redux的Chrome外掛直觀的檢視元件結構和整個工程的資料變化。儘管如此,還是有2個小坑值得一提,一旦應用live reload之後,斷點除錯就會失效,要重新reload應用才能恢復;另外一個就是除錯過程中保證DevTools所在TAB在最前面,否則APP會瞬間變得卡頓。
React在WEB上可以通過react-router來管理路由,不夠在RN中,路由管理變得更簡單。通過Navigator元件,我們把所有的Scene、場景、或者頁面通過一個堆疊管理起來,頁面的操作就是簡單的出棧入棧操作。比如最初我們處於home頁,接著我們push到對話列表頁,再push到對話詳情、專案列表頁,然後我們又可以pop回對話詳情頁。當然實際情況可能還要複雜一點,
比如往回跳多個頁面,跳回到指定頁面等等,這一切都是針對一個堆疊來進行操作的。所有這一切我們都可以用類似下面的一行程式碼來實現:
通過Navigator元件物件的引用,我們可以跳轉到對話列表(chat)頁面,與此同時,我們帶上專案ID,設計師ID等引數,這些引數我們在chat頁面中很容易獲得。
使用React做過Web開發的同學知道,我們往往通過Redux把資料集中管理起來。在RN中,不同的點在於,我們希望資料能夠被持久化,以免每次應用重啟之後,所有資料又要重新載入。AsyncStorage能夠很方便跟Redux整合到一起,後面會講到AsyncStorage的具體應用,這裡先不展開來說了。
OK,下面是元件部分。元件也是我們是否選擇一個前端框架的重要因素。RN框架本身給我們提供很多實用的元件,像列表、觸控操作、導航、圖片等,這些還遠遠不夠。所幸的RN社群是異常活躍的,真是隻有我們想不到,沒有做不到的元件 : ) 大家可以簡單瀏覽一下,像輪播、側滑、檔案上傳…… 我們直接拿來主義就可以了。
以上的部分,我們都是在一些準備調研,真正的挑戰才剛剛開始。大家來看,這是一個報價列表的頁面,動起來了!沒有卡頓,沒有掉幀,有木有,這手感,大家有沒有想上手摸一把 :) 在對話列表內部,還可以通過上滑直接將列表中的一項放大成全屏,繼續上滑,TabBar還可以置頂,並且可以進行滑動切換操作,下滑又可以退出全屏。我剛拿到這個互動需求之後,我是一臉懵逼的。那我們就一起來看一下通過RN的動畫和手勢能不能實現這些互動。
在WEB頁面中,我們通過CSS3動畫,像transition、animation等可以方便的實現很多過渡和動畫效果。JS層面,我們也可以使用requestAnimationFrame來進行動畫操作,實際上很多動畫庫就是基於它來進行封裝的。在RN沒辦法通過過渡,動畫幀來實現動畫,但是RN框架給我們提供更為精細的動畫支援。我們來一張圖:
Animated元件能夠用於實現精細體驗,友好的互動動畫。我們可以通過定義特定的Value作為動畫變化的引數,而這些動畫可以是隨時間漸變的,彈跳的,或者有加速度的。動畫的可以是單個的,也可以是多個動畫進行組合,比如說並行、順序、交錯的方式進行組合。這些動畫都必須應用在特定的動畫元件之上,除了內建一些動畫元件,我們還可以根據需要自定義動畫元件。動畫的過程中,我們還可以進行跟蹤,根據Animated.event物件獲得動畫過程中的相關變數。實際上,通過剛才說的這些,實現一些常規的動畫效果已經遊刃有餘了。下面我們通過幾個程式碼片段,直觀的感受一下。
首先我們定義一個動畫的值opacityValue用於記錄透明度的變化。然後將這個值應用於Animated.Image元件的style屬性之上,這跟我們書寫內聯樣式沒有什麼卻別,只不過opacity的值是我們定義的特定型別的動畫值。那我們如何觸發這個圖片的透明度動畫呢?我們使用Animated.timing對透明度進行一個線性的操作,第一個引數是我們定義的值,第二個是指定動畫完結時的值,持續時間,變化虛線。綜合起來就是說,在4秒鐘之內,圖片的透明度將會由0線性的變化成1。在動畫完成之後,我們還可以在回撥中做一些事情。這是一個很有用的操作,我們可以把動畫和業務操作錯開來,避免動畫和資料操作同時佔用資源,造成卡頓。
除了剛才介紹的精細控制,RN也提供了粗粒度的動畫控制 LayoutAnimation,我們可以把多個動畫值,一次變化到另一個狀態,具體的動畫效果交由框架去完成。簡單的動畫,我們可以這麼做,但是一旦複雜起來,我們還是會更加傾向於使用Animated去做控制。
RN的元件還給我們提供了一個很有意思的介面叫做 setNativeProps,顧名思義就是設定原生元件的屬性,這就類似於我們在WEB中直接操作DOM,結合起requestAnimationFrame有可能讓你爽到。一般情況下,我們都不建議你這麼去用,因為脫離了框架的操作會讓程式變得失控,除非你自己知道自己在幹什麼,還有就是別忘了寫上顯著的註釋!
一般情況下,動畫都是伴隨著手勢產生的。RN中很很多元件都對手勢操作進行了封裝,比如Touch打頭的元件,對觸控操作進行了處理,ScrollView中的onScroll是對滑動操作進行了封裝。RN中的事件也是分為捕獲、目標和冒泡三個階段,在各個階段,我們都可以進行一些操作,比如判斷是否需要響應事件,我們可以在子元件之前響應事件,也可以在子元件之後響應事件;我們可以處理簡單的觸控事件,也可以處理滑動操作。
有的情況下,我們需要把多個手指的操作,協調成一個單點操作,這是我們需要使用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元件,他可以配置資源下載後的儲存路徑,然後根據這個路徑,就可以從本地檔案系統中直接讀取到該圖片,從而實現了對非固定路徑資源的快取。
為了實現資金的閉環,支付是必不可少的一個功能。在Web端,我們使用的Ping++的支付服務,當我們知道Ping++沒有RN的SDK時,差點嚇尿了 :) 萬幸的他們內部正在研發的RN的SDK,而且最終經歷各種坎坷,把我們的第一筆錢付出去了。所以,做前端同學,如果發現你身邊有搞Native的,趕緊和他成為好基友,關鍵的時候,他會拔刀相助,救你於水深火熱之中的。
有了開發前的準備工作,也攻克了開發過程中的種種問題,一個APP就基本開發出來 了,它應該包含了你需要的主要業務流程,這是我們終於可以細細把玩,愛不釋手了。然而好景不長,你會發它他竟然會崩潰,卡頓,而且也不容易定位到原因;訊息推送也是必備功能。
訊息推送的流程比較簡單,如果是iOS應用,首先我們需要使用蘋果開發者賬號申請一張證書,iOS APP可以獲得裝置token, 後臺結合token和證書可以申請訊息推送的請求,獲得授權之後,就可以呼叫推送介面,直接推送必要的訊息既可。
在開始討論異常監控之前,我們最好了解異常發生的原因。這是RN在Android和iOS兩個平臺上的架構,最上層是打出的安裝包,可以執行在對應的作業系統上。中間部分是我們書寫的JS程式碼,在往下是原生元件和核心類庫部分。以上,我們可以大致看出可能的異常來源:使用者業務程式碼產生的異常,這部分屬於JS異常;JS模組和Native相互呼叫可能產生的異常,我們稱之為Native異常;還有元件渲染過程中產生的異常,這部分叫做UI異常。我們分別看一下各自異常的處理方式。
全域性的JS異常,可以通過react-native模組中的ErrorUtils工具類來捕獲。也可以通過模組react-native-exception-handler來統一處理JS異常,比如記錄的日誌系統。
通過RN Android框架的原始碼,我們可以找到對應Native異常和UI異常的錯誤處理,這就意味著我們可以通過修改原始碼來自定義異常的處理方式。不過這就意味著,一旦升級RN版本,我們就需要做出相應的修改,這會帶來維護成本。
也許這些對於你來說有些複雜,那也可以使用像bugly這樣的異常監控平臺,不僅可以隨時監控APP的崩潰、卡頓和錯誤等發生的情況,也可以清晰的知道使用者和手機的分佈情況。
我們知道JS可以通過應用內的JS引擎動態解釋執行。所以無論我們的原始碼做了多大的修改,只要無需構建,我們都可以通過熱更新動態的推送到使用者的手機上。這個過程大致如下:當用戶的APP啟動或喚醒的時候,檢查APP內的bundle和圖片資源是否是最新的,如果不是,則從熱更新伺服器載入最新的bundle和圖片。實際情況可能要稍微複雜一點。
在使用者的APP這邊,我們需要檢查bundle版本,進行下載、解壓、reload操作。如果是增量更新,還要進行bundle合併。對於熱更新伺服器,我們要針對Android和iOS提供不同的bundle版本,每次釋出或者更新時都需要打包釋出對應的bundle版本;如果是增量提供bundle patch版本,還要對bundle進行拆分。哈,所以的這些都做出來,工作量可能甚至要超過APP開發本身的工作量了 :( 所以有沒有現成的方案呢?
顯然是有的,我們目前使用的是微軟提供CodePush熱更新服務,只要簡單註冊配置,然後在APP端引入CodePush的客戶端外掛,就可以完成剛剛提到的那麼多工作。他還提供了版本的出錯回退機制。不過訪問速度是它的缺點,如果每次更新需要幾十M甚至上百M,那就要斟酌一下了,目前來看,我們使用起來感覺還是很爽的。
最後,也可能是最重要的一塊內容是效能優化。所為天下武功,唯快不破,如何讓我們的應用快起來時優化的關鍵。下面我們從加速速度、滾動速度和響應速度三個方面來提供一些優化建議。
加速速度帶來的是給使用者的既視體驗,如果避免白屏,把資料和頁面第一時間呈現給用使用者是關鍵。首先我們考慮的是從快取中載入往次訪問資料,然後非同步載入去載入最新的資料。如果是對實時性要求並不是很高的資料,我們可以使用Redux中統一管理的資料,之前我們提到過,這一部分資料我們也做了持久化快取。
大列表在應用中必會出現的部分,而列表本身操作又特別複雜和頻繁,這就導致列表內的元件會重複渲染,這會帶來極大的效能消耗,通過使用shouldComponentUpdate我們可以判斷元件是否需要渲染,從而阻止不必要的渲染——這很簡單,也非常有效。除此之外,ListView列表元件本身也提供了一些配置來提高渲染效率,比如首屏載入的數量、可視部分的數量。如果大家剛剛開始使用RN,恭喜你,你可以使用FlatList元件,列表的操作變得簡單,效能也非常出色。
最後,我們看如何提升響應速度。在Navigator頁面切換後,如果需要通過網路載入資料,很容易造成轉場動畫的卡頓,這是因為業務邏輯和UI渲染邏輯出現了交錯。RN提供了InteractionManager讓我們去處理這樣的情況,我們只要把業務邏輯放到runAfterInteractions方法的回撥中去執行就可以確保轉場動畫的完整展示。對於按鈕點選或其他的一些位操作可能也會出現類似情況,處理的方式也是大同小異,requestAnimationFrame的回撥使得互動和業務邏輯能夠錯開。實際上,上面提到的卡頓丟幀始作俑者都是JS單執行緒,如果我們使用setNativeProps就可以跳出這個模型,那是那句話,除非你知道自己在做什麼,否則不要這麼做。
當然還會有很多其他的優化建議,我沒有辦法完整列舉下來。實際上,如果大家按照上面給出的建議去做了優化,APP的體驗應該已經很不錯了。但凡事無絕對,實在快不起來的時候,我們別忘了把Loading效果用起來 :) 這會讓使用者願意多等一會。
作者:大執午
連結:https://www.jianshu.com/p/72e9225de21c
來源:簡書
簡書著作權歸作者所有,任何形式的轉載都請聯絡作者獲得授權並註明出處。