引言
React Native以其獨(dú)到的特性崖咨,吸引著互聯(lián)網(wǎng)公司紛紛為之投入或多或少的人力。在實(shí)際的開發(fā)過程中油吭,開發(fā)者們也確實(shí)嘗到了甜頭击蹲,它的組件化思想署拟、熱更新機(jī)制 以及jsx和es6等的引入,都給開發(fā)者們帶來了很大的便利歌豺。也難怪在npm和github上推穷,每天都會有很多react-native的新模塊出現(xiàn)。這也充分表明了各大公司對其的看好类咧。
然而馒铃,從目前qq群、微信公眾號轮听、社區(qū)骗露、論壇等各大信息交流平臺中了解到,大家都是保持在研究和觀望狀態(tài)血巍,頂多把某個(gè)不重要的頁面交給React Native來練手萧锉。其中緣由紛繁復(fù)雜。
今天我們這里主要是探討——bundle文件太大
不想看原理述寡,直接看怎么使用請點(diǎn)擊這里柿隙,開源鏈接
現(xiàn)狀
React Native應(yīng)用的開發(fā)者們,在項(xiàng)目開發(fā)完后鲫凶,都會遇到一個(gè)問題禀崖,生成的bundle文件太大。一個(gè)AwesomeProject項(xiàng)目螟炫,在沒有什么邏輯代 碼的情況下波附,打完之后約530k。隨著業(yè)務(wù)的增多昼钻,業(yè)務(wù)復(fù)雜性的上升掸屡,文件的大小勢必會急劇增大。react-native打包成一個(gè)bundle的做 法然评,必定是要得到解決的仅财。
分析
react-native默認(rèn)提供的打包方式有兩種:
·?離線打包
react-native bundle
--entry-file index.ios.js
--platform ios
--dev true
--bundle-output dest/main.jsbundle
--assets-dest dest
·?在線打包
http://localhost:8081/index.ios.bundle?platform=ios&dev=true
具體有哪些參數(shù)可以打開如下文件進(jìn)行查看:
$youProjectRoot/AwesomeProject/node_modules/react-native/local-cli/bundle/bundleCommandLineArgs.js
如:
module.exports = [{
command: 'entry-file',
description: 'Path to the root JS file,either absolute or relative to JS root',
type: 'string',
required: true,},
......
官網(wǎng)中還給出了一些其它的使用方式,地址:
https://github.com/facebook/react-native/tree/master/packager
不過碗淌,不論哪種方式都是只有一個(gè)“entry-file”盏求,然后根據(jù)“entry-file”去進(jìn)行依賴分析、文件壓縮等操作,最后輸出在 “bundle-output”中。然后通過NSBundle的URLForResource方法來指定加載打好的的bundle文件混卵。如:
jsCodeLocation = [[NSBundlemainBundle] URLForResource:@"main"withExtension:@"jsbundle"];
這樣的打包模式,對用戶體驗(yàn)來說是非常不錯(cuò)的魂莫。但是考慮到國內(nèi)的網(wǎng)絡(luò)狀況以及對App size的控制,打成一個(gè)Bundle的模式在國內(nèi)還是行不通的爹耗。
思考
在傳統(tǒng)的Hybrid開發(fā)中耙考,要解決文件太大的問題谜喊,我們常常會想到如下幾個(gè)辦法:
· ?進(jìn)行拆包
· ?按需加載本地文件
· ?按需加載線上文件
那么,能否把Hybrid開發(fā)中的經(jīng)驗(yàn)應(yīng)用在React Native項(xiàng)目中呢倦始。在React Native項(xiàng)目中斗遏,針對文件大的問題,我們做了如下嘗試:
· ?多業(yè)務(wù)進(jìn)行拆包
借助gulp鞋邑、grunt等工具诵次,通過配置不同的任務(wù),在調(diào)用React Native提供的打包命令枚碗,可以將App打包成多個(gè)文件逾一。
· ?按需加載本地文件
在開發(fā)環(huán)境的情況下,React Native是支持加載本地文件的肮雨。這里想要做的是遵堵,在打包完的bundle中也可以加載本地文件,這就需要對require進(jìn)行擴(kuò)張了怨规。
· ? 按需加載線上文件
在開發(fā)Hybrid時(shí)陌宿,為了減少包體積。開發(fā)者們經(jīng)常會將一些不重要的頁面或文件波丰,走線上動態(tài)獲取的方式壳坪。這個(gè)功能只有在web端的 requirejs中有,ReactNative和webpack中都是不支持的掰烟。要實(shí)現(xiàn)此項(xiàng)功能爽蝴,需要對React Native中的require進(jìn)行擴(kuò)張。
· ? 按需加載react-native模塊
不論是reactjs還是react-native纫骑,在代碼的組織方式上都是按模塊進(jìn)行劃分的蝎亚。可能Facebook也意識到react框架太大了惧磺。這個(gè)模塊劃分的方式,給開發(fā)者們的按需載入創(chuàng)造了機(jī)會捻撑。
實(shí)現(xiàn)
這里簡單闡述下部分功能的實(shí)現(xiàn)思路:
· ? React Native自身模塊拆分
在打完的main.jsbundle中磨隘,常常會看到好多polyfills的文件,那這些文件從哪里來的呢顾患。打開
node_modules/react-native/packager/react-packager/src/Resolver/index.js
文件番捂,會看到這些polyfills文件都是在這里設(shè)置的,
path.join(__dirname,'polyfills/String.prototype.es6.js'),
path.join(__dirname,'polyfills/Array.prototype.es6.js'),
......
由名字可以看出江解,這些是用來對es6设预、es7進(jìn)行適配的。所以代碼中如果只有es5的語法是不是就可以不需要這些文件了呢犁河,這也是個(gè)優(yōu)化點(diǎn)鳖枕,不過看起來量不大魄梯。
有人可能經(jīng)常會有這樣的想法:我們實(shí)際項(xiàng)目中用到的React Native模塊其實(shí)并沒幾個(gè),我們在打包的時(shí)候宾符,是否可以只打包我們需要的模塊呢酿秸。我們找到文件
/node_modules/react-native/Libraries/react-native/react-native.js
可以看到所有React Native的模塊定義都是在這里了,包括Components魏烫、APIs等等辣苏。
var ReactNative = {
// Components
get ActivityIndicatorIOS() { return require('ActivityIndicatorIOS'); },
get ART() { return require('ReactNativeART'); },
......
所以,可以在打包的時(shí)候哄褒,根據(jù)實(shí)際情況稀蟋,通過腳本等手段,注釋掉一些用不到或不常用的模塊以減少輸出的體積呐赡。當(dāng)然也可以把部分不常用的模塊退客,抽出來單獨(dú)作為一個(gè)文件,在需要的時(shí)候罚舱,通過按需加載的方式引入進(jìn)來井辜。
· ? 業(yè)務(wù)模塊拆分
App的設(shè)計(jì)一般都是按照業(yè)務(wù)線劃分的。每個(gè)業(yè)務(wù)都對應(yīng)一套自己的邏輯管闷。當(dāng)然也有部分業(yè)務(wù)線會出現(xiàn)依賴情況粥脚。按React Native提供的打包方法,將所有業(yè)務(wù)線的邏輯都打在一起包个,勢必會造成好多業(yè)務(wù)線代碼的浪費(fèi)刷允。有可能那個(gè)業(yè)務(wù)線就根本不會被用戶訪問到。所以我們就想著能不能將一些基礎(chǔ)的碧囊、公共的業(yè)務(wù)線打在一起树灶,其它獨(dú)立的業(yè)務(wù)線都各自獨(dú)立成包。
React Native提供的打包方法允許輸入一個(gè)入口文件糯而,那么這個(gè)入口文件可以是整個(gè)App的入口天通,也可以是各業(yè)務(wù)線自己的入口。由此我們可以將各業(yè)務(wù)線單獨(dú)成包熄驼,但這樣的結(jié)果并不能直接投入使用像寒。可以想到瓜贾,這里并沒有過濾機(jī)制诺祸,各業(yè)務(wù)線之間一些模塊會被重復(fù)的打進(jìn)去也包括react-native模塊。而 React Native打包提供的參數(shù)中也只有blacklist會涉及一些過濾祭芦,但卻無法滿足我們的需求筷笨。
還好packager為我們提供了很多可以的API。通過參考
/node_modules/react-native/local-cli/bundle/buildBundle.js
中的打包邏輯,我們以一個(gè)入口文件的打包為例胃夏,可以將打包邏輯設(shè)計(jì)成如下:
1轴或、加載打包需要用到的模塊
var config = require('config.js')
var ReactPackager =require('react-native/packager/react-packager')
var Bundle =require('react-native/packager/react-packager/src/Bundler/Bundle')
var saveAssets =require('react-native/local-cli/bundle/saveAssets')
var outputBundle =require('react-native/local-cli/bundle/output/bundle')
2、創(chuàng)建client
var client =ReactPackager.createClientFor({
projectRoots: config.projectRoots,
blacklistRE: config.blacklistRE,
...})
3构订、調(diào)用outputBundle進(jìn)行打包侮叮,將打包后的bundle返回
outputBundle.build(client, {
entryFile: config.file,
......})
4、分析bundle中的module悼瘾,將符合條件的module加入到新的bundle中
定義一個(gè)新的bundle
var newBundle = new Bundle();
bundle.getModules().forEach(function(module) {
if(filter(module.sourcePath)){
newBundle._modules.push(module);
}......})
5囊榜、定義過濾機(jī)制
function filter(path){
var ret = true;
if(
(path.indexOf('/react-native/')!=-1)||
(sourcePath.indexOf('/fbjs/')!=-1)||
......){ret = false;}return ret;}
上只是個(gè)簡單的過濾,在復(fù)雜的過濾中亥宿,還需要調(diào)用ReactPackager.getDependencies找到每個(gè)模塊的依賴卸勺,然后在過濾的時(shí)候調(diào)用過濾模塊的依賴,依次遞歸才能達(dá)到真正的濾掉烫扼。
6曙求、對module進(jìn)行合并、替換等處理
newBundle.finalize()
7映企、調(diào)用outputBundle輸出新的bundle
outputBundle.save(newBundle, {},false)
到此悟狱,一個(gè)帶有過濾功能的打包腳本就基本成型了,之后的多文件入口同時(shí)打包的功能堰氓,也就是要在上面做些擴(kuò)擴(kuò)展就可以了挤渐。
在打包方面,其實(shí)也也可走網(wǎng)絡(luò)打包双絮,packager的網(wǎng)絡(luò)打包邏輯中浴麻,凡是請求以.bundle結(jié)尾的文件,都會對這個(gè)文件進(jìn)行打包囤攀。而其它格式 的文件软免,則請求什么返回什么。所以可以根據(jù)該特性來實(shí)現(xiàn)打包焚挠「嘞簦可以將過濾條件作為querystring的方式傳遞過去,然后在
react-native/packager/react-packager/src/Bundler/index.js
文件中對querystring進(jìn)行攔截蝌衔,并實(shí)現(xiàn)其過濾功能榛泛。
然而在實(shí)際的拆包中會發(fā)現(xiàn),packager中打出的包都會將模塊名稱替換為數(shù)字id胚委。這導(dǎo)致拆出的包中挟鸠,引入不到某些模塊叉信,因?yàn)椴皇窃谝黄鸫虬抖K的id都對不上,或者會出現(xiàn)重復(fù)的情況。
我們的思路是打包的時(shí)候不進(jìn)行id的替換硅急,依然使用原有的模塊名稱覆享,做到類似在web中requireJS使用的那樣。 找到文件
node_modules/react-native/packager/reat-packager/src/Resolver/index.js
將如下代碼中的moduleName营袜,替換為model的絕對路徑
functiondefineModuleCode(moduleName, code, verboseName = '') {
return [
`__d(`,
`${JSON.stringify(moduleName)}/*<-替換的地方*/ /* ${verboseName} */, `,
`function(global, require, module, exports){`,
`${code}`,
'\n});',
].join('');
}
這樣只完成了define(如:define(0,...))中的名稱替換撒顿,我們還需要找到require(如:require(0))中的名稱替換,于是找到如下文件
node_modules/react-native/packager/reat-packager/src/Bundle/Bundle.js
在super(BundleBase)中荚板,定義一個(gè)獲取模塊的方法getModuleName凤壁,將下面的super.getMainModuleId替換為super.getModuleName,這樣在_addRequireCall就可以拿到模塊的絕對路徑了
_addRequireCall(moduleId) {
const code =`;require(${JSON.stringify(moduleId)});`;
const name = 'require-' + moduleId;
......
}finalize(options) {
options = options || {};
if (options.runMainModule) {
options.runBeforeMainModule.forEach(this._addRequireCall,this);
this._addRequireCall(super.getMainModuleId());/*<-替換的地方*/
}super.finalize();}
這樣就完成了模塊名稱的保留跪另,我們就可以愉快的使用我們的拆包模塊了拧抖。
· ?按需加載實(shí)現(xiàn)
經(jīng)過上面的介紹,我們已經(jīng)完成了模塊的拆分免绿。那么光有獨(dú)立的模塊還是不能讓App運(yùn)行起來唧席,需要有一種能力將這些模塊聯(lián)系起來,這就是模塊加載機(jī)制嘲驾。
常規(guī)的加載會有如下兩種場景:
1淌哟、本地模塊
有時(shí)候?yàn)榱思涌祉撁娲蜷_速度,我們常常會選擇將首頁和非首頁的頁面進(jìn)行分開打包辽故,在App啟動時(shí)徒仓,只加載首頁的模塊,待首頁模塊加載完畢后榕暇,再去異步的加載后續(xù)頁面的模塊蓬衡。這里的本地模塊加載就是用在這種場景中。那么在React Native中該如何實(shí)現(xiàn)這種加載方式呢彤枢。
要讀寫本地文件狰晚,光有javascript是辦不到的,所以一定要借助native的能力缴啡。簡單的代碼實(shí)現(xiàn)如下:
#import"RCTBridgeModule.h"
@implementation RequireLocal
RCT_EXPORT_MODULE()
RCT_EXPORT_METHOD(loadPath:(NSString*)path callback:(RCTResponseSenderBlock)callback)
{
NSString *filePath = [[NSBundle mainBundle] pathForResource:pathofType:nil];
if ([[NSFileManager defaultManager] fileExistsAtPath:filePath]) {
NSString *content = [[NSString alloc]initWithContentsOfFile:filePath
......
}
@end
代碼的流程為:按照React Native中對native模塊封裝的規(guī)范壁晒,實(shí)現(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)行一次包裝不撑,如:
var str='__d("'+filePath+'", function(global, require, module, exports) {'+
content+
'})'
最后調(diào)用eval,便可將拿到的內(nèi)容執(zhí)行到當(dāng)前的jscontext中晤斩。
2焕檬、線上模塊
在App的開發(fā)中經(jīng)常會為了控制size大小而發(fā)愁,尤其是蘋果的100m限制澳泵,所以各業(yè)務(wù)線都在絞盡腦汁的想辦法減size实愚。自然而然的大家就想到了將一些資源放在服務(wù)端,在需要的時(shí)候?qū)⑵洚惒郊虞d下來兔辅,也就是常常聽說的直連腊敲。對于服務(wù)器異步加載的實(shí)現(xiàn),代碼如下:
fetch(filePath)
.then((response) =>response.text())
.then((responseText) => {
......
代碼的流程為:采用React Native提供的fetch方法维苔,將需要的模塊異步的從服務(wù)器上拉回來兔仰,接下來的動作,和上面的“本地模塊”的邏輯一樣蕉鸳。在實(shí)際的模塊加載中乎赴,我們還需要對模塊進(jìn)行緩存,以提高模塊的訪問速度潮尝。
后續(xù)
在經(jīng)過上面的介紹中榕吼,我們應(yīng)該大概知道拆包和按需加載的實(shí)現(xiàn)原理。但是大家也都看到了勉失,這要侵入react-native的代碼中羹蚣,進(jìn)行很多地方的修改。這樣不利于之后對react-native的版本升級乱凿。所以我們需要想一種更合理的解決辦法顽素。也就是我們現(xiàn)在正在做的一個(gè)嘗試。
將React Native中的cmd模塊徒蟆,在線下或運(yùn)行時(shí)編譯為AMD模塊胁出,然后調(diào)用r.js的來對其進(jìn)行打包,以達(dá)到干凈的完成拆包和按需加載的功能段审。而且r.js 的打包配置的靈活度我覺得比packager全蝶、webpack、browserify等工具都靈活好使寺枉。
Q&A
問:是否考慮過多個(gè)業(yè)務(wù)公用一套rn的基礎(chǔ)庫抑淫?
魏曉軍:是。
問:如果有姥闪,怎么做版本控制始苇?
魏曉軍:目前通文件夾控制,在我們的app中筐喳,基礎(chǔ)框架一般只維護(hù)2個(gè)版本催式,再要有新的版本就會推動下掉一個(gè)老版本 往弓。
問:線上資源的更新策略是什么樣的蓄氧?例如攜程酒店和機(jī)票是公用一套rn的底層庫嗎?
魏曉軍:更新策略槐脏,通過md5對比喉童,差分到文件級別顿天。酒店和機(jī)票現(xiàn)在還沒上rn版本堂氯,若上鸟缕,則都是公用一套rn底層。
問:用r.js打包react-native比webpack靈活在哪里呢番甩?
魏曉軍:這都是相對的,webpack有它獨(dú)特的優(yōu)勢缘薛。這里我只拿r.js中的path窍育、module等屬性的概念來做對比,webpack在這方就相對弱了宴胧,拆包也只能通過代碼中的require.entry來識別漱抓。
問:官方 RN 是在不斷的迭代更新的,想請問下攜程實(shí)際使用的是什么版本恕齐,和官方 RN 有差異嗎辽旋?
魏曉軍:我們目前是基于0.23開發(fā)的。
問:和官方 RN 保持同步更新嗎檐迟?策略又是怎樣的补胚?
魏曉軍:不同步更新,也沒法同步更新追迟。只有看到某些特別的亮點(diǎn)后溶其,會選擇跨越式的更新,如從0.23可能直接到0.32敦间。