一. 拆包動機
RN作為非常優(yōu)秀的移動端跨平臺開發(fā)框架,在近幾年得到眾多開發(fā)者的認可击罪。國內(nèi)各大廠采用在當前原生應(yīng)用內(nèi)集成RN的方式芋簿,使得App應(yīng)用的靈活性得到了很大的提升。在原生應(yīng)用內(nèi)嵌入RN,就是需要在原生應(yīng)用內(nèi)加載RN模塊(1個或多個JSBundle)骏全,并得以顯示苍柏。JSBundle中包含了當前RN模塊的js代碼。如果存在多個RN模塊需要被加載時姜贡,就需要分別打出多個JSBundle试吁,并且多個JSBundle包含了很多重復的代碼(例如:第三方依賴)。拆包的方式楼咳,就是將其中重復不變的代碼打成基礎(chǔ)包熄捍,動態(tài)變化的打成業(yè)務(wù)包。那么就做到了JSBundle的拆分母怜。JSBundle的拆分余耽,對降低內(nèi)存的占用,減少加載時間苹熏,減少熱更新時流量帶寬等碟贾,在優(yōu)化方面起到了非常大的作用。
二.bundle簡要分析
1.bundle命令
- entry-file:即入口文件轨域,打包時以該文件作為入口袱耽,一步步進行模塊分析處理。
- platform:用于區(qū)分打包什么平臺的 bundle
- dev:用于區(qū)分 bundle 使用環(huán)境疙挺,非 dev 時扛邑,會對代碼進行 minified
- bundle-output:打包產(chǎn)物輸出地址,即打包好的 bundle 存放地址
- sourcemap-output:打包時生成對應(yīng)的 sourcemap 文件存放地址铐然,在跟蹤查找錯誤或崩潰時蔬崩,能幫助開發(fā)快速定位到代碼
- assets-dest:bundle 中使用的靜態(tài)資源文件存放地址
1.結(jié)構(gòu)分析
var
__DEV__ = false,
__BUNDLE_START_TIME__ = this.nativePerformanceNow ? nativePerformanceNow() : Date.now(),
process = this.process || {};
process.env=process.env || {};
process.env.NODE_ENV = "production";
!(function(r) {
"use strict";
r.__r = o,
r.__d = function(r,i,n) {
if(null != e[i])
return;
e[i] = {
dependencyMap:n,factory:r,hasError:!1,importedAll:t,importedDefault:t,isInitialized:!1,publicModule:{exports:{}}
}
},
r.__c = n;
.... 代碼省略
})();
__d(function(g,r,i,a,m,e,d){var n=r(d[0]),t=r(d[1]),o=n(r(d[2])),u=r(d[3]);t.AppRegistry.registerComponent(u.name,function(){return o.default})},0,[1,2,328,330]);
....省略其他 __d 代碼
__d(function(g,r,i,a,m,e,d){m.exports=function(t){if(t&&t.__esModule)return t;var o={};if(null!=t)for(var n in t)if(Object.prototype.hasOwnProperty.call(t,n)){var c=Object.defineProperty&&Object.getOwnPropertyDescriptor?Object.getOwnPropertyDescriptor(t,n):{};c.get||c.set?Object.defineProperty(o,n,c):o[n]=t[n]}return o.default=t,o}},329,[]);
__d(function(e,s,t,a,n,N,d){n.exports={name:"RNTest",displayName:"RNTest"}},330,[]);
__r(79);
__r(0);
以最基礎(chǔ)的RN項目的 bundle 為例,可以看到 bundle 文件中大致定義了四個模塊:
(1)var 聲明的變量搀暑,對當前運行環(huán)境的定義沥阳,bundle 的啟動時間、Process進程環(huán)境相關(guān)信息
(2)(function() { })() 閉包中定義的代碼塊自点,其中定義了對 define(__d)桐罕、 require(__r)、clear(__c) 的支持桂敛,以及 module(react-native及第三方dependences依賴的module) 的加載邏輯
(3)__d 定義的代碼塊功炮,包括RN框架源碼 js 部分、自定義js代碼部分术唬、圖片資源信息薪伏,供 require 引入使用
(4)__r 定義的代碼塊,找到 __d 定義的代碼塊 并執(zhí)行
最終歸納出以下結(jié)構(gòu)
polyfills : 預加載粗仓,最早執(zhí)行的一些function嫁怀,聲明es語法新增的接口设捐,定義模塊聲明方法等
module difinitations : 模塊聲明,以__d開頭塘淑,一般為每一個js文件或資源文件萝招,將其封裝成一個module對象,并進行標號
require calls : bundle文件尾部指定入口文件存捺,如如require(79)槐沼,最后一行require(0);
ps:79可以找到是InitializeCore,這個加載了js-c++-java三層的通信注冊類捌治,通信臨聽類等
三.拆包方案
其他方案對比
- moles-packer
簡介:攜程大廠推出母赵,穩(wěn)定可靠,針對react native0.44時代的版本
優(yōu)點:重寫了react native自帶的打包工具具滴,重寫就是為了分包凹嘲,為分包而生的項目,肯定可靠
缺點:不持續(xù)維護更新构韵,只適合rn老版本用戶了周蹭,0.5以上的rn版本全部撲街
- 自己修改打包代碼
簡介:現(xiàn)在很多教程都是讓你去修改打包的源碼,在里面判斷分包疲恢,58的0.44版本就是這個方案
優(yōu)點:如果很懂打包源碼,這個做法靈活显拳,定制化強,100%沒問題
缺點:上手難杂数,需要完全理解打包源碼,網(wǎng)上的教程比較古老
- diff patch
簡介:大致的做法就是先打個正常的完整的jsbundle揍移,然后再打個只包含了基礎(chǔ)引用(react和第三方module)的基礎(chǔ)包,比對一下patch那伐,得出業(yè)務(wù)包,這樣基礎(chǔ)包和業(yè)務(wù)包都有了
優(yōu)點:簡單暴力罕邀,如果只是想簡單做下分包的可以嘗試下
缺點:1、不利于維護诉探,由于module后面都是rn生成數(shù)字,依賴變了數(shù)字也變阵具,導致基礎(chǔ)包變了所有包都需要變2、圖片沒法分包阳液,有的第三方庫是有圖片的,這個方法只處理jsbundle不處理圖片
Metro
在執(zhí)行 react-native bundle | unbundle 命令時帘皿,RN框架背后其實是依賴了 Metro-Bundler 來完成打包、加載任務(wù)鹰溜。Metro 作為一個獨立的打包工具,官方文檔 對于它的定義如下:
The JavaScript bundler for React Native.
Fast:Metro aims for sub-second reload cycles, fast startup and quick bundling speeds.
快:Metro旨在實現(xiàn)亞秒級重載循環(huán)曹动,快速啟動和快速捆綁速度。
Scalable:Works with thousands of modules in a single application.
可擴展:在單個應(yīng)用程序中使用數(shù)千個模塊墓陈。
Integrated:Supports every React Native project out of the box.
集成:支持開箱即用的每個React Native項目。
Metro 的高度可擴展性贡必,為我們提供了自由配置的打包方式。我們可以根據(jù)實際的需要來控制打包過程中的一些需求仔拟。官方為我們提供了很多種可配置的方式,可以使用以下三種方式創(chuàng)建Metro配置(按優(yōu)先級排序):
metro.config.js
metro.config.json
package.json中的 metro 字段
還可以通過在調(diào)用 CLI 時指定 --config <path / to / config> 來為配置提供自定義文件利花。
Metro中的常見配置結(jié)構(gòu)如下所示:
module.exports = {
resolver: {
/* resolver options */
},
transformer: {
/* transformer options */
},
serializer: {
/* serializer options */
},
server: {
/* server options */
}
/* general options */
};
在打包過程中科侈,Metro-Bundler 幫助我們完成了全部工作,解析加載的過程如下:
項目中炒事,入口點文件(如 index.js)利用 import 依賴了其他組件兑徘。即組件間都是相互依賴的。
Resolution 代表 解析 的過程羡洛,負責梳理關(guān)聯(lián)js文件間的相互依賴關(guān)系挂脑。
Transformation 代表 轉(zhuǎn)換 的過程,負責將模塊文件轉(zhuǎn)換成平臺可理解的格式欲侮。
Serialization 代表 序列化 的過程崭闲,負責在完成轉(zhuǎn)換過程并將模塊轉(zhuǎn)換為可訪問的格式后,將其序列化威蕉。序列化程序?qū)⒛K組合在一起以生成一個或多個包刁俭。捆綁包實際上是一組模塊,組合成一個JavaScript文件韧涨。
更多關(guān)于配置的詳細信息可以查看(和諧翻墻):
(2)Role of Metro Bundler in React native
核心修改項
拆包的核心思想就是將基礎(chǔ)包和業(yè)務(wù)包拆分牍戚。那么我們只需要使用如下兩個配置項即可:
createModuleIdFactory
用于生成 require 語句的模塊ID侮繁,配置 createModuleIdFactory 讓其每次打包的 module 使用固定的id(路徑相關(guān))。
參數(shù)是要打包的 module 文件的絕對路徑如孝,返回的是打包后的 module 的 id
processModuleFilter
起到過濾功能宪哩,用于從輸出中丟棄特定模塊。配置 processModuleFilter 過濾基礎(chǔ)包第晰,打出對應(yīng)業(yè)務(wù)包锁孟。
參數(shù)是 Module 信息,返回值是 boolean 類型 茁瘦,如果是 false 就過濾掉不進行打包
Metro Config 配置文件
在打包過程中品抽,我們需要依賴 createModuleIdFactory 、processModuleFilter 來幫助我們將JSBundle拆分為基礎(chǔ)包和業(yè)務(wù)模塊包甜熔。拆分的過程就需要我們通過配置 config 文件來完成圆恤。接下來我們來看看如何編寫 config 配置文件。
在編寫 config 配置文件之前腔稀,先來想個問題哑了,為什么要固定基礎(chǔ)包中的模塊ID( __r(id) )呢?
在上面我們貼出的bundle文件中烧颖,可以看到最底部有兩段代碼:
__r(79);
__r(0);
不同文件打出的 bundle弱左,最底部都為__r(0); 而上面的會隨著順序依次增加,例如以 index.js 文件打出的 bundle id 為 79炕淮,以 CustomComponent.js 打出的為 80拆火。
基礎(chǔ)包(common.bundle)
在打基礎(chǔ)包的時候,我們會把RN的基礎(chǔ)文件以及第三方的依賴打進去涂圆。當我們在打業(yè)務(wù)包的時候们镜,可能會做修改模狭,例如導入組件的順序發(fā)生變化嚼鹉,或者依賴版本做了更新等等驱富。都有可能導致ID發(fā)生變化,造成基礎(chǔ)包中不能找到對應(yīng)的模塊ID线脚,導致基礎(chǔ)包失效姊舵。所以需要將ID固定寓落。一種簡單的方式就是以模塊名稱作為 require 即可零如。所以配置 createModuleIdFactory 讓其每次打包的 module 使用固定的模塊名稱即可考蕾。
業(yè)務(wù)包 (bussiness.bundle)
在打業(yè)務(wù)包時肖卧,需要結(jié)合 createModuleIdFactory塞帐、processModuleFilter 同時進行葵姥。createModuleIdFactory負責固定 module 的ID句携。processModuleFilter 負責過濾掉基礎(chǔ)包的內(nèi)容模塊矮嫉。
createModuleIdFactory 源代碼
//node_modules/metro/src/lib/createModuleIdFactory.js
"use strict";
function createModuleIdFactory() {
const fileToIdMap = new Map();
let nextId = 0;
return path => {
let id = fileToIdMap.get(path);
if (typeof id !== "number") {
id = nextId++;
fileToIdMap.set(path, id);
}
return id;
};
}
module.exports = createModuleIdFactory;
我們知道蠢笋,createModuleIdFactory 用于生成 require 語句的模塊ID,從上述源碼也可以看出瞻惋,系統(tǒng)使用整數(shù)型的方式熟史,從0開始遍歷所有模塊窄俏,并依次使 Id 增加 1凹蜈。所以我們可以修改此處邏輯,以模塊路徑名稱的方式作為Id即可计雌。