一、誕生背景
1.無線開發的痛點
React Native最近兩三年之內整個框架在業界應該說是非常熱門,很多團隊、大公司都在做RN的一些研究開發工作。先一起回想下在React Native框架出現之前,互聯網APP開發是一種什么樣的模式。最初,大多數同學應該是用原生開發Android或者iOS,再加上HTML5內嵌的方式,即Web APP。之后又衍生出了Hybrid APP,基于PhoneGap/Cordova框架實現了WebView的能力強化。不知道大家在做這種開發的時候,有沒有遇到過一些瓶頸或者一些痛點,反正我們的團隊是遇到了很多。這里總結一下之前傳統的方式有哪些問題。
第一,效率低下。因為無論是Android還是iOS,使用傳統的原生開發都有一定的開發門檻。而且代碼上不能復用,這意味著任何一個業務要在Android和iOS各做一次開發,測試和業務開發工作都不能復用。
第二,性能比較差。用傳統的H5開發方式,受限于 WebView 容器的一些瓶頸,導致無論在頁面加載還是用戶體驗上,相比原生應用有比較大的差距。
第三,靈活性不夠。因為傳統原生開發意味著任何改動都需要發版,在Android上因為像國內應用商店非常多,而且涉及到各種不同的渠道包,所以發版成本很大;在iOS則受限于蘋果的審核機制。對我們來講,任何這種線上問題處理起來都非常痛苦。
最后,接入困難。因為Android和iOS平臺有差異,所以任何一種垂直業務接入APP的成本非常高,很多業務代碼和業務流程并不能復用,造成業務團隊的開發、接入成本非常高。
2.React Native登場
說了這么多痛點,我們也在反思到底需要一種什么樣的框架來解決這些問題。非常幸運,我們在2015年的時候注意到Facebook發布了非常具有顛覆性的RN框架。簡單來說,這是一種跨平臺的移動應用開發框架。在當時它非常有顛覆性,因為它最大的特點就是完全用JavaScript進行應用的開發,但是最終會渲染成原生的組件。對開發者來說,這意味著你擁有了Web開發的效率,同時兼顧了原生的性能。這對我們當時業務的吸引力非常大,這個框架一經推出,國內外很多公司都在用,像Facebook自己也在用。在國內,手機百度、手機QQ、京東APP也很早就進行了開發。RN對我們團隊來講都有哪些優點,或者說為什么要用它,這里大概總結了以下四個原因。
第一,學習成本低。因為它的開發基于JavaScript,JS語言本身在開發者當中有非常良好的群眾基礎,任何一個有經驗的前端團隊可以快速地上手RN開發。
第二,多端代碼復用。因為所有的業務都用JavaScript開發完之后只有一份代碼,然后通過編譯打包機制直接部署到不同的平臺,如Android、iOS甚至Windows平臺。
第三,接近原生的性能。開發者使用JavaScript進行RN框架開發,開發完之后在再通過中間的虛擬DOM。這個實際上是它核心所在,傳統的H5的應用是跑在Web View的容器當中,容器中需要維護一個真實的DOM,而真實的DOM上每一次操作都會有回流(reflow)和重繪(repaint),效率并不高。Facebook最有顛覆性的一點就是提出了一個虛擬DOM的概念,把整個DOM放在內存當中,然后通過高效的diff算法來計算比較哪些UI組件需要更新,最終只對這些需要更新的組件進行真實操作。經過測試,采用RN框架,無論是加載性能還是頁面滑動性的用戶體驗上,都比原來H5的方式要好很多。
最后,社區活躍。除了Facebook之外,GitHub上有很多第三方的團隊、個人、公司開發貢獻了很多非常優秀的第三方組件,它的社區是非常健康、非?;钴S的。
3.React Native的局限
不過現實是殘酷的,即便確定了用RN框架做業務開發,在實際的開發當中也發現了RN的一些不足。對我們的業務來講,最不能接受的主要是以下四個方面。
第一點,RN框架原生并不支持Web端。這意味著如果一個業務需要同時上Android、iOS和H5頁面的話,那除了用RN之外,還需要用傳統的H5或用ReactJS框架再做一次開發,這樣效率是非常低的。
第二點,RN框架官方并不支持熱更新。雖然現在有很多第三方方案,比如微軟的CodePush,但是官方并不原生支持熱更新,而熱更新對我們的業務來說也是非常重要。
第三點,Facebook給出的官方RN API不能完全滿足業務快速的發展。它只給了一些很基礎的API,但業務中經常會用到的一些多媒體,比如錄音、錄像、視頻播放文件以及文件上傳、壓縮、加密等等,這些都沒有提供。
最后,前面提到RN框架性能非常不錯,比H5好很多。實際上經過真正的業務開發后,發現90%的場景下RN的性能非常棒,可以滿足我們的業務需求;但是在另外的10%的場景下,特別是一些交互非常復雜、頁面非常復雜、需要頻繁的更新、需要一些手勢交互的場景,RN仍有些內存跟性能的瓶頸。
4.解決方案:JDReact三端融合平臺
既然RN有優點也有缺點,那怎么辦?
我們的解決方案是基于RN框架進行了深度定制和二次開發,逐步打造了符合京東業務的JDReact三端融合平臺,主要的工作是以下四大方面:
第一,把RN的核心Base庫拿來做裁剪和二次開發,把不需要的功能刪減掉,把性能、兼容性、穩定性的問題修復,包括也支持了拆分打包。
第二,在后端搭建了一個功能支撐平臺,幫RN框架增加了灰度更新升級、數據監控以及降級容災功能,這些對業務來說是非常重要的。
第三,基于整個RN框架,結合京東的一些業務特點,封裝了一套自己的業務組件,包括UI公共組件庫。目的是為了讓垂直業務開發者可以很快地使用框架進行業務開發,完全不用關心設計的樣式跟交互,可以快速接入業務。
第四,打通Web端,實現了一套RN框架向ReactJS轉換的工具。可以做到一次代碼編寫,直接部署到Android、iOS跟Web三端。
二、JDReact三端融合平臺全解析
1.整體架構
下圖所示就是整個JDReact三端融合平臺的架構圖。
最下面是一個后端接入平臺,包含剛剛提到的灰度更新、降級容災、數據采集和持續集成,這些是由服務端提供的一套服務。中間這一層是提供給內部開發者的一套完整的SDK開發工具,里面除了一些API之外,也封裝了大量的京東定制功能組件,包括UI公共組件。其中還有一塊是Web轉換工具,提供了一套RN轉換的腳本。業務開發者完全不需要關注這些細節,只要關心他自己的業務邏輯,就可以直接開發出覆蓋三端的應用。最上面的業務層就是京東APP所有使用三端融合平臺開發的業務,這些都可以直接部署到Android、iOS和Web。
2.改進和優化實踐
前面主要介紹了整體的平臺架構,現在開始來分享一些干貨,就是我們在開發過程當中團隊遇到的RN的一些問題,包括如何改進跟優化的一些實踐。我列了一些功能點跟大家一起分享。
功能裁剪
有同學抱怨過RN庫太大了,所以拿到RN的第一件事就是裁剪。對Android平臺來講,除了把RN的基礎庫裁剪以外,很重要一點就是要把方法數減少。因為Android平臺dex有方法數限制,一旦超過65K就需要拆分成多個dex,整個應用的安裝跟加載都會有性能問題。所以,要對Android方法數進行嚴格控制,我們的做法就是根據業務情況,把一些用不到的組件方案中的功能組件刪除。其中最重要改動就是把Android中support-v7和stetho庫依賴給去掉,去掉之后不僅大小減小了很多,而且方法數減少了將近7000。除了移除這個功能庫,很重要一點,因為不是一個全新的RN應用,需要跟現有的體量很大的APP做集成整合,所以盡量讓一些依賴庫復用主站中依賴庫,比如fresco、okhttp等。一來縮減包的大小,二來避免包的沖突。但是主站中的版本很可能跟RN中引用的版本有差異,需要中間做一層適配層,把這些差異盡量抹平,保證這些功能和方法都能工作。
加載性能優化
雖然說RN框架號稱比H5的加載性能快很多,但實際開發中發現在Android的一些低端機型上,加載速度還是達不到原生體驗,極端情況下甚至會出現白屏。主要原因是業務jsbundle比較大,RN框架在加載jsbundle和通過JSCore解析jsbundle時耗時太長。當用戶看到真正業務頁面之前會出現長時間的空白頁面。
當時提出了兩個解決方案,第一種方式是實現一套預加載機制。預加載機制就是在用戶真正進入業務之前,把jsbundle提前加載解析,提前把RootView生成。簡單來說就是用空間換時間。但這樣做并不是所有的業務都適合,因為會帶來一些內存增長,所以一般在很核心很重要的業務采取預加載機制。第二種方式是修改了RN框架底層庫,在RN框架開始加載jsbundle文件時,顯示一個loading的進度提示用戶正在做加載的動作。當JS文件加載并且解析渲染完成之后,把進度條去掉,最終被頁面展現給用戶。這樣雖然等待時間并沒有減少,但是用戶體驗會好很多,整體的時間從收到的反饋來看還是比H5要好很多,這是我們做的一個優化點。
內存優化
我們還做了一件很重要的事情,就是內存優化。在RN框架開發中碰到的最大的坑就是內存這塊,因為業務中會經常碰到ListView的使用,根據這些業務的需要,可能要加載很多頁,兩頁、三頁、甚至可能會無限加載。這種方式在早期的RN版本當中肯定會引起OOM(OutOfMemory)崩潰,原因是在RN的早期版本當中并沒有對ListView做內存復用。這意味著ListView滾多少,圖片都會在內存中,當頁面加載地越多,出現OOM崩潰的幾率也越大,這是一個非常不能接受的問題。
在RN的早期版本,我們團隊在JS層實現了一套內存回收。它的原理跟原生當中的原理也差不多,就是當頁面劃出兩個屏幕之后,會強制把圖片和內容進行回收,用一個空白的View替換。當內容劃到用戶可見的屏幕范圍之后,再把圖片給加載出來,這也是原生常用的一種內存回收的方式。修改后的效果很好,無論頁面加載再多,都不會出現卡頓和OOM崩潰。在RN的新版本(0.43之后),引入了一個新的FlatList組件。這個組件完全解決了ListView的內存回收問題。它的實現機制和我們的方案類似也是在JS層中做內存回收的動作。所以給大家建議,如果開發中碰到類似的問題,完全可以升級到最新的RN Base 0.43以上使用FlatList組件。如果版本比較低的話,那就需要自己實現這套機制。
第二個比較大的內存問題就是圖片,iOS平臺可能相對好一些,在Android問題會相對多一些。RN的底層圖片框架庫用的是Fresco,而我們主App中用的也是Fresco底層庫,這里就會有些問題。第一個就是重復初始化,這也是當時業務開發當中碰到的問題。當主App中的Fresco進行初始化之后,如果RN中也進行一次初始化,實際上之前那部分內存并沒有被釋放,會出現內存泄漏。我們做了專門的檢測,避免RN重復初始化的問題。第二個也是跟RN框架里面的實現有關系,因為它采用的圖片編解碼用的是ARGB_8888,這種方式支持Alpha通道。但實際上大部分情況下可以采用RGB_565編碼,雖然丟失了Alpha通道,但是圖片在內存當中的大小可以減少50%。不過有些業務可能也真的需要一些透明的背景,需要Alpha通道,所以也提供了一些API來針對特殊圖片,讓它采用ARGB_8888進行編解碼。這樣既解決內存問題,也滿足了業務的需求。
最后一個經驗就是在所有的RN頁面退出之前,建議強制調用Fresco框架的clearMemoryCache方法,通知Fresco清除內存緩存。可以保證GC及時地把這些圖片內存給回收掉,避免整個APP的內存占用過高,經過實踐驗證這也很有效地解決了內存問題。
拆分打包
關于拆分包,因為目前我們采取的方式是每一個業務打成一個jsbundle文件,這意味著業務越多,jsbundle文件會越大。而這些jsbundle文件當中,業務的代碼其實占比很小。百分之七八十都是Facebook提供的一些公共組件庫。我們的做法是在編譯打包之前,把這些公共組件庫先抽取出來,放在一個common jsbundle里面,然后業務只保留業務相關的一些jsbundle文件。最終在真正的加載之前,做一個簡單的合并動作,這樣業務越多,這種優化的效果就越好,可以有效減緩jsbundle文件大小的增長速度。
性能優化
除了內存之外,最關心的就是性能。前面也提了RN的性能其實比H5要好很多,可以滿足我們90%的場景,但實際上還有10%的場景,RN做的并不是很好。主要也是因為整個RN的機制,它雖然是最終渲染成原生的組件,但是UI的控制還是在JS中做的。受限于JS單線程一些限制,當有一些很復雜的交互、很復雜的手勢或者快速的滑動,很有可能引起JS中的阻塞,造成動畫的一些渲染的數據不能及時同步到原生當中,造成了整個頁面的卡頓。
建議的方案有三種,第一個做RN的Base升級,把RN升級到最新的0.45,它會采用了一個新的叫Yoga的引擎。這種引擎是完全用native實現的,可以把大部分的動畫渲染和交互放在原生的線程中做。經過測試,采用了Yoga引擎,整體的渲染性能可以提升30%以上。
第二種方式,有一些非常復雜的一些交互,比如左右滑動結合上下滑動一些手勢,如果用單純用RN做,很容易碰到一些手勢沖突的問題。所以把這種組件原生化,完全用原生實現,所有的交互跟手勢控制全在原生做。這樣做就可以達到非常完美的性能,但同時也需要原生開發團隊介入。
最后一個經驗就是盡量使用Animated這種動畫類,減少JS控制的UI數據同步,避免JS線程阻塞。
版本檢測
另外在jsbundle文件當中增加了一個version文件,解決版本沖突檢測。因為要支持線上更新,就意味著需要把每一個業務jsbundle文件做一套完善的版本控制。需要知道當前這個jsbundle文件的版本號是多少,可以跑在哪個客戶端的版本當中,可以支持的這它的RN底層庫是多少。這些信息都會記錄下來,然后在每一次的升級之前做版本檢測。這可以有效地避免線上不同客戶端和不同RN版本之間的版本沖突問題,可以支持線上的灰度升級。
兼容檢測
RN其實有最低版本支持,像它的早期版本在Android是支持API 16以上,iOS是iOS7以上。其實我們的主APP要支持的版本會比他更低一些,所以需要在主APP中做一些保護和判斷,一旦檢測到用戶的版本不支持RN,就需要做一些降級處理,比如說把入口關閉或者跳轉M頁。這樣最大的程度避免不支持RN版本的用戶出現崩潰的情況。RN其實可以支持x86芯片,但是考慮到如果要支持x86的話,需要增加一套基于x86的so文件,會對包大小有影響,所以對所有的x86做了降級。
原生能力擴展
前面剛才也提到了我們的業務非常多樣,很多的能力RN并不支持。所以基于RN框架我們擴展很多業務上用到的原生組件,比如做了整個多媒體的視頻播放、視頻錄制、音頻播放、音頻錄制等組件,還有一些文件上傳、語音識別等。在RN提供了這套JS的接口,給垂直業務團隊快速開發和使用。
3.通用組件庫封裝
我們也結合自己的業務做了一套通用組件庫的封裝,例如京東當中的用戶登錄、購物車、收銀臺等等業務,在RN中做了一套組件的封裝。把所有的接口都提供了JS的API,樣式和交互像常用的下拉刷新、對話框、按鈕等等,也提供了一套通用的樣式組件給開發者。在做業務開發的時候,完全不需要關心這些樣式怎么畫、顏色怎么搭配,只需要關注業務邏輯。剩下的事情由框架做,這可以提升整個業務開發的效率。
4.三端融合
剛才前面提到了很重要的一項工作就是克服了RN不支持Web端的問題。我們做了一套Web轉換的工具,打通了三端。其實在業內三端融合也有廣泛的研究,方案主要有三種。
第一種方式,就是在RN跟ReactJS之上再封裝一套輕量的跨平臺的抽象層,像微軟發布的ReactXP就類似于這樣的架構。使用這種架構,意味著所有API、類、組件都不能用RN API,必須要用新的定義的接口,而且目前API支持也不是太多,還在完善中,所以沒有采用這種方式。
第二種就是ReactJS做開發,之后通過工具轉換成RN,這種方案適合于比較偏重H5業務的一些團隊,因為他優先需要上的是H5頁面,用戶體驗比較偏重H5。通過工具向RN轉換其實是個有損轉換,因為RN支持的樣式實際比CSS樣式少。從ReactJS向RN轉換的話,可能會丟掉一些屬性和布局。
第三種方案就是先用RN做開發,開發完之后再通過WebPack工具向ReactJS進行轉換。這種方式的好處是可以優先保證RN中的體驗,而且RN的樣式支持是CSS的一個子集,這意味著從RN向ReactJS轉換不會丟失功能和屬性,所以業內更多的方案也是采用這種方式。GitHub上有一些類似的開源框架。但它們支持的組件并不是太全,不能完全覆蓋我們的業務,所以我們自己實現了一套。包括之前說的所有的原生組件,它只有原生部分,我們也增加了JS部分的實現,使我們的框架可以完整、功能完全沒有丟失地轉化為Web頁面。
5.灰度更新
下面簡單介紹我們后端的接入平臺在服務端增加的灰度更新控制。發版之后,如果需要做一些RN組件更新,可以通過后臺的更新服務器做一次支持這種灰度的更新。它大概的流程就是首先由APP端發起更新的請求,發送到路由控制,路由控制負責控制用戶是不是在灰度比例范圍之內。如果符合灰度策略,把這個請求轉到服務端處理,服務端根據客戶端上報的jsbundle文件的版本跟服務端部署的版本做一次比較,看有沒有適合這個業務的新版本。如果有,把這個升級的版本號以及下發地址回傳給客戶端,客戶端會直接根據下發的下載地址從云存儲上下載升級包,完成整個升級過程。因為用戶量比較大,所以每一次更新一定要有一個灰度策略,根據灰度比例逐漸放到全網,這是非常重要的。
6.降級容災
我們把降級容災定義為兩種:一種叫被動降級,一種叫主動降級。所謂的被動降級是指客戶端確實不支持RN框架,每次加載RN框架都會出現問題,那必須要進行被動的降級,跳轉到對應的H5頁面,使得對業務的影響降到最低。這種降級邏輯是在客戶端當中做處理的,就是前面介紹的兼容性檢測。第二種是主動降級,很可能在業務開發的時候會發現一些上游的接口出現了問題,導致客戶端中的某項業務不能正確地運行,這個時候就需要由服務端控制對這項業務進行精準的降級。我們會支持多個維度靈活的配置,可以根據客戶端的版本號、客戶端的型號,配置灰度比例、白名單,精準地對某些用戶的某些業務或者某些地區的某些用戶進行降級,減少業務上的損失。在一些非常大的促銷的時候,像雙11、618這種峰值非常高的時候,可能會采用這種方式。
7.持續集成
這主要是我們內部的一個開發模型,把所有RN的基礎庫,包括自己提供的一些公共組件庫、公共UI組件庫都部署在內部的NPM Server上。每一個接入的業務開發者、每個業務都會有一個獨立的GIT。因為在我們內部,其實業務開發團隊可能會很多,我們的團隊是負責維護框架,而業務開發團隊各個部門各個地區都會有,他們會有申請自己獨立的GIT,然后從NPM Server上下載最新的SDK包進行業務開發。業務開發完成調試之后,會通過CI打包平臺發起打包命令,然后觸發Jenkins當中的job,從對應的業務的規則拉取代碼,進行編譯打包。編譯打包成功之后,把它部署到對應的Android或者iOS客戶端版本當中進行整個發布。這種方式對業務開發者來說,最大的好處就是打包編譯完全是腳本自動化,不需要獲取客戶端的源碼就可以做到這個業務的開發和上線。
8.數據監控
JDReact三端融合平臺我們也做了一套非常完善的數據監控中心。因為需要知道所有RN頁面啟動的時間、加載頁面的時間、服務端返回的響應時間、界面渲染的時間。需要把這些APM數據上報,通過上報的數據分析,不斷地優化性能。第二,也會把業務開發當中碰到的一些異常日志進行上報,可以幫助我們快速的定位問題,發現問題并部署相應的升級包。第三,因為灰度升級更新這個機制,需要有數據埋點來統計升級的成功率。有多少的用戶真的是發布升級之后可以成功升級到這個版本。最后,也會針對DAU、UV、PV等基礎數據做統計,這個主要也是幫業務方搜集一些運營數據,好做業務上的決策。
三、總結
這個框架推出有一年多的時間,到目前為止,京東APP當中已經有20多個業務正在使用這套框架,其中也有一些比較重要和常用的業務。我們整個平臺也經歷了去年的雙11、618,今年即將到來的618,我們也會做更多的后臺保障,在穩定性,包括降級上做一些處理,確保業務能夠正常地推進。未來也希望能夠不斷的完善這個平臺,不光是京東內部在用,一些很好的組件框架也可以開放出來,跟大家一起學習進步。
作者簡介
沈晨,京東商城專家架構師、JDReact三端融合平臺負責人。
給InfoQ中文站投稿或者參與內容翻譯工作,請郵件至editors@cn.infoq.com。也歡迎大家通過新浪微博(@InfoQ,@丁曉昀),微信(微信號:InfoQChina)關注我們。