react-native 拆包/分步加載方案

RN中,發(fā)布js代碼時(shí),會(huì)打包成jsbundle形式然遏。隨著業(yè)務(wù)的增大,jsbundle體積也會(huì)逐漸增大镇防,特別是多Module場(chǎng)景下啦鸣,會(huì)生成多個(gè)jsbundle(包含相同的基礎(chǔ))潮饱。不僅增加APP来氧、熱更新包體積,也對(duì)jsbundle的加載效率造成很大影響香拉。針對(duì)jsbundle的拆包啦扬,成為集成RN必須考慮的問(wèn)題。

拆包目的

  • 解決jsbundle體積過(guò)大
  • 按需分步加載凫碌,提高加載效率
  • 提高熱更新包diff/load效率

react-native bundle 命令

react-native bundle是RN的將js代碼打包成jsbundle命令扑毡。具體使用方式不再贅述,可以通過(guò)react-native bundle -help查看盛险。對(duì)index.js進(jìn)行打包瞄摊,執(zhí)行:

react-native bundle --entry-file ./index.js --dev false --bundle-output index.jsbundle --bundle-encoding utf-8 --platform "ios" 

以index.js為入口,將index.js以及dependence文件打包苦掘,在當(dāng)前目錄生成index.jsbundle换帜。分析index.jsbundle文件(刪減后):

// header:各依賴模塊引用部分
var __DEV__=false,__BUNDLE_START_TIME__=this.nativePerformanceNow?nativePerformanceNow():Date.now(),process=this.process||{};process.env=process.env||{};process.env.NODE_ENV='production';
// body:入口模塊和各業(yè)務(wù)模塊定義部分
__d(function(e,r,a,i,l){var n=r(l[0]);babelHelpers.interopRequireDefault(n);r(l[1])},11,[12,17]);
// footer:入口模塊注冊(cè)部分
require(11);

生成的jsbundle基本分成3個(gè)部分:

  • 頭部:全局定義,主要是define鹤啡,require等全局模塊的定義
  • 中部:模塊定義惯驼,RN框架和業(yè)務(wù)的各個(gè)模塊定義
  • 尾部:引擎初始化和入口函數(shù)執(zhí)行
6DIP96ivBUlcwJOINfUO.jpg

__d是RN自定義的define,__d后面的數(shù)字是模塊的id递瑰,是在RN打包過(guò)程中祟牲,解析依賴關(guān)系,自增長(zhǎng)生成抖部。__d結(jié)構(gòu):

// 一個(gè)基本的結(jié)構(gòu)说贝。注:v0.55,之前的版本define可能不同
__d(
    function(e,r,a,i,l){
        var n=r(l[0]);
        babelHelpers.interopRequireDefault(n);
        r(l[1])
    },
    11,//模塊 id
    [12,17]
);
// define
        __d(
          function(global, ...) { (module transformed code) },
          moduleId,
          dependencyMap?,
          moduleName?
        );

針對(duì)不同模塊的入口js文件打包慎颗,將生成不同jsbundle對(duì)比乡恕,可以發(fā)現(xiàn):

  • jsbundle的頭部相同
  • 中部很多模塊的定義存在大量重復(fù)
  • 如果模塊js中AppRegistry.registerComponent,尾部的入口模塊id基本相同哗总,如上例中的require(11)

實(shí)際上頭部和中部重復(fù)的模塊占用了500K的大小几颜,每個(gè)入口js生成的jsbundle都會(huì)包含這500K代碼。這就是我們拆包需要解決的一個(gè)主要問(wèn)題之一讯屈。

拆包方案

對(duì)RN的bundle命令有了了解蛋哭,我們可以理清一個(gè)思路:將共通基礎(chǔ)部分的模塊define與具體的業(yè)務(wù)模塊define分拆,避免重復(fù)寫(xiě)入jsbundle涮母。
基于這個(gè)思路谆趾,有幾種主流方案:

  1. diff and patch躁愿。將jsbundle通過(guò)diff,生成common和每個(gè)業(yè)務(wù)的patch包沪蓬,然后在APP運(yùn)行時(shí)對(duì)common和patch合并成執(zhí)行的jsbundle彤钟。
  2. 修改RN的bundle命令打包流程,使得直接生成common+business包
  3. 修改RN的unbundle命令跷叉,生成common+business包

實(shí)際上逸雹,方案1是目前其他各種方案的基礎(chǔ),也是最簡(jiǎn)單的拆包方案云挟。方案2梆砸、3需要較強(qiáng)的nodejs編碼能力,另外园欣,RN的升級(jí)非常平凡帖世,變動(dòng)較大,侵入式的生成方案成本較高沸枯。本篇只針對(duì)方案1做介紹日矫,方案2、3另行介紹绑榴,也可參照文末鏈接哪轿,手Q、攜程等解決方案彭沼。
:unbundle命令可以完成默認(rèn)方式的拆包缔逛,生成以__d結(jié)構(gòu)為單位的js文件集。但RN的unbundle不支持iOS平臺(tái)姓惑。

common.js 共通模塊定義

diffandpatch方案的基本原理褐奴,是將業(yè)務(wù)模塊與共通模塊jsbundle進(jìn)行對(duì)比,獲取差異性的patch于毙。所以敦冬,common模塊的定義很重要。一般只包含基本的react引用:

// common.js
import React from 'react'; 
import {} from 'react-native';

以common.js 為--entry-file打包唯沮,生成common.js:

react-native bundle --entry-file ./common.js --dev false --bundle-output common.jsbundle --bundle-encoding utf-8 --platform "ios" 

這樣脖旱,對(duì)RN基礎(chǔ)模塊的define就全部生成在common.jsbundle中。

businese.js 業(yè)務(wù)模塊定義

這里要注意的是介蛉,業(yè)務(wù)js的入口文件萌庆,最好將common.js的代碼加入到頭部,保持與common.js一致:

// business.js
import React from 'react'; 
import {} from 'react-native';
/*
* business contents
*/

這樣打包出來(lái)的business.jsbundle币旧,common部分模塊的id會(huì)保持一致践险,否則可能出現(xiàn)很多id不一致的情況。

business.jsbundle與common.jsbundle 做diff分拆

這里有兩種不同的diff方式。

1. 簡(jiǎn)單diff patch

直接對(duì)business.jsbundle與common.jsbundle做diff巍虫,將差異點(diǎn)生成patch文件彭则。
APP運(yùn)行加載business時(shí),將common.jsbundle和business.patch直接merge合成business.jsbundle占遥。
這種方式簡(jiǎn)單俯抖,實(shí)現(xiàn)快。針對(duì)熱更新瓦胎,也可以通過(guò)patch的方式實(shí)現(xiàn)芬萍。但問(wèn)題是,雖然可以避免common模塊重復(fù)打包進(jìn)jsbundle的問(wèn)題凛捏,但在APP運(yùn)行時(shí)担忧,merge后的business.jsbundle仍然擺脫不了體積大、加載緩慢的問(wèn)題坯癣。

2. 針對(duì)__d模塊define的diff

簡(jiǎn)單diff方案無(wú)法解決加載緩慢的問(wèn)題,本質(zhì)還是到客戶端最欠,一次load整個(gè)業(yè)務(wù)jsbundle的方式問(wèn)題示罗。如果有一種方式,可以動(dòng)態(tài)注入新的jsbundle芝硬,那么蚜点,就可以分步加載js模塊。

2.1 RCTBridge JS動(dòng)態(tài)注入實(shí)現(xiàn)

RN本質(zhì)是通過(guò)jscontext來(lái)實(shí)現(xiàn)js代碼的加載和執(zhí)行拌阴。RN與js的交互方式需要了解底層實(shí)現(xiàn)绍绘。對(duì)于jsbundle加載來(lái)說(shuō),在native側(cè)迟赃,一個(gè)重要的地方陪拘,是RCTBridge對(duì)象的使用。


2824296-d2765ca19b7d4b51.png

RCTRootView內(nèi)部持有了一個(gè)RCTBridge,但是這個(gè)RCTBridge并沒(méi)有太多的代碼纤壁,而是持有了另一個(gè)RCTBatchBridge對(duì)象左刽,大部分的業(yè)務(wù)邏輯都轉(zhuǎn)發(fā)給BatchBridge,BatchBridge里面寫(xiě)著的大量的核心代碼酌媒。
我們可以通過(guò)initWithBundleURL初始化時(shí)欠痴,或者注冊(cè)delegate方式,將jsbundle賦值給bridge:

//  RCTBridge 初始化
- (instancetype)initWithBundleURL:(NSURL *)bundleURL
                   moduleProvider:(RCTBridgeModuleListProvider)block
                    launchOptions:(NSDictionary *)launchOptions
// delegate
- (instancetype)initWithDelegate:(id<RCTBridgeDelegate>)delegate
                   launchOptions:(NSDictionary *)launchOptions

- (NSURL *)sourceURLForBridge:(RCTBridge *)bridge

此時(shí)的jscontext環(huán)境已經(jīng)注入了加載的jsbundle秒咨±桑考察BatchBridge的方法列表,我們發(fā)現(xiàn)一個(gè)私有方法雨席,可以在BatchBridge初始化jsbundle后菩咨,動(dòng)態(tài)注入新的jsbundle

- (void)enqueueApplicationScript:(NSData *)script
                             url:(NSURL *)url
                      onComplete:(dispatch_block_t)onComplete;

我們可以通過(guò)extension的方式,將方法公開(kāi):

@interface RCTBridge()

@property(atomic, strong) RCTBridge *batchedBridge;

- (void)enqueueApplicationScript:(NSData *)script
                             url:(NSURL *)url
                      onComplete:(dispatch_block_t)onComplete;

@end

這樣就可以在后續(xù)向BatchBridge注入新的jsbundle:

// self.bridge = [[RCTBridge alloc] initWithBundleURL:commonJSLocation moduleProvider:nil launchOptions:nil];
NSString *businessPath = [[NSBundle mainBundle] pathForResource:@"business" ofType:@"jsbundle"];
NSData *busJSCode = [NSData businessPath];
[self.bridge.batchedBridge enqueueApplicationScript:busJSCode url:[NSURL businessPath] onComplete:^{
                dispatch_async(dispatch_get_main_queue(), ^{
                    RNFuncViewController *vc = [[RNFuncViewController alloc] initWithModuleName:@"Business" bridge:self.bridge];
                    [self.navigationController pushViewController:vc animated:YES];
                });
            }];

如此,我們就實(shí)現(xiàn)了jsbundle的動(dòng)態(tài)注入旦委。

2.2 js注入為目的的分拆

如果直接拿上面簡(jiǎn)單分拆后的common和business jsbundle直接進(jìn)行分步加載奇徒,RN會(huì)load報(bào)錯(cuò)。這是因?yàn)橛酰ㄟ^(guò)兩次react-native bundle打出的jsbundle摩钙,會(huì)存在相同模塊id的define,產(chǎn)生沖突查辩。diff需要遵循幾個(gè)策略:
2.2.1. 對(duì)比common.jsbundle胖笛,對(duì)于business.jsbundle中多出的__d,需要設(shè)定一個(gè)唯一的start_id宜岛,從start_id開(kāi)始长踊,增量的對(duì)新的__d的模塊id做替換。例如:


1529055374940.jpg
// index2.js
import React from 'react';
import {AppRegistry} from 'react-native';

import App2 from './App2';

AppRegistry.registerComponent('App2', () => App2);

common和index2 存在兩個(gè)差異__d(紅色為差異點(diǎn))萍倡。其中第一個(gè)差異__d的模塊id相同身弊,都為11(實(shí)際上入口模塊id):

// common.jsbundle 差異部分
__d(function(e,r,a,i,l){var n=r(l[0]);babelHelpers.interopRequireDefault(n);r(l[1])},11,[12,17]);
// index2.jsbundle 差異部分
__d(function(e,r,t,n,p){var i=r(p[0]),u=(babelHelpers.interopRequireDefault(i),r(p[1])),l=r(p[2]),a=babelHelpers.interopRequireDefault(l);u.AppRegistry.registerComponent('App2',function(){return a.default})},11,[12,17,307]);
__d(function(e,t,r,n,l){Object.defineProperty(n,"__esModule",{value:!0});var o=t(l[0]),a=babelHelpers.interopRequireDefault(o),s=t(l[1]),i=(function(e){function t(){return babelHelpers.classCallCheck(this,t),babelHelpers.possibleConstructorReturn(this,(t.__proto__||Object.getPrototypeOf(t)).apply(this,arguments))}return babelHelpers.inherits(t,e),babelHelpers.createClass(t,[{key:"render",value:function(){return a.default.createElement(s.View,{style:c.container},a.default.createElement(s.Text,{style:c.welcome},"Welcome to React Native!"),a.default.createElement(s.Text,{style:c.instructions},"To get started, edit AppNext.js"),a.default.createElement(s.Text,{style:c.instructions},"Press Cmd+R to reload,\nCmd+D or shake for dev menu"))}}]),t})(o.Component);n.default=i;var c=s.StyleSheet.create({container:{flex:1,justifyContent:'center',alignItems:'center',backgroundColor:'#F5FCFF'},welcome:{fontSize:20,textAlign:'center',margin:10},instructions:{textAlign:'center',color:'#333333',marginBottom:5}})},307,[12,17]);

設(shè)定index2.jsbundle的start_id為1000,做替換:

// 替換后的index2.jsbundle 差異點(diǎn)
__d(function(e,r,t,n,p){var i=r(p[0]),u=(babelHelpers.interopRequireDefault(i),r(p[1])),l=r(p[2]),a=babelHelpers.interopRequireDefault(l);u.AppRegistry.registerComponent('App2',function(){return a.default})},1000,[12,17,1001]);
__d(function(e,t,r,n,l){Object.defineProperty(n,"__esModule",{value:!0});var o=t(l[0]),a=babelHelpers.interopRequireDefault(o),s=t(l[1]),i=(function(e){function t(){return babelHelpers.classCallCheck(this,t),babelHelpers.possibleConstructorReturn(this,(t.__proto__||Object.getPrototypeOf(t)).apply(this,arguments))}return babelHelpers.inherits(t,e),babelHelpers.createClass(t,[{key:"render",value:function(){return a.default.createElement(s.View,{style:c.container},a.default.createElement(s.Text,{style:c.welcome},"Welcome to React Native!"),a.default.createElement(s.Text,{style:c.instructions},"To get started, edit AppNext.js"),a.default.createElement(s.Text,{style:c.instructions},"Press Cmd+R to reload,\nCmd+D or shake for dev menu"))}}]),t})(o.Component);n.default=i;var c=s.StyleSheet.create({container:{flex:1,justifyContent:'center',alignItems:'center',backgroundColor:'#F5FCFF'},welcome:{fontSize:20,textAlign:'center',margin:10},instructions:{textAlign:'center',color:'#333333',marginBottom:5}})},1001,[12,17]);

注意列敲,第一個(gè)__d的模塊id替換為1000阱佛,第二個(gè)替換為1001。同時(shí)戴而,因?yàn)榈谝粋€(gè)dependence到了第二個(gè)凑术,所以在dependencyMap中將原來(lái)的307改為1001。
index2.jsbundle中所意,刪除其他行代碼淮逊,只保留上面兩條修改過(guò)的__d,保存為新的index2.jsbundle扶踊。

2.2.2 修改require
實(shí)際上運(yùn)行上面新的jsbundle泄鹏,仍然會(huì)報(bào)錯(cuò)。這是因?yàn)橐鎏矗覀冞€未修改入口require命满。報(bào)錯(cuò)信息可能跟AppRegistry相關(guān)。
在jsbundle的尾部绣版,是模塊的調(diào)用入口胶台。對(duì)比可以看到,common和index2的入口id都是11≡映椋現(xiàn)在index2中id11改為1000诈唬,所以,需要在index2中增加id為1000的入口缩麸。最終index2.jsbundle:

__d(function(e,r,t,n,p){var i=r(p[0]),u=(babelHelpers.interopRequireDefault(i),r(p[1])),l=r(p[2]),a=babelHelpers.interopRequireDefault(l);u.AppRegistry.registerComponent('App2',function(){return a.default})},1000,[12,17,1001]);
__d(function(e,t,r,n,l){Object.defineProperty(n,"__esModule",{value:!0});var o=t(l[0]),a=babelHelpers.interopRequireDefault(o),s=t(l[1]),i=(function(e){function t(){return babelHelpers.classCallCheck(this,t),babelHelpers.possibleConstructorReturn(this,(t.__proto__||Object.getPrototypeOf(t)).apply(this,arguments))}return babelHelpers.inherits(t,e),babelHelpers.createClass(t,[{key:"render",value:function(){return a.default.createElement(s.View,{style:c.container},a.default.createElement(s.Text,{style:c.welcome},"Welcome to React Native!"),a.default.createElement(s.Text,{style:c.instructions},"To get started, edit AppNext.js"),a.default.createElement(s.Text,{style:c.instructions},"Press Cmd+R to reload,\nCmd+D or shake for dev menu"))}}]),t})(o.Component);n.default=i;var c=s.StyleSheet.create({container:{flex:1,justifyContent:'center',alignItems:'center',backgroundColor:'#F5FCFF'},welcome:{fontSize:20,textAlign:'center',margin:10},instructions:{textAlign:'center',color:'#333333',marginBottom:5}})},1001,[12,17]);
require(1000);

再次運(yùn)行RN铸磅,發(fā)現(xiàn)可以實(shí)現(xiàn)分步加載common和business。

分布加載多個(gè)業(yè)務(wù)

按照上面的拆包方法,可以進(jìn)一步將多個(gè)不同business拆包阅仔。按需加載business.jsbundle吹散。native端加載對(duì)應(yīng)business.jsbundle后,按照對(duì)應(yīng)的moduleName來(lái)調(diào)用business八酒。
為進(jìn)一步提高加載效率空民,可以考慮bridge池緩存預(yù)加載好common.jsbundle的bridge,進(jìn)入具體的business羞迷,注入具體的business.jsbundle界轩。

本文介紹了RN中拆包以及分步加載jsbundle的思路。理解拆包衔瓮,重要點(diǎn)還是在jsbundle的構(gòu)成浊猾,以及RN與js的交互原理上。本文介紹的方案都很基礎(chǔ)热鞍,如需實(shí)際操作葫慎,還需要相應(yīng)的腳本協(xié)助,以及native端加載管理流程碍现。

參考資料:
攜程是如何做React Native優(yōu)化的
React_Native拆分bundle之patch拆包
基于拆分包的React Native在iOS端加載性能優(yōu)化
React Native按需加載 手Q狼人殺探索之路

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末幅疼,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子昼接,更是在濱河造成了極大的恐慌,老刑警劉巖悴晰,帶你破解...
    沈念sama閱讀 206,311評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件慢睡,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡铡溪,警方通過(guò)查閱死者的電腦和手機(jī)漂辐,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,339評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)棕硫,“玉大人髓涯,你說(shuō)我怎么就攤上這事」纾” “怎么了纬纪?”我有些...
    開(kāi)封第一講書(shū)人閱讀 152,671評(píng)論 0 342
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)滑肉。 經(jīng)常有香客問(wèn)我包各,道長(zhǎng),這世上最難降的妖魔是什么靶庙? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 55,252評(píng)論 1 279
  • 正文 為了忘掉前任问畅,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘护姆。我一直安慰自己矾端,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,253評(píng)論 5 371
  • 文/花漫 我一把揭開(kāi)白布卵皂。 她就那樣靜靜地躺著秩铆,像睡著了一般。 火紅的嫁衣襯著肌膚如雪渐裂。 梳的紋絲不亂的頭發(fā)上豺旬,一...
    開(kāi)封第一講書(shū)人閱讀 49,031評(píng)論 1 285
  • 那天,我揣著相機(jī)與錄音柒凉,去河邊找鬼族阅。 笑死,一個(gè)胖子當(dāng)著我的面吹牛膝捞,可吹牛的內(nèi)容都是我干的坦刀。 我是一名探鬼主播,決...
    沈念sama閱讀 38,340評(píng)論 3 399
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼蔬咬,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼鲤遥!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起林艘,我...
    開(kāi)封第一講書(shū)人閱讀 36,973評(píng)論 0 259
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤盖奈,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后狐援,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體钢坦,經(jīng)...
    沈念sama閱讀 43,466評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,937評(píng)論 2 323
  • 正文 我和宋清朗相戀三年啥酱,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了爹凹。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,039評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡镶殷,死狀恐怖禾酱,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情绘趋,我是刑警寧澤颤陶,帶...
    沈念sama閱讀 33,701評(píng)論 4 323
  • 正文 年R本政府宣布,位于F島的核電站埋心,受9級(jí)特大地震影響指郁,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜拷呆,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,254評(píng)論 3 307
  • 文/蒙蒙 一闲坎、第九天 我趴在偏房一處隱蔽的房頂上張望疫粥。 院中可真熱鬧,春花似錦腰懂、人聲如沸梗逮。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,259評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)慷彤。三九已至,卻和暖如春怖喻,著一層夾襖步出監(jiān)牢的瞬間底哗,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,485評(píng)論 1 262
  • 我被黑心中介騙來(lái)泰國(guó)打工锚沸, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留跋选,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 45,497評(píng)論 2 354
  • 正文 我出身青樓哗蜈,卻偏偏與公主長(zhǎng)得像前标,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子距潘,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,786評(píng)論 2 345

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