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

引言

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默認提供的打包方式有兩種:

·?離線打包

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ù)可以打開如下文件進行查看:

$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

不過,不論哪種方式都是只有一個“entry-file”,然后根據(jù)“entry-file”去進行依賴分析、文件壓縮等操作,最后輸出在 “bundle-output”中。然后通過NSBundle的URLForResource方法來指定加載打好的的bundle文件。如:

jsCodeLocation = [[NSBundlemainBundle] URLForResource:@"main"withExtension:@"jsbundle"];

這樣的打包模式,對用戶體驗來說是非常不錯的。但是考慮到國內(nèi)的網(wǎng)絡(luò)狀況以及對App size的控制,打成一個Bundle的模式在國內(nèi)還是行不通的。

思考

在傳統(tǒng)的Hybrid開發(fā)中,要解決文件太大的問題,我們常常會想到如下幾個辦法:

· ?進行拆包

· ?按需加載本地文件

· ?按需加載線上文件

那么,能否把Hybrid開發(fā)中的經(jīng)驗應(yīng)用在React Native項目中呢。在React Native項目中,針對文件大的問題,我們做了如下嘗試:

· ?多業(yè)務(wù)進行拆包

借助gulp、grunt等工具,通過配置不同的任務(wù),在調(diào)用React Native提供的打包命令,可以將App打包成多個文件。

· ?按需加載本地文件

在開發(fā)環(huán)境的情況下,React Native是支持加載本地文件的。這里想要做的是,在打包完的bundle中也可以加載本地文件,這就需要對require進行擴張了。

· ? 按需加載線上文件

在開發(fā)Hybrid時,為了減少包體積。開發(fā)者們經(jīng)常會將一些不重要的頁面或文件,走線上動態(tài)獲取的方式。這個功能只有在web端的 requirejs中有,ReactNative和webpack中都是不支持的。要實現(xiàn)此項功能,需要對React Native中的require進行擴張。

· ? 按需加載react-native模塊

不論是reactjs還是react-native,在代碼的組織方式上都是按模塊進行劃分的。可能Facebook也意識到react框架太大了。這個模塊劃分的方式,給開發(fā)者們的按需載入創(chuàng)造了機會。

實現(xiàn)

這里簡單闡述下部分功能的實現(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進行適配的。所以代碼中如果只有es5的語法是不是就可以不需要這些文件了呢,這也是個優(yōu)化點,不過看起來量不大。

有人可能經(jīng)常會有這樣的想法:我們實際項目中用到的React Native模塊其實并沒幾個,我們在打包的時候,是否可以只打包我們需要的模塊呢。我們找到文件

/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'); },

......

所以,可以在打包的時候,根據(jù)實際情況,通過腳本等手段,注釋掉一些用不到或不常用的模塊以減少輸出的體積。當然也可以把部分不常用的模塊,抽出來單獨作為一個文件,在需要的時候,通過按需加載的方式引入進來。

· ? 業(yè)務(wù)模塊拆分

App的設(shè)計一般都是按照業(yè)務(wù)線劃分的。每個業(yè)務(wù)都對應(yī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ù)的打進去也包括react-native模塊。而 React Native打包提供的參數(shù)中也只有blacklist會涉及一些過濾,但卻無法滿足我們的需求。

還好packager為我們提供了很多可以的API。通過參考

/node_modules/react-native/local-cli/bundle/buildBundle.js

中的打包邏輯,我們以一個入口文件的打包為例,可以將打包邏輯設(shè)計成如下

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進行打包,將打包后的bundle返回

outputBundle.build(client, {

entryFile: config.file,

......})

4、分析bundle中的module,將符合條件的module加入到新的bundle中

定義一個新的bundle

var newBundle = new Bundle();

bundle.getModules().forEach(function(module) {

if(filter(module.sourcePath)){

newBundle._modules.push(module);

}......})

5、定義過濾機制

function filter(path){

var ret = true;

if(

(path.indexOf('/react-native/')!=-1)||

(sourcePath.indexOf('/fbjs/')!=-1)||

......){ret = false;}return ret;}

上只是個簡單的過濾,在復(fù)雜的過濾中,還需要調(diào)用ReactPackager.getDependencies找到每個模塊的依賴,然后在過濾的時候調(diào)用過濾模塊的依賴,依次遞歸才能達到真正的濾掉。

6、對module進行合并、替換等處理

newBundle.finalize()

7、調(diào)用outputBundle輸出新的bundle

outputBundle.save(newBundle, {},false)

到此,一個帶有過濾功能的打包腳本就基本成型了,之后的多文件入口同時打包的功能,也就是要在上面做些擴擴展就可以了。

在打包方面,其實也也可走網(wǎng)絡(luò)打包,packager的網(wǎng)絡(luò)打包邏輯中,凡是請求以.bundle結(jié)尾的文件,都會對這個文件進行打包。而其它格式 的文件,則請求什么返回什么。所以可以根據(jù)該特性來實現(xiàn)打包。可以將過濾條件作為querystring的方式傳遞過去,然后在

react-native/packager/react-packager/src/Bundler/index.js

文件中對querystring進行攔截,并實現(xiàn)其過濾功能。

然而在實際的拆包中會發(fā)現(xiàn),packager中打出的包都會將模塊名稱替換為數(shù)字id。這導(dǎo)致拆出的包中,引入不到某些模塊,因為不是在一起打包,模塊的id都對不上,或者會出現(xiàn)重復(fù)的情況。

我們的思路是打包的時候不進行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)中,定義一個獲取模塊的方法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();}

這樣就完成了模塊名稱的保留,我們就可以愉快的使用我們的拆包模塊了。

· ?按需加載實現(xiàn)

經(jīng)過上面的介紹,我們已經(jīng)完成了模塊的拆分。那么光有獨立的模塊還是不能讓App運行起來,需要有一種能力將這些模塊聯(lián)系起來,這就是模塊加載機制。

常規(guī)的加載會有如下兩種場景:

1、本地模塊

有時候為了加快頁面打開速度,我們常常會選擇將首頁和非首頁的頁面進行分開打包,在App啟動時,只加載首頁的模塊,待首頁模塊加載完畢后,再去異步的加載后續(xù)頁面的模塊。這里的本地模塊加載就是用在這種場景中。那么在React Native中該如何實現(xiàn)這種加載方式呢。

要讀寫本地文件,光有javascript是辦不到的,所以一定要借助native的能力。簡單的代碼實現(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ī)范,實現(xiàn)RCTBridgeModule協(xié)議,并通過定義宏RCT_EXPORT_MODULE、RCT_EXPORT_METHOD將native模塊的功能暴露給javascript來調(diào)用。

在native的模塊中,采用NSBundle的 pathForResource方法,將文件路拿到。再借助NSString的initWithContentsOfFile方法獲取到文件的內(nèi)容。然后在javascript中,將拿到的內(nèi)容,進行一次包裝,如:

var str='__d("'+filePath+'", function(global, require, module, exports) {'+

content+

'})'

最后調(diào)用eval,便可將拿到的內(nèi)容執(zhí)行到當前的jscontext中。

2、線上模塊

在App的開發(fā)中經(jīng)常會為了控制size大小而發(fā)愁,尤其是蘋果的100m限制,所以各業(yè)務(wù)線都在絞盡腦汁的想辦法減size。自然而然的大家就想到了將一些資源放在服務(wù)端,在需要的時候?qū)⑵洚惒郊虞d下來,也就是常常聽說的直連。對于服務(wù)器異步加載的實現(xiàn),代碼如下:

fetch(filePath)

.then((response) =>response.text())

.then((responseText) => {

......

代碼的流程為:采用React Native提供的fetch方法,將需要的模塊異步的從服務(wù)器上拉回來,接下來的動作,和上面的“本地模塊”的邏輯一樣。在實際的模塊加載中,我們還需要對模塊進行緩存,以提高模塊的訪問速度。

后續(xù)

在經(jīng)過上面的介紹中,我們應(yīng)該大概知道拆包和按需加載的實現(xiàn)原理。但是大家也都看到了,這要侵入react-native的代碼中,進行很多地方的修改。這樣不利于之后對react-native的版本升級。所以我們需要想一種更合理的解決辦法。也就是我們現(xiàn)在正在做的一個嘗試。

將React Native中的cmd模塊,在線下或運行時編譯為AMD模塊,然后調(diào)用r.js的來對其進行打包,以達到干凈的完成拆包和按需加載的功能。而且r.js 的打包配置的靈活度我覺得比packager、webpack、browserify等工具都靈活好使。

Q&A

問:是否考慮過多個業(yè)務(wù)公用一套rn的基礎(chǔ)庫?

魏曉軍:是。

問:如果有,怎么做版本控制?

魏曉軍:目前通文件夾控制,在我們的app中,基礎(chǔ)框架一般只維護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。

原文

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,401評論 6 531
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,011評論 3 413
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,263評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,543評論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,323評論 6 404
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 54,874評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 42,968評論 3 439
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,095評論 0 286
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,605評論 1 331
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 40,551評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,720評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,242評論 5 355
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 43,961評論 3 345
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,358評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,612評論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,330評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,690評論 2 370

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