RN拆包解析

一. 拆包動機

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í)行

最終歸納出以下結構


image

polyfills : 預加載,最早執(zhí)行的一些function,聲明es語法新增的接口,定義模塊聲明方法等
module difinitations : 模塊聲明,以__d開頭,一般為每一個js文件或資源文件,將其封裝成一個module對象,并進行標號
require calls : bundle文件尾部指定入口文件,如如require(79),最后一行require(0);

ps:79可以找到是InitializeCore,這個加載了js-c++-java三層的通信注冊類,通信臨聽類等

三.拆包方案

其他方案對比

  1. moles-packer

簡介:攜程大廠推出,穩(wěn)定可靠,針對react native0.44時代的版本

優(yōu)點:重寫了react native自帶的打包工具,重寫就是為了分包,為分包而生的項目,肯定可靠

缺點:不持續(xù)維護更新,只適合rn老版本用戶了,0.5以上的rn版本全部撲街

  1. 自己修改打包代碼

簡介:現(xiàn)在很多教程都是讓你去修改打包的源碼,在里面判斷分包,58的0.44版本就是這個方案

優(yōu)點:如果很懂打包源碼,這個做法靈活,定制化強,100%沒問題

缺點:上手難,需要完全理解打包源碼,網(wǎng)上的教程比較古老

  1. 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 幫助我們完成了全部工作,解析加載的過程如下:


image

項目中,入口點文件(如 index.js)利用 import 依賴了其他組件。即組件間都是相互依賴的。

Resolution 代表 解析 的過程,負責梳理關聯(lián)js文件間的相互依賴關系。

Transformation 代表 轉換 的過程,負責將模塊文件轉換成平臺可理解的格式。

Serialization 代表 序列化 的過程,負責在完成轉換過程并將模塊轉換為可訪問的格式后,將其序列化。序列化程序將模塊組合在一起以生成一個或多個包。捆綁包實際上是一組模塊,組合成一個JavaScript文件。

更多關于配置的詳細信息可以查看(和諧翻墻):

(1) Configuring Metro

(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即可。

參考文檔

https://blog.csdn.net/u013718120/article/details/84571326

?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

友情鏈接更多精彩內容