React Native技術在小米有品App中的應用

語言: CN / TW / HK

筆者khzliu同學,關注於大前端、跨端技術

小米有品簡介

小米有品是小米旗下的一個開放的生活購物平臺,小米有品App從最初的米家App內一個商城標籤頁經過兩年的發展成為一個獨立的商城應用。作為一個電商系統應用,小米有品App支撐了會場導購、營銷活動、內容直播、商城交易和售後物流等業務的前端頁面呈現與互動。因為電商的業務場景特點就是更新迭代速度快、動態化要求高、前端UI樣式和互動邏輯經常變動,所以小米有品App在技術選型上要滿足這些基本需求,同時小米有品又是一個精品電商平臺,極致的效能和流暢的互動體驗也是小米有品App追求的目標。

圖片

React Native

React Native是Facebook在2015年3月釋出的跨平臺移動應用開發框架,此框架採用MIT開源協議,支援使用JavaScript語言和React前端框架來構建效能媲美原生的移動端應用,並支援跨iOS,Android,Web三端平臺,採用React Naitve官方的一句話就是“Learn once, write anywhere.”。React Native另外一個特點就是社群活躍,且有大量的第三方庫可供參考和使用,可大大提高研發效率。

為了滿足有品業務快速發展需要,小米有品App在最初技術選型時期經過大量調研,選擇了具有跨端能力、支援熱更新且效能媲美原生應用的React Native框架來支援前端業務開發。在小米有品App內部針對不同的業務場景也同時保留適合某種特殊場景的其他技術方案,比如部分涉及到外部團隊開發的活動類頁面則支援使用Hybrid H5方式進行展示,掃一掃、登入、賬號中心等功能類且不經常變動的頁面則採用Native進行開發。

圖片

支撐的業務

在小米有品App中大概70%以上的業務是使用React Native技術進行開發支援的,這些業務當中不僅包括核心的商城交易類頁面,還包括內容直播類、分類搜尋商品聚合類、秒殺等活動類以及訊息服務類業務頁面,未來React Native頁面佔比會提高到90%以上。

圖片

商城交易類頁面是整個商城的核心,比如產品站、購物車、結算、支付和訂單、物流和售後等,這類頁面整體結構和互動並不複雜,但是要求頁面穩定性特別高,所以在異常處理和邏輯邊界以及日誌上報上需要特別注意;內容直播類頁面主要負責有品使用者社群運營,注重長列表的應用與優化,圖、文、視三方面的顯示和直播內特效動畫互動等,比如社群主頁,種草文章以及有品直播這類頁面;活動類頁面特點就是頁面內樓層UI樣式種類多、迭代速度快,需要在React Native的基礎上增加Low-Code/No-Code技術來支援不斷更新的樓層樣式,另外還需要支援樓層間多級聯動,App內這類頁面包括S級別秒殺活動、拼團活動等;還有部分訊息服務類頁面,比如訊息、通知和客服,這類則著重網路連通,訊息實時和應用許可權處理等等。

應用的技術

小米有品App從最初使用React Native到現在,大前端團隊也積累了許多使用經驗,比如如何能夠實時的且在使用者無感知的情況下進行業務更新和過載、如何管理和使用資原始檔、如何擴充套件符合有品業務本身的元件庫等。同時也面臨不少挑戰,比如怎麼使得React Native的頁面效能發揮到極致、如何能在React Native的基礎上讓研發更高效、如何適應動態擴增的業務場景,如何解決工程臃腫,促使測試釋出標準化、流程化以及從開發到測試再到載入之後業務的隔離問題。

圖片

更新與過載

早期的小米有品App體量和業務量較小,所有的業務採用的是單個React Native包進行包的構建和更新。一個完整的版本包內既有可執行的JS程式碼,還有一些常用的資源類檔案。最初有品的版本包的大小隻有800K左右,但隨著業務的增加版本包的大小也逐漸增加到3M,在當時網路處於3G到4G轉換的時期,這個體量要做到及時更新就需要使用某種技術方案來優化更新速度。

針對早期的單包構建和載入方案,使用Diff-Match-Patch則是最直接有效的優化包更新速度的方式,服務端拿最新的版本包和上一個版本包做程式碼Diff並生成一份兒Patch包,這個包Patch包的體量相較於整包的大小是非常小的,往往僅有幾K或者幾十K的大小,客戶端則通過拿到本地包的e-tag去訪問更新介面,來獲取適用本地版本的Patch包,最後客戶端拿到Patch包結合本地快取的版本包進行Patch操作就可以達到快速更新的要求。按照當時使用經驗服務端只需要保留最後五個版本的Patch包就可以覆蓋90%以上的更新使用者。

圖片

解決更新慢的問題後另一個可以提升頁面載入速度的就是在客戶端做版本包快取,Diff-Match-Patch優化僅僅解決了版本包下載鏈路的問題,還有一些初裝App或者許久未開啟App的場景,這就需要在客戶端每次發版時內建一個當前發版時間的最新版本包,並在App啟動時優先載入本地快取的包或者內建包,這樣可以保證使用者可以提早看到具體的業務頁面,然後客戶端在後臺同步進行最新版本包的檢測和下載,待使用者切換至無React Native頁面時重新載入最新的版本包,達到無感知的版本更新與過載,當用戶再次開啟React Native頁面時看到的就是最新版本的頁面。

圖片

後期隨著業務的發展,單包的體積愈發不受控制,僅某單一業務的原始碼和資源都可達到1~2M,在App內可能有十幾個業務,這種情況就需要考慮進行包拆分來解決,有品通過調研並設計適用於自身業務場景的拆包方案來適應有品業務的擴張,後面會通過其他文章介紹方案細節。

資源處理

React Native構建輸出產物中,我們用到的不僅僅是編譯後的可執行JS Bundle檔案,還有一些資原始檔供業務載入使用,比如圖片(PNG、JPEG、SVG)、JSON檔案、表格、PDF等等。如下圖:

圖片

想要正確載入到這些資原始檔,基本方式就是獲取這些資源在本地的儲存路徑,然後就可以正確載入到這些資原始檔。React Native的打包工具Metro為我們提供了一套解決require方式引入的本地資原始檔資源載入方案,對應Android系統,其基本原理就是Metro打包時會把require中的檔案按照檔案路徑去掉'/'符號並替換為'_',將路徑前追加scale對應的dpi drawable路徑輸出到構建產物,並把該資原始檔生成一個資源模組打到包內,比如構建完成的一個資源模組如下:

圖片

這個資原始檔在檔案系統內的儲存路徑就是drawable-mdpi/testapp_youpin.png,原生端載入該模組時通過SourceCode模組持有的scriptURL獲取當前包的絕對路徑然後根據模組的引數資訊抹去'assets',轉換時也同時轉換為實際的檔名格式並追加到scriptURL後面,最後得到檔案在檔案系統內的真實路徑完成載入。

拋去React Native原生支援的這種資源載入模式我們還可以自定義圖片載入,大概思路就是在構建完成Bundle檔案後把需要圖片資料夾拷貝到和Bundle同級目錄內然後壓縮下發給客戶端,客戶端再提供一個原生模組來獲取當前資源包在檔案系統內的絕對路徑,最後JS呼叫原生橋接的獲取Bundle儲存路徑的方法去獲取到這個絕對路徑並拼接資原始檔的相對路徑完成圖片資源的載入。示例程式碼如下:

圖片

WebView

有品的業務場景中也會使用到WebView來載入一些三方的H5頁面,這類頁面載入到小米有品App內並依賴有品的登入態以及涉及到呼叫有品App內的提供的API進行H5與原生的通訊。React Native社群中中提供的react-native-webview庫可以支援並提供WebView容器進行H5頁面的載入,此外,在我們使用該庫的時候通常需要解決Cookie同步和Native通訊問題。

Cookie同步

在Android系統中載入H5頁面時需要使用CookieSyncManager可以把當前域名的Cookie資訊在載入WebView之前同步到WebView的JSContext中。

圖片

在iOS系統中,由於UIWebView支援自動同步Cookie,所以使用UIViewView元件則不需要處理Cookie問題,但迫於記憶體和效能方面的考量,建議統一使用效能更高記憶體佔用更小的WKWebView來載入H5。Cookie同步時,在WKWebView載入URL之前先判斷域名下的Cookie是否已經同步,如果沒有同步則通過在WKWebView內部注入JS指令碼,該指令碼待整個doucument載入完成後通過document.cookie = xxx的方式把Cookie注入到WKWebView的JSContext中。

圖片

WebView通訊

WebView中的H5與React Native程式碼進行通訊的處理方式大致和原生端WebView與原生通訊處理方式類似,有兩種方式來支援H5呼叫React Native端提供的方法,第一種是通過WebView控制元件提供的onShouldStartLoadWithRequest回撥方式進行URL攔截,雙方約定一個URL格式用來進行通訊,React Native端攔截到符合格式的請求連結,然後阻斷請求並獲取連結當中的方法名稱和方法引數進行方法呼叫。這種通訊方式能夠有效的實現H5呼叫React Native或者Native提供的函式,但是由於URL的連結長度限制,不能很好的處理二進位制檔案,特別是一些大檔案資料的傳遞操作;另一種方式是向WebView的JS上下文內直接注入關聯物件,讓H5內的JS可以直接呼叫注入物件的方法達到方法呼叫和回撥處理。Native或者React Native呼叫H5方法則相對容易,可以獲取WebView物件的當前引用呼叫它的postMessage方法,H5端監聽訊息佇列來進行通訊。

圖片

Web適配

React Native目前官方僅支援iOS和Android兩端,想要在不改動React Native原始碼的情況下讓同一套業務程式碼能夠在瀏覽器上執行對於研發同學來說是非常重要的,支援輸出Web端可以減少大量工作量,真正實現一份程式碼三端複用。React Native在iOS和Android端使用原生元件進行渲染,在瀏覽器當中只需要把渲染部分使用瀏覽器支援的DOM進行渲染就可以實現React Native在Web上的轉化,目前比較成熟的方案就是React Native官方推薦的社群開源庫react-native-web來解決,使用方法簡單方便。

傳送門:  https://necolas.github.io/react-native-web/

動態化

React Native為我們提供了動態更新的支援,可以在不更新App客戶端的情況下發布升級新版業務功能,但有時我們並不滿足於當下,我們想要更快的更新速度,甚至不用業務包發版的情況下達到業務的更新。快速更新在某些場景下是很有必要的,特別是微小的UI改動,不斷新增的UI樓層樣式以及複用性較高的元件,所以要做到Low-Code/No-Code,追求動態化的極致。小米有品內部使用的Teris就是用於實現理想的動態化方案,他根據現有業務需求,抽象基礎元件並定義一套標準的資料規範,通過解析該資料規範建立節點並渲染頁面。

圖片

原生元件&UI庫

React Native的一個特別重要的優勢就是它的社群活躍,全世界的開發者都在幫你實現某些通用的元件庫,可以為我們節約不少研發成本,所以站在巨人的肩膀上才能看的更遠。常用的三方元件庫有提供網路狀態監測的@react-native-community/netinfo、支援相機能力的@react-native-community/cameraroll、進度條元件@react-native-community/progress-view、橫向滑動元件@react-native-community/slider、支援地理位置資訊的@react-native-community/geolocation等等。

對於不滿足我們需求的元件,我們還可以自定義去實現,小米有品大前端團隊也根據自身需求的特殊性實現了內部使用的通用類UI元件和業務類元件,比如商品卡片、地圖、嵌入式檢視、直播元件等等。

圖片

通用跨端元件庫Duplo

效能

高效能是小米有品App選擇React Native技術方案的重要因素之一,藉助於原生實現節點渲染使得頁面的流暢性完全可以和原生頁面相媲美。如果你是一位客戶端開發者,你在實現一個頁面的時候肯定會考慮這個頁面平均幀率,頁面的記憶體佔用,頁面中動畫如何實現才能減少CPU的計算來減少電量的消耗。而當前端開發者使用JS去開發React Native頁面時往往不知道或者很少去關注這些原生端實現介面開發時關心的優化點,結果就是頁面出現卡頓、記憶體爆炸、動畫不流暢。使用React Native開發時重點關注長列表的效能優化、記憶體優化以及動畫這三點基本上可以解決90%的效能問題。

長列表

長列表在前端頁面開發中應用的最多,在瀏覽器開發中大部分使用塊級元素進行平鋪就可以實現長列表展示,但在React Native中這種簡單的流式平鋪往往會造成頁面的卡頓。React Native為我們提供ScrollView、VirtualizedList、FlatList、SectionList四種官方元件來實現長列表的瀑布流,其中FlatList和SectionList都是基於VirtualizedList的上層封裝,VirtualizedList相較於ScrollView提供了元素複用機制,可以有效的減少記憶體佔用並提高頁面滾動流暢度,同時也提供部分配置項來優化列表渲染,比如以下配置項:

  • initialNumToRender 可以指定開始渲染元素的數量,提高首屏渲染速度,使用該配置時儘量配置元素數量剛好填滿一屏,因為這部分元素在使用者滑動時不會被解除安裝;

  • keyExtractor此函式用於為給定的元素生成一個不重複的 Key。Key 的作用是使 React 能夠區分同類元素的不同個體,以便在重新整理時能夠確定其變化的位置,減少重新渲染的開銷。

  • getItemLayout是一個可選的優化,用於避免動態測量內容尺寸的開銷,不過前提是你可以提前知道內容的高度。

  • removeClippedSubviews 一個將“剪裁子檢視”(clipped subviews)(指的是那些在父檢視之外的檢視)從檢視層級中刪除的本地優化,為的是減輕渲染系統的工作負擔。但是這些被剪裁掉的子檢視依然保留在記憶體中,所以它們所佔的儲存空間沒有被釋放,內部狀態也都保留了下來。這可能會極大的改善長列表的滑動效能

  • windowSize 設定可視區外最大能被渲染的元素的數量,以可視區的長度為單位。比如說,如果列表佔滿了整個螢幕,而 windowSize 屬性被設定為 21 的話,那渲染的長度為包括當前可見螢幕區域在內,往上 10 個螢幕的長度和往下 10 個螢幕的長度。將 windowSize 設定為一個較小值,能有減小記憶體消耗並提高效能,但是當你快速滾動列表時,遇到尚未渲染的內容的機率會增大,而這些尚未渲染的內容會暫時性地被空白區塊所替代。

  • maxToRenderPerBatch 每批增量渲染可渲染的最大數量。能立即渲染出的元素數量越多,填充速率就越快,但是響應性可能會有一些損失,因為每個被渲染的元素都可能參與或干擾對按鈕點選事件或其他事件的響應。

  • updateCellsBatchingPeriod 具有較低渲染優先順序的元素(比如那些離螢幕相當遠的元素)的渲染批次之間的時間間隔。與 maxToRenderPerBatch 具有相同的目的,都是為了在渲染速率和響應性之間獲得一個平衡。

記憶體

記憶體佔用一個也是React Native開發中影響效能的重要因素,開發者必須時時刻刻考慮到如何去降低記憶體的佔用,雖說目前手機記憶體大部分已經達到6G,甚至12G,但是過高的內建佔用容易使處於後臺的App被系統程序殺死。

圖片

常見的記憶體優化也比較多,比如減小Bundle包的大小。Bundle包被載入到記憶體當中是常駐於記憶體的,可以通過提取公共元件減少重複程式碼,從而縮小Bundle包體積。執行時JS程式碼中高記憶體佔用的變數要及時釋放來減少記憶體消耗,另外也可以使用RAM格式的Bundle延緩載入未使用的模組,或者對Bundle進行拆包處理實現按需載入並釋放不使用的業務程式碼從而減少記憶體佔用;上面講到的長列表優化當中使用可複用的瀑布流元件也可以減少當前記憶體佔用;另一個記憶體佔用的大戶就是圖片,一個頁面當中使用過多的圖片會造成記憶體的暴增,特別是顯示區域較小的圖層載入高解析度圖片,固定的圖片顯示區域使用相同大小的圖片可以大大減小記憶體的佔用。雖然圖片的大小不影響最終繪製的圖形大小,但高質量圖片需要被讀取並快取到記憶體當中而導致記憶體佔用上升,另外也可以使用支援記憶體快取管理的圖片元件,比如FastImage, 也可以用SDWebImage進行自定義封裝等等。圖片色彩飽和度在一定程度上也會增加記憶體的佔用,也可以通過調整圖片色彩飽和度來降低記憶體佔用;其他造成記憶體佔用過高的因素還有元件的View層級太深,雖說React Native在實際渲染時已經自動優化掉無用的View層級,但是我們需要在開發時就關注並減少View層級數,對於層級較深的檢視層可以考慮繪製成圖片來減少記憶體的佔用。

動畫

我們知道,根據React Native的實現原理,想要改變一個UI的樣式是需要經過一次JS和Native通訊來實現,這很大的限制了動畫的效能。React Native中為我們提供了Animated和LayoutAnimation用於動畫的實現,動畫的效能上我們可以通過啟用useNativeDriver來使用原生動畫驅動,這樣可以跳過每幀繪製時的JS執行緒和UI執行緒通訊達到效能的提升;另一種是使用setNativeProps來直接操作原生DOM,減少大量setState造成的JS執行緒內的Diff計算;React Native也為我們提供了InteractionManager模組,可以將一些耗時任務安排到動畫完成之後再執行,增加動畫的流暢度;最後我們還可以藉助一些第三方庫來實現動畫,比如BindingX,有興趣的可以嘗試一下。

重繪

過渡的重繪也會帶來效能的損耗,所有的JS計算目前都在同一個執行緒中進行,執行緒阻塞就會影響互動的流暢度,我們可通過shouldComponentUpdate來減少不必要的重繪,可以藉助PureComponent元件來替我們完成這部分的優化;避免在設定props屬性時直接使用內聯箭頭函式也能減少元件的重繪,內聯箭頭函式在每次繪製時都會生成一個新的物件,相當於對props設定了一個新的值導致重繪;在React Native中也提供了setNativeProps方法來直接操作DOM,所以小的區域性變化可以藉助setNativeProps來提升效能;最後一些圖層的圓角、圖層的陰影和透明度雖然不會引起重繪,但會造成GPU的過渡繪製而影響幀率。

拆包

一個前端專案主要流程就是開發、打包、部署,其中打包過程中一個優化點就是將大包拆分成小包,優化後能夠有效的降低記憶體的佔用,並且減少載入的時間以及減少網路流量的消耗。早期React Native在有品是採用單Bridge單Bundle的模式進行使用的,如果使用React Native來支援導購、會場、搜尋、眾籌等這些單業務單Bundle的業務場景時就需要針對每個業務進行單獨打包,受限於React Native基礎JS程式碼的量以及移動端裝置的效能,特別是記憶體方面。商城類業務需要業務間頁面來回跳轉,頁面堆疊很可能包含多個業務頁面,而且Bundle載入初始化時間長影響頁面開啟速度,所以目前不足以支援單Bundle單Bridge的模式,需要使用多業務單Bridge多Bundle的模式,對業務包進行包拆分,提取公共基礎模組實現記憶體共享。

圖片

有品結合自身業務特點設計並開發了支援RN拆包的系統平臺,該平臺共有四大模組:拆包平臺管理前端負責應用管理、業務管理、包管理等前端操作;包管理系統負責提供管理平臺基礎服務、包的分發、對接打包服務、對接許可權系統和流程管理系統等;打包平臺提供Bundle的打包能力,對接雲端儲存和Sentry平臺等;客戶端SDK則提供容器展示、包更新和載入策略、日誌上報等功能。

結束語

本篇文章簡要的介紹了React Native技術在小米有有品App中的應用,並未對應用的技術細節做深入講解,加上React Native技術點又非常多,所以後續會更新一系列與React Native技術相關的文章來為大家做深入介紹。本篇先作為React Native技術專題的開篇,讓大家對React Native技術有個初步瞭解,如果大家對React Native技術有興趣,可以關注我們的後續更新。

招聘

小米有品是小米旗下的一個精品電商平臺,有品大前端團隊也在專注打造一個專業的電商前端團隊,歡迎關注前端機會的同學聯絡:[email protected]