前言
正文從這開始~
引言
React Native以其獨到的特性,吸引著互聯(lián)網(wǎng)公司紛紛為之投入或多或少的人力。在實際的開發(fā)過程中,開發(fā)者們也確實嘗到了甜頭,它的組件化思想、熱更新機制以及jsx和es6等的引入,都給開發(fā)者們帶來了很大的便利。也難怪在npm和github上,每天都會有很多react-native的新模塊出現(xiàn)。這也充分表明了各大公司對其的看好。
然而,從目前qq群、微信公眾號、社區(qū)、論壇等各大信息交流平臺中了解到,大家都是保持在研究和觀望狀態(tài),頂多把某個不重要的頁面交給React Native來練手。其中緣由紛繁復(fù)雜。
今天我們這里主要是探討——bundle文件太大。
現(xiàn)狀
React Native應(yīng)用的開發(fā)者們,在項目開發(fā)完后,都會遇到一個問題,生成的bundle文件太大。一個AwesomeProject項目,在沒有什么邏輯代碼的情況下,打完之后約530k。隨著業(yè)務(wù)的增多,業(yè)務(wù)復(fù)雜性的上升,文件的大小勢必會急劇增大。react-native打包成一個bundle的做 法,必定是要得到解決的。
分析
react-native默認(rèn)提供的打包方式有兩種:
離線打包
在線打包
http://localhost:8081/index.ios.bundle?platform=ios&dev=true
具體有哪些參數(shù)可以打開如下文件進(jìn)行查看:
官網(wǎng)中還給出了一些其它的使用方式,地址:
https://github.com/facebook/react-native/tree/master/packager
不過,不論哪種方式都是只有一個“entry-file”,然后根據(jù)“entry-file”去進(jìn)行依賴分析、文件壓縮等操作,最后輸出在 “bundle-output”中。然后通過NSBundle的URLForResource方法來指定加載打好的的bundle文件。如:
這樣的打包模式,對用戶體驗來說是非常不錯的。但是考慮到國內(nèi)的網(wǎng)絡(luò)狀況以及對App size的控制,打成一個Bundle的模式在國內(nèi)還是行不通的。
思考
在傳統(tǒng)的Hybrid開發(fā)中,要解決文件太大的問題,我們常常會想到如下幾個辦法:
進(jìn)行拆包
按需加載本地文件
按需加載線上文件
那么,能否把Hybrid開發(fā)中的經(jīng)驗應(yīng)用在React Native項目中呢。在React Native項目中,針對文件大的問題,我們做了如下嘗試:
多業(yè)務(wù)進(jìn)行拆包
借助gulp、grunt等工具,通過配置不同的任務(wù),在調(diào)用React Native提供的打包命令,可以將App打包成多個文件。
按需加載本地文件
在開發(fā)環(huán)境的情況下,ReactNative是支持加載本地文件的。這里想要做的是,在打包完的bundle中也可以加載本地文件,這就需要對require進(jìn)行擴張了。
按需加載線上文件
在開發(fā)Hybrid時,為了減少包體積。開發(fā)者們經(jīng)常會將一些不重要的頁面或文件,走線上動態(tài)獲取的方式。這個功能只有在web端的 requirejs中有,ReactNative和webpack中都是不支持的。要實現(xiàn)此項功能,需要對React Native中的require進(jìn)行擴張。
按需加載react-native模塊
不論是reactjs還是react-native,在代碼的組織方式上都是按模塊進(jìn)行劃分的。可能Facebook也意識到react框架太大了。這個模塊劃分的方式,給開發(fā)者們的按需載入創(chuàng)造了機會。
實現(xiàn)
這里簡單闡述下部分功能的實現(xiàn)思路:
React Native自身模塊拆分
在打完的main.jsbundle中,常常會看到好多polyfills的文件,那這些文件從哪里來的呢。打開
文件,會看到這些polyfills文件都是在這里設(shè)置的,
由名字可以看出,這些是用來對es6、es7進(jìn)行適配的。所以代碼中如果只有es5的語法是不是就可以不需要這些文件了呢,這也是個優(yōu)化點,不過看起來量不大。
有人可能經(jīng)常會有這樣的想法:我們實際項目中用到的React Native模塊其實并沒幾個,我們在打包的時候,是否可以只打包我們需要的模塊呢。我們找到文件
可以看到所有React Native的模塊定義都是在這里了,包括Components、APIs等等。
所以,可以在打包的時候,根據(jù)實際情況,通過腳本等手段,注釋掉一些用不到或不常用的模塊以減少輸出的體積。當(dāng)然也可以把部分不常用的模塊,抽出來單獨作為一個文件,在需要的時候,通過按需加載的方式引入進(jìn)來。
業(yè)務(wù)模塊拆分
App的設(shè)計一般都是按照業(yè)務(wù)線劃分的。每個業(yè)務(wù)都對應(yīng)一套自己的邏輯。當(dāng)然也有部分業(yè)務(wù)線會出現(xiàn)依賴情況。按React Native提供的打包方法,將所有業(yè)務(wù)線的邏輯都打在一起,勢必會造成好多業(yè)務(wù)線代碼的浪費。有可能那個業(yè)務(wù)線就根本不會被用戶訪問到。所以我們就想著能不能將一些基礎(chǔ)的、公共的業(yè)務(wù)線打在一起,其它獨立的業(yè)務(wù)線都各自獨立成包。
React Native提供的打包方法允許輸入一個入口文件,那么這個入口文件可以是整個App的入口,也可以是各業(yè)務(wù)線自己的入口。由此我們可以將各業(yè)務(wù)線單獨成包,但這樣的結(jié)果并不能直接投入使用。可以想到,這里并沒有過濾機制,各業(yè)務(wù)線之間一些模塊會被重復(fù)的打進(jìn)去也包括react-native模塊。而 React Native打包提供的參數(shù)中也只有blacklist會涉及一些過濾,但卻無法滿足我們的需求。
還好packager為我們提供了很多可以的API。通過參考
中的打包邏輯,我們以一個入口文件的打包為例,可以將打包邏輯設(shè)計成如下:
加載打包需要用到的模塊
創(chuàng)建client
調(diào)用outputBundle進(jìn)行打包,將打包后的bundle返回
分析bundle中的module,將符合條件的module加入到新的bundle中
定義一個新的bundle
定義過濾機制
上只是個簡單的過濾,在復(fù)雜的過濾中,還需要調(diào)用ReactPackager.getDependencies找到每個模塊的依賴,然后在過濾的時候調(diào)用過濾模塊的依賴,依次遞歸才能達(dá)到真正的濾掉。
對module進(jìn)行合并、替換等處理
調(diào)用outputBundle輸出新的bundle
到此,一個帶有過濾功能的打包腳本就基本成型了,之后的多文件入口同時打包的功能,也就是要在上面做些擴擴展就可以了。
在打包方面,其實也也可走網(wǎng)絡(luò)打包,packager的網(wǎng)絡(luò)打包邏輯中,凡是請求以.bundle結(jié)尾的文件,都會對這個文件進(jìn)行打包。而其它格式 的文件,則請求什么返回什么。所以可以根據(jù)該特性來實現(xiàn)打包。可以將過濾條件作為querystring的方式傳遞過去,然后在
文件中對querystring進(jìn)行攔截,并實現(xiàn)其過濾功能。
然而在實際的拆包中會發(fā)現(xiàn),packager中打出的包都會將模塊名稱替換為數(shù)字id。這導(dǎo)致拆出的包中,引入不到某些模塊,因為不是在一起打包,模塊的id都對不上,或者會出現(xiàn)重復(fù)的情況。
我們的思路是打包的時候不進(jìn)行id的替換,依然使用原有的模塊名稱,做到類似在web中requireJS使用的那樣。 找到文件
將如下代碼中的moduleName,替換為model的絕對路徑
這樣只完成了define(如:define(0,...))中的名稱替換,我們還需要找到require(如:require(0))中的名稱替換,于是找到如下文件
在super(BundleBase)中,定義一個獲取模塊的方法getModuleName,將下面的super.getMainModuleId替換為super.getModuleName,這樣在_addRequireCall就可以拿到模塊的絕對路徑了
這樣就完成了模塊名稱的保留,我們就可以愉快的使用我們的拆包模塊了。
按需加載實現(xiàn)
經(jīng)過上面的介紹,我們已經(jīng)完成了模塊的拆分。那么光有獨立的模塊還是不能讓App運行起來,需要有一種能力將這些模塊聯(lián)系起來,這就是模塊加載機制。
常規(guī)的加載會有如下兩種場景:
1、本地模塊
有時候為了加快頁面打開速度,我們常常會選擇將首頁和非首頁的頁面進(jìn)行分開打包,在App啟動時,只加載首頁的模塊,待首頁模塊加載完畢后,再去異步的加載后續(xù)頁面的模塊。這里的本地模塊加載就是用在這種場景中。那么在React Native中該如何實現(xiàn)這種加載方式呢。
要讀寫本地文件,光有javascript是辦不到的,所以一定要借助native的能力。簡單的代碼實現(xiàn)如下:
代碼的流程為:按照React Native中對native模塊封裝的規(guī)范,實現(xiàn)RCTBridgeModule協(xié)議,并通過定義宏RCT_EXPORT_MODULE、RCT_EXPORT_METHOD將native模塊的功能暴露給javascript來調(diào)用。
在native的模塊中,采用NSBundle的 pathForResource方法,將文件路拿到。再借助NSString的initWithContentsOfFile方法獲取到文件的內(nèi)容。然后在javascript中,將拿到的內(nèi)容,進(jìn)行一次包裝,如:
最后調(diào)用eval,便可將拿到的內(nèi)容執(zhí)行到當(dāng)前的jscontext中。
2、線上模塊
在App的開發(fā)中經(jīng)常會為了控制size大小而發(fā)愁,尤其是蘋果的100m限制,所以各業(yè)務(wù)線都在絞盡腦汁的想辦法減size。自然而然的大家就想到了將一些資源放在服務(wù)端,在需要的時候?qū)⑵洚惒郊虞d下來,也就是常常聽說的直連。對于服務(wù)器異步加載的實現(xiàn),代碼如下:
代碼的流程為:采用React Native提供的fetch方法,將需要的模塊異步的從服務(wù)器上拉回來,接下來的動作,和上面的“本地模塊”的邏輯一樣。在實際的模塊加載中,我們還需要對模塊進(jìn)行緩存,以提高模塊的訪問速度。
后續(xù)
在經(jīng)過上面的介紹中,我們應(yīng)該大概知道拆包和按需加載的實現(xiàn)原理。但是大家也都看到了,這要侵入react-native的代碼中,進(jìn)行很多地方的修改。這樣不利于之后對react-native的版本升級。所以我們需要想一種更合理的解決辦法。也就是我們現(xiàn)在正在做的一個嘗試。
將React Native中的cmd模塊,在線下或運行時編譯為AMD模塊,然后調(diào)用r.js的來對其進(jìn)行打包,以達(dá)到干凈的完成拆包和按需加載的功能。而且r.js 的打包配置的靈活度我覺得比packager、webpack、browserify等工具都靈活好使。
Q&A
問:是否考慮過多個業(yè)務(wù)公用一套rn的基礎(chǔ)庫?
魏曉軍:是。
問:如果有,怎么做版本控制?
魏曉軍:目前通文件夾控制,在我們的app中,基礎(chǔ)框架一般只維護(hù)2個版本,再要有新的版本就會推動下掉一個老版本。
問:線上資源的更新策略是什么樣的?例如攜程酒店和機票是公用一套rn的底層庫嗎?
魏曉軍:更新策略,通過md5對比,差分到文件級別。酒店和機票現(xiàn)在還沒上rn版本,若上,則都是公用一套rn底層。
問:用r.js打包react-native比webpack靈活在哪里呢?
魏曉軍:這都是相對的,webpack有它獨特的優(yōu)勢。這里我只拿r.js中的path、module等屬性的概念來做對比,webpack在這方就相對弱了,拆包也只能通過代碼中的require.entry來識別。
問:官方 RN 是在不斷的迭代更新的,想請問下攜程實際使用的是什么版本,和官方 RN 有差異嗎?
魏曉軍:我們目前是基于0.23開發(fā)的。
問:和官方 RN 保持同步更新嗎?策略又是怎樣的?
魏曉軍:不同步更新,也沒法同步更新。只有看到某些特別的亮點后,會選擇跨越式的更新,如從0.23可能直接到0.32。
關(guān)于本文
作者:魏曉軍@互聯(lián)網(wǎng)技術(shù)聯(lián)盟
聯(lián)系客服