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í)行
__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è)思路谆趾,有幾種主流方案:
- diff and patch躁愿。將jsbundle通過(guò)diff,生成common和每個(gè)業(yè)務(wù)的patch包沪蓬,然后在APP運(yùn)行時(shí)對(duì)common和patch合并成執(zhí)行的jsbundle彤钟。
- 修改RN的bundle命令打包流程,使得直接生成common+business包
- 修改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ì)象的使用。
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做替換。例如:
// 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狼人殺探索之路