React Native 拆分業(yè)務(wù)包 bundle拆包 分包 攜程方案

引言

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敦间。

原文

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末瓶逃,一起剝皮案震驚了整個(gè)濱河市束铭,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌厢绝,老刑警劉巖契沫,帶你破解...
    沈念sama閱讀 221,820評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異昔汉,居然都是意外死亡懈万,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,648評論 3 399
  • 文/潘曉璐 我一進(jìn)店門靶病,熙熙樓的掌柜王于貴愁眉苦臉地迎上來会通,“玉大人,你說我怎么就攤上這事娄周√槌蓿” “怎么了?”我有些...
    開封第一講書人閱讀 168,324評論 0 360
  • 文/不壞的土叔 我叫張陵煤辨,是天一觀的道長裳涛。 經(jīng)常有香客問我,道長众辨,這世上最難降的妖魔是什么调违? 我笑而不...
    開封第一講書人閱讀 59,714評論 1 297
  • 正文 為了忘掉前任,我火速辦了婚禮泻轰,結(jié)果婚禮上技肩,老公的妹妹穿的比我還像新娘。我一直安慰自己浮声,他們只是感情好虚婿,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,724評論 6 397
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著泳挥,像睡著了一般然痊。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上屉符,一...
    開封第一講書人閱讀 52,328評論 1 310
  • 那天剧浸,我揣著相機(jī)與錄音,去河邊找鬼矗钟。 笑死唆香,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的吨艇。 我是一名探鬼主播躬它,決...
    沈念sama閱讀 40,897評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼东涡!你這毒婦竟也來了冯吓?” 一聲冷哼從身側(cè)響起倘待,我...
    開封第一講書人閱讀 39,804評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎组贺,沒想到半個(gè)月后凸舵,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,345評論 1 318
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡失尖,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,431評論 3 340
  • 正文 我和宋清朗相戀三年啊奄,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片雹仿。...
    茶點(diǎn)故事閱讀 40,561評論 1 352
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖整以,靈堂內(nèi)的尸體忽然破棺而出胧辽,到底是詐尸還是另有隱情,我是刑警寧澤公黑,帶...
    沈念sama閱讀 36,238評論 5 350
  • 正文 年R本政府宣布邑商,位于F島的核電站,受9級特大地震影響凡蚜,放射性物質(zhì)發(fā)生泄漏人断。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,928評論 3 334
  • 文/蒙蒙 一朝蜘、第九天 我趴在偏房一處隱蔽的房頂上張望恶迈。 院中可真熱鬧,春花似錦谱醇、人聲如沸暇仲。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,417評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽奈附。三九已至,卻和暖如春煮剧,著一層夾襖步出監(jiān)牢的瞬間斥滤,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,528評論 1 272
  • 我被黑心中介騙來泰國打工勉盅, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留佑颇,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,983評論 3 376
  • 正文 我出身青樓草娜,卻偏偏與公主長得像漩符,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個(gè)殘疾皇子驱还,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,573評論 2 359

推薦閱讀更多精彩內(nèi)容