一. 拆包動機
RN作為非常優(yōu)秀的移動端跨平臺開發(fā)框架,在近幾年得到眾多開發(fā)者的認可。國內各大廠采用在當前原生應用內集成RN的方式,使得App應用的靈活性得到了很大的提升。在原生應用內嵌入RN,就是需要在原生應用內加載RN模塊(1個或多個JSBundle),并得以顯示。JSBundle中包含了當前RN模塊的js代碼。如果存在多個RN模塊需要被加載時,就需要分別打出多個JSBundle,并且多個JSBundle包含了很多重復的代碼(例如:第三方依賴)。拆包的方式,就是將其中重復不變的代碼打成基礎包,動態(tài)變化的打成業(yè)務包。那么就做到了JSBundle的拆分。JSBundle的拆分,對降低內存的占用,減少加載時間,減少熱更新時流量帶寬等,在優(yōu)化方面起到了非常大的作用。
二.bundle簡要分析
1.bundle命令
- entry-file:即入口文件,打包時以該文件作為入口,一步步進行模塊分析處理。
- platform:用于區(qū)分打包什么平臺的 bundle
- dev:用于區(qū)分 bundle 使用環(huán)境,非 dev 時,會對代碼進行 minified
- bundle-output:打包產(chǎn)物輸出地址,即打包好的 bundle 存放地址
- sourcemap-output:打包時生成對應的 sourcemap 文件存放地址,在跟蹤查找錯誤或崩潰時,能幫助開發(fā)快速定位到代碼
- assets-dest:bundle 中使用的靜態(tài)資源文件存放地址
1.結構分析
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);
以最基礎的RN項目的 bundle 為例,可以看到 bundle 文件中大致定義了四個模塊:
(1)var 聲明的變量,對當前運行環(huán)境的定義,bundle 的啟動時間、Process進程環(huán)境相關信息
(2)(function() { })() 閉包中定義的代碼塊,其中定義了對 define(__d)、 require(__r)、clear(__c) 的支持,以及 module(react-native及第三方dependences依賴的module) 的加載邏輯
(3)__d 定義的代碼塊,包括RN框架源碼 js 部分、自定義js代碼部分、圖片資源信息,供 require 引入使用
(4)__r 定義的代碼塊,找到 __d 定義的代碼塊 并執(zhí)行
最終歸納出以下結構

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,然后再打個只包含了基礎引用(react和第三方module)的基礎包,比對一下patch,得出業(yè)務包,這樣基礎包和業(yè)務包都有了
優(yōu)點:簡單暴力,如果只是想簡單做下分包的可以嘗試下
缺點:1、不利于維護,由于module后面都是rn生成數(shù)字,依賴變了數(shù)字也變,導致基礎包變了所有包都需要變2、圖片沒法分包,有的第三方庫是有圖片的,這個方法只處理jsbundle不處理圖片
Metro
在執(zhí)行 react-native bundle | unbundle 命令時,RN框架背后其實是依賴了 Metro-Bundler 來完成打包、加載任務。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.
可擴展:在單個應用程序中使用數(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 字段
還可以通過在調用 CLI 時指定 --config <path / to / config> 來為配置提供自定義文件。
Metro中的常見配置結構如下所示:
module.exports = {
resolver: {
/* resolver options */
},
transformer: {
/* transformer options */
},
serializer: {
/* serializer options */
},
server: {
/* server options */
}
/* general options */
};
在打包過程中,Metro-Bundler 幫助我們完成了全部工作,解析加載的過程如下:
項目中,入口點文件(如 index.js)利用 import 依賴了其他組件。即組件間都是相互依賴的。
Resolution 代表 解析 的過程,負責梳理關聯(lián)js文件間的相互依賴關系。
Transformation 代表 轉換 的過程,負責將模塊文件轉換成平臺可理解的格式。
Serialization 代表 序列化 的過程,負責在完成轉換過程并將模塊轉換為可訪問的格式后,將其序列化。序列化程序將模塊組合在一起以生成一個或多個包。捆綁包實際上是一組模塊,組合成一個JavaScript文件。
更多關于配置的詳細信息可以查看(和諧翻墻):
(2)Role of Metro Bundler in React native
核心修改項
拆包的核心思想就是將基礎包和業(yè)務包拆分。那么我們只需要使用如下兩個配置項即可:
createModuleIdFactory
用于生成 require 語句的模塊ID,配置 createModuleIdFactory 讓其每次打包的 module 使用固定的id(路徑相關)。
參數(shù)是要打包的 module 文件的絕對路徑,返回的是打包后的 module 的 id
processModuleFilter
起到過濾功能,用于從輸出中丟棄特定模塊。配置 processModuleFilter 過濾基礎包,打出對應業(yè)務包。
參數(shù)是 Module 信息,返回值是 boolean 類型 ,如果是 false 就過濾掉不進行打包
Metro Config 配置文件
在打包過程中,我們需要依賴 createModuleIdFactory 、processModuleFilter 來幫助我們將JSBundle拆分為基礎包和業(yè)務模塊包。拆分的過程就需要我們通過配置 config 文件來完成。接下來我們來看看如何編寫 config 配置文件。
在編寫 config 配置文件之前,先來想個問題,為什么要固定基礎包中的模塊ID( __r(id) )呢?
在上面我們貼出的bundle文件中,可以看到最底部有兩段代碼:
__r(79);
__r(0);
不同文件打出的 bundle,最底部都為__r(0); 而上面的會隨著順序依次增加,例如以 index.js 文件打出的 bundle id 為 79,以 CustomComponent.js 打出的為 80。
基礎包(common.bundle)
在打基礎包的時候,我們會把RN的基礎文件以及第三方的依賴打進去。當我們在打業(yè)務包的時候,可能會做修改,例如導入組件的順序發(fā)生變化,或者依賴版本做了更新等等。都有可能導致ID發(fā)生變化,造成基礎包中不能找到對應的模塊ID,導致基礎包失效。所以需要將ID固定。一種簡單的方式就是以模塊名稱作為 require 即可。所以配置 createModuleIdFactory 讓其每次打包的 module 使用固定的模塊名稱即可。
業(yè)務包 (bussiness.bundle)
在打業(yè)務包時,需要結合 createModuleIdFactory、processModuleFilter 同時進行。createModuleIdFactory負責固定 module 的ID。processModuleFilter 負責過濾掉基礎包的內容模塊。
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即可。