網(wǎng)站首頁 編程語言 正文
背景
隨著業(yè)務(wù)的發(fā)展,每一個 React Native 應(yīng)用的代碼數(shù)量都在不斷增加,bundle 體積不斷膨脹,對應(yīng)用性能的負(fù)面影響愈發(fā)明顯。雖然我們可以通過 React Native 官方工具 Metro 進(jìn)行拆包處理,拆分為一個基礎(chǔ)包和一個業(yè)務(wù)包進(jìn)行一定程度上的優(yōu)化,但對日益增長的業(yè)務(wù)代碼也無能為力,我們迫切地需要一套方案來減小我們 React Native 應(yīng)用的體積。
多業(yè)務(wù)包
第一個想到的就是拆分多業(yè)務(wù)包,既然拆分為一個業(yè)務(wù)包不夠,那我多拆為幾個業(yè)務(wù)包不就可以了。當(dāng)一個 React Native 應(yīng)用拆分為多個業(yè)務(wù)包之后其實就相當(dāng)于拆分為多個應(yīng)用了,只不過代碼在同一倉庫里。這雖然可以解決單個應(yīng)用不斷膨脹的問題,但是有不少局限性。接下來一一分析:
- 鏈接替換,不同的應(yīng)用需要不同的地址,替換成本較高。
- 頁面之間通信,之前是個單頁應(yīng)用,不同頁面之間可以直接通信;拆分之后是不同應(yīng)用相互通信需要借助客戶端橋接實現(xiàn)。
- 性能損耗,打開每個拆分的業(yè)務(wù)包都需要單獨起一個 React Native 容器,容器初始化、維持都需要消耗內(nèi)存、占用CPU。
- 粒度不夠,最小的維度也是頁面,無法繼續(xù)對頁面中的組件進(jìn)行拆分。
- 重復(fù)打包,部分在不同頁面之間共享的工具庫,每個業(yè)務(wù)包都會包含。
- 打包效率,每一個業(yè)務(wù)包的打包過程,都要經(jīng)過一遍完整的 Metro 打包過程,拆分多個業(yè)務(wù)包打包時間成倍增加。
動態(tài)導(dǎo)入
作為一個前端想到的另一方案自然就是動態(tài)導(dǎo)入(Dynamic import)了,基于其動態(tài)特性對于多業(yè)務(wù)包的眾多缺點,此方案都可避免。此外擁有了動態(tài)導(dǎo)入我們就可以實現(xiàn)頁面按需加載,組件懶加載等等能力。但是 Metro 官方并不支持動態(tài)導(dǎo)入,因此需要對 Metro 進(jìn)行深度定制,這也是本文即將介紹的在 React Native 中實現(xiàn)動態(tài)導(dǎo)入。
Metro 打包原理
在介紹具體方案之前我們先看下 Metro 的打包機(jī)制及其構(gòu)建產(chǎn)物。
打包過程
如下圖所示Metro打包會經(jīng)過三個階段,分別是 Resolution、Transformation、Serialization。
image
Resolution 的作用是從入口開始構(gòu)建依賴圖;Transformation 是和 Resolution 階段同時執(zhí)行的,其目的是將所有 module(一個模塊就是一個 module ) 轉(zhuǎn)換為目標(biāo)平臺可識別語言,這里面既有高級 JavaCript 語法的轉(zhuǎn)換(依賴 BaBel),也有對特定平臺,比如安卓的特殊 polyfills。這兩個階段主要是生產(chǎn)中間產(chǎn)物 IR 為最后一階段所消費。
Serialization 則是將所有 module 組合起來生成 bundle,這里需要特別注意 Metro API 文檔中 Serializer Options 中的兩個配置:
- 簽名為?
createModuleIdFactory
, type 為?() => (path: string) => number
。 這個函數(shù)為每個 module 生成一個唯一的 moduleId,默認(rèn)情況下是自增的數(shù)字。所有的依賴關(guān)系都依仗此 moduleId。 - 簽名為?
processModuleFilter
, type 為?(module: Array) => boolean
。這個函數(shù)用來過濾模塊,決定是否打入 bundle。
bundle 分析
一個 React Native 典型的 bundle 從上到下可以分為三個部分:
- 第一部分為 polyfills,主要是一些全局變量如?
DEV
;以及通過 IIFE 聲明的一些重要全局函數(shù),如:?__d
、?__r
?等; - 第二部分是各個 module 的定義,以?
__d
?開頭,業(yè)務(wù)代碼全部在這一塊; - 第三部分是應(yīng)用的初始化?
__r(react-native/Libraries/Core/InitializeCore.js moduleId)
?和?__r(${入口 moduleId})
。
我們看下具體函數(shù)的分析
__d函數(shù)
function define(factory, moduleId, dependencyMap) { const mod = { dependencyMap, factory, hasError: false, importedAll: EMPTY, importedDefault: EMPTY, isInitialized: false, publicModule: { exports: {} } }; modules[moduleId] = mod; }
__d
?其實就是?define
?函數(shù),可以看到其實現(xiàn)很簡單,做的就是聲明一個?mode
,同時?moduleId
?與?mode
?做了一層映射,這樣通過?moduleId
?就可以拿到 module 實現(xiàn)。我們看下?__d
?如何使用:
__d(function (global, _$$_REQUIRE, _$$_IMPORT_DEFAULT, _$$_IMPORT_ALL, module, exports, _dependencyMap) { var _reactNative = _$$_REQUIRE(_dependencyMap[0], "react-native"); var _reactNavigation = _$$_REQUIRE(_dependencyMap[1], "react-navigation"); var _reactNavigationStack = _$$_REQUIRE(_dependencyMap[2], "react-navigation-stack"); var _routes = _$$_REQUIRE(_dependencyMap[3], "./src/routes"); var _appJson = _$$_REQUIRE(_dependencyMap[4], "./appJson.json"); var AppNavigator = (0, _reactNavigationStack.createStackNavigator)(_routes.RouteConfig, (0, _routes.InitConfig)()); var AppContiner = (0, _reactNavigation.createAppContainer)(AppNavigator); _reactNative.AppRegistry.registerComponent(_appJson.name, function () { return AppContiner; }); }, 0, [1, 552, 636, 664, 698], "index.android.js");
這是?__d
?的唯一用處,定義一個 module。這里解釋下入?yún)ⅲ谝粋€是個函數(shù),就是 module 的工廠函數(shù),所有的業(yè)務(wù)邏輯都在這里面,其是在?__r
?之后調(diào)用的;第二個是 moduleId,模塊的唯一標(biāo)識;第三部分是其依賴的模塊的 moduleId;第四個是此模塊的文件名稱。
__r函數(shù)
function metroRequire(moduleId) { ... const moduleIdReallyIsNumber = moduleId; const module = modules[moduleIdReallyIsNumber]; return module && module.isInitialized ? module.publicModule.exports : guardedLoadModule(moduleIdReallyIsNumber, module); } function guardedLoadModule(moduleId, module) { ... return loadModuleImplementation(moduleId, module); } function loadModuleImplementation(moduleId, module) { ... const moduleObject = module.publicModule; moduleObject.id = moduleId; factory( global, metroRequire, metroImportDefault, metroImportAll, moduleObject, moduleObject.exports, dependencyMap ); return moduleObject.exports; ... }
__r
?其實就是?require
?函數(shù)。如上精簡后的代碼所示,require
?方法首先判斷所要加載的模塊是否已經(jīng)存在并初始化完成,若是則直接返回模塊,否則調(diào)用?guardedLoadModule
?方法,最終調(diào)用的是?loadModuleImplementation
?方法。loadModuleImplementation
?方法獲得模塊定義時傳入的?factory
?方法并調(diào)用,最后返回。
方案設(shè)計
基于以上對 Metro 工作原理及其產(chǎn)物 bundle 的分析,我們可以大致得出這樣一個結(jié)論:React Native 啟動時,JS 測(即 bundle)會先初始化一些變量,接著通過 IIFE 聲明核心方法?define
?和?require
;接著通過?define
?方法定義所有的模塊,各個模塊的依賴關(guān)系通moduleId
?維系,維系的紐帶就是?require
;最后通過?require
?應(yīng)用的注冊方法實現(xiàn)啟動。
實現(xiàn)動態(tài)導(dǎo)入自然需要將目前的 bundle 進(jìn)行重新拆分和組合,整個方案的關(guān)鍵點在于:分和合,分就是 bundle 如何拆分,什么樣的 module 需要拆分出去,什么時候進(jìn)行拆分,拆分之后的 bundle 存儲在哪里(涉及到后續(xù)如何獲取);合就是拆出去的 bundle 如何獲取,并在獲取之后仍在正確的上下文內(nèi)執(zhí)行。
分
前面有說過 Metro 工作的三個階段,其中之一就是 Resolution,這一階段的主要任務(wù)是從入口開始構(gòu)建整個應(yīng)用依賴圖,這里為了方便示意以樹來代替。
image
識別入口
如上所示就是一個依賴樹,正常情況下會打出一個 bundle,包含模塊 A、B、C、D、E、F、G。現(xiàn)在我想對模塊 B 和 F 做動態(tài)導(dǎo)入。怎么做呢第一步當(dāng)然是標(biāo)識,既然叫動態(tài)導(dǎo)入自然而然的想到了 JavaScript 語法上的動態(tài)導(dǎo)入。
只需要將?import A from '.A'
?改成?const A = import('A')
?即可,這就需要引入 Babel 插件()了,事實上官方 Metro 相關(guān)配置包 metro-config 已經(jīng)集成了此插件。官方做的不僅僅于此,在 Transformation 階段還對采用動態(tài)導(dǎo)入的 module 增加了唯一標(biāo)識?Async = true
。
此外在最終產(chǎn)物 bundle 上 Metro 提供了一個名叫 AsyncRequire.js 的文件模版來做動態(tài)導(dǎo)入的語法的 polyfill,具體實現(xiàn)如下
const dynamicRequire = require; module.exports = function(moduleID) { return Promise.resolve().then(() => dynamicRequire.importAll(moduleID)); };
總結(jié)一下 Metro 默認(rèn)會如何處理動態(tài)導(dǎo)入:在 Transformation 通過 Babel 插件處理動態(tài)導(dǎo)入語法,并在中間產(chǎn)物上增加標(biāo)識?Async
,在 Serialization 階段用 Asyncrequire.js 作為模板替換動態(tài)導(dǎo)入的語法,即
const A = import(A); //變?yōu)? const A = function(moduleID) { return Promise.resolve().then(() => dynamicRequire.importAll(moduleID)); };
Asyncrequire.js 不僅關(guān)乎我們?nèi)绾尾鸱郑€和我們最后的合息息相關(guān),留待后續(xù)再談。
樹拆分
通過上文我們知道構(gòu)建過程中會生成一顆依賴樹,并對其中使用動態(tài)的導(dǎo)入的模塊做了標(biāo)識,接下來就是樹如何進(jìn)行拆分了。對于樹的通用處理辦法就是 DFS,通過對上圖依賴樹做 DFS 分析之后可以得到如下做了拆分的樹,包含一顆主樹和兩顆異步樹。對于每棵樹的依賴進(jìn)行收集即可得到如下三組 module 集合:A、E、C;B、D、E、G;F、G。
image
當(dāng)然在實際場景中,各個模塊的依賴遠(yuǎn)比這個復(fù)雜,甚至存在循環(huán)依賴的情況,在做 DFS 的過程中需要遵循兩個原則:
- 已經(jīng)在處理過的 module,后續(xù)遇到直接退出循環(huán)
- 各個異步樹依賴的非主樹 module 都需要包含進(jìn)來
bundle 生成
通過這三組 module 集合即可得到三個bundle(我們將主樹生成的 bundle 稱為主 bundle;異步樹生成的稱為異步 bundle)。至于如何生成,直接借助前文提到的 Metro 中 processBasicModuleFilter 方法即可。Metro 原本在一次構(gòu)建過程中,只會經(jīng)過一次 Serialization 階段生成一個 bundle。現(xiàn)在我們需要對每一組 module 都進(jìn)行一次 bundle 生成。
這里需要注意幾個問題:
- 去重,一種是已經(jīng)打入主 bundle 的 module 異步 bundle 不需要打入;一種是同時存在于不同異步樹內(nèi)的 module,對于這種 module,我們可以將其標(biāo)記為動態(tài)導(dǎo)入單獨打包,見下圖
image
- 生成順序,需要先生成異步 bundle,再生成主 bundle。因為需要將異步 bundle 的信息(比如文件名稱、地址)與 moduleId 做映射填入主 bundle,這樣在真正需要的時候可以通過 moduleId 的映射拿到異步 bundle 的地址信息。
- 緩存控制,為了保證每個異步 bundle 在能夠享受緩存機(jī)制的同時能夠及時更新,需要對異步 bundle 做 content hash 添加到文件名上
- 存儲,異步 bundle 如何存儲,是和主 bundle 一起,還是單獨存儲,需要時再去獲取呢。這個需要具體分析:對于采用了bundle 預(yù)加載的可以將異步 bundle 和主 bundle 放到一起,需要時直接從本地拿即可(所謂預(yù)加載就是在客戶端啟動時就已經(jīng)將所有 bundle 下載下來了,在用戶打開 React Native 頁面時無需再去下載 bundle)。對于大部分沒有采用預(yù)加載技術(shù)的則分開存儲更合適。
至此我們已經(jīng)獲得了主 bundle 和異步 bundle,大致結(jié)構(gòu)如下:
/* 主 bundle */ // moduleId 與 路徑映射 var REMOTE_SOURCE_MAP = {${id}: ${path}, ... } // IIFE __r 之類定義 (function (global) { "use strict"; global.__r = metroRequire; global.__d = define; global.__c = clear; global.__registerSegment = registerSegment; ... })(typeof global !== 'undefined' ? global : typeof window !== 'undefined' ? window : this); // 業(yè)務(wù)模塊 __d(function (global, _$$_REQUIRE, _$$_IMPORT_DEFAULT, _$$_IMPORT_ALL, module, exports, _dependencyMap) { var _reactNative = _$$_REQUIRE(_dependencyMap[0], "react-native"); var _asyncModule = _$$_REQUIRE(_dependencyMap[4], "metro/src/lib/bundle-modules/asyncRequire")(_dependencyMap[5], "./asyncModule") ... },0,[1,550,590,673,701,855],"index.ios.js"); ... // 應(yīng)用啟動 __r(91); __r(0); /* 異步 bundle */ // 業(yè)務(wù)模塊 __d(function (global, _$$_REQUIRE, _$$_IMPORT_DEFAULT, _$$_IMPORT_ALL, module, exports, _dependencyMap) { var _reactNative = _$$_REQUIRE(_dependencyMap[0], "react-native"); ... },855,[956, 1126],"asyncModule.js");
合
大部分工作其實在分這一階段已經(jīng)做完了,接下來就是如何合了,前面有提到過動態(tài)導(dǎo)入的語法在生成的 bundle 中會被 AsyncRequire.js 中的模板所替代。仔細(xì)研究下其代碼發(fā)現(xiàn)其是用?Promise
?包裹了一層?require(moduleId)
?來實現(xiàn)。
現(xiàn)在我們直接?require(moduleId)
?必然是拿不到真正的 module 實現(xiàn)了,因為異步 bundle 還沒有獲取到,module 還沒有定義。但可以對 AsyncRequire.js 做如下改造
const dynamicRequire = require; module.exports = function (moduleID) { return fetch(REMOTE_SOURCE_MAP[moduleID]).then(res => { // 行1 new Function(res)(); // 行2 return dynamicRequire.importAll(moduleID) // 行3 }); };
接下來一行行進(jìn)行分析
- 行1將之前 mock 的 Promise 替換為真正的 Promise 請求,先去獲取 bundle 資源,
REMOTE_SOURCE_MAP
?是在生成階段寫入主 bundle 的 moduleId 與異步 bundle 資源地址的映射。fetch
?根據(jù)異步 bundle 的存儲方式的不同選擇不同的方式獲取真正的代碼資源; - 行2通過 Function 方法執(zhí)行獲取到的代碼,即是模塊的聲明,這樣最后返回 module 的時候就已經(jīng)是定義過的了;
- 行3 返回真正的模塊實現(xiàn)。
這樣我們就實現(xiàn)了合,異步 bundle 的獲取、執(zhí)行就都在 AsyncRequire.js 內(nèi)完成了。
總結(jié)
至此我們就完成了 React Native 動態(tài)導(dǎo)入的改造。相對于多業(yè)務(wù)包,因為其動態(tài)特性使得業(yè)務(wù)方使用的時候所有修改都在同一個 React Native 應(yīng)用內(nèi)部閉環(huán)完成,外部無感知,多業(yè)務(wù)包的眾多缺陷也就不存在了。與此同時構(gòu)建時會充分利用第一次的生產(chǎn)的 IR,這樣每一個 bundle 不需要再單獨走 Metro 的完整構(gòu)建流程。
當(dāng)然有一點是必須需要考慮的,那就是我們對 Metro 進(jìn)行改造之后,對于后續(xù)的升級是否有影響,導(dǎo)致只能鎖定 React Native 和 Metro 版本。這個其實完全不用擔(dān)心,從前面的分析可以知道,我們對于整個流程的改造可以分為兩部分:構(gòu)建時、運行時。在構(gòu)建時我們確實新增了不少能力,比如新的分組算法、代碼生成;但是運行時則是完全基于現(xiàn)有版本能力的增強(qiáng)。這就使得動態(tài)導(dǎo)入的運行時無兼容性問題,即使升級到新版本依然不會報錯,只不過再我們再次改造構(gòu)建時之前失去了動態(tài)導(dǎo)入的能力。
最后真正在生產(chǎn)環(huán)境上使用還有一些工程上的改造,比如:構(gòu)建平臺適配、提供快速接入組件等等限于篇幅就不在此詳述了。
原文鏈接:https://zhuanlan.zhihu.com/p/535867372
相關(guān)推薦
- 2022-07-25 C#爬蟲基礎(chǔ)之HttpClient獲取HTTP請求與響應(yīng)_C#教程
- 2022-07-11 MongoDB分片方式及片鍵選擇
- 2022-06-22 C++深入探究類與對象之友元與運算符重載_C 語言
- 2024-07-15 golang使用migrate遷移pg數(shù)據(jù)庫表報錯處理
- 2022-06-18 基于Python實現(xiàn)實時監(jiān)控CPU使用率_python
- 2022-07-21 安裝MiniConda和Pytorch以及Cuda的筆記及心得
- 2022-05-06 Linq中ToList()和CopyToDataTable()用法詳解_實用技巧
- 2023-04-24 numpy.reshape()的函數(shù)的具體使用_python
- 最近更新
-
- window11 系統(tǒng)安裝 yarn
- 超詳細(xì)win安裝深度學(xué)習(xí)環(huán)境2025年最新版(
- Linux 中運行的top命令 怎么退出?
- MySQL 中decimal 的用法? 存儲小
- get 、set 、toString 方法的使
- @Resource和 @Autowired注解
- Java基礎(chǔ)操作-- 運算符,流程控制 Flo
- 1. Int 和Integer 的區(qū)別,Jav
- spring @retryable不生效的一種
- Spring Security之認(rèn)證信息的處理
- Spring Security之認(rèn)證過濾器
- Spring Security概述快速入門
- Spring Security之配置體系
- 【SpringBoot】SpringCache
- Spring Security之基于方法配置權(quán)
- redisson分布式鎖中waittime的設(shè)
- maven:解決release錯誤:Artif
- restTemplate使用總結(jié)
- Spring Security之安全異常處理
- MybatisPlus優(yōu)雅實現(xiàn)加密?
- Spring ioc容器與Bean的生命周期。
- 【探索SpringCloud】服務(wù)發(fā)現(xiàn)-Nac
- Spring Security之基于HttpR
- Redis 底層數(shù)據(jù)結(jié)構(gòu)-簡單動態(tài)字符串(SD
- arthas操作spring被代理目標(biāo)對象命令
- Spring中的單例模式應(yīng)用詳解
- 聊聊消息隊列,發(fā)送消息的4種方式
- bootspring第三方資源配置管理
- GIT同步修改后的遠(yuǎn)程分支