中文字幕av专区_日韩电影在线播放_精品国产精品久久一区免费式_av在线免费观看网站

溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務條款》

React?Native中怎么實現動態導入

發布時間:2022-07-01 09:46:07 來源:億速云 閱讀:163 作者:iii 欄目:開發技術

這篇文章主要介紹“React Native中怎么實現動態導入”的相關知識,小編通過實際案例向大家展示操作過程,操作方法簡單快捷,實用性強,希望這篇“React Native中怎么實現動態導入”文章能幫助大家解決問題。

    背景

    隨著業務的發展,每一個 React Native 應用的代碼數量都在不斷增加,bundle 體積不斷膨脹,對應用性能的負面影響愈發明顯。雖然我們可以通過 React Native 官方工具 Metro 進行拆包處理,拆分為一個基礎包和一個業務包進行一定程度上的優化,但對日益增長的業務代碼也無能為力,我們迫切地需要一套方案來減小我們 React Native 應用的體積。

    多業務包

    第一個想到的就是拆分多業務包,既然拆分為一個業務包不夠,那我多拆為幾個業務包不就可以了。當一個 React Native 應用拆分為多個業務包之后其實就相當于拆分為多個應用了,只不過代碼在同一倉庫里。這雖然可以解決單個應用不斷膨脹的問題,但是有不少局限性。接下來一一分析:

    • 鏈接替換,不同的應用需要不同的地址,替換成本較高。

    • 頁面之間通信,之前是個單頁應用,不同頁面之間可以直接通信;拆分之后是不同應用相互通信需要借助客戶端橋接實現。

    • 性能損耗,打開每個拆分的業務包都需要單獨起一個 React Native 容器,容器初始化、維持都需要消耗內存、占用CPU。

    • 粒度不夠,最小的維度也是頁面,無法繼續對頁面中的組件進行拆分。

    • 重復打包,部分在不同頁面之間共享的工具庫,每個業務包都會包含。

    • 打包效率,每一個業務包的打包過程,都要經過一遍完整的 Metro 打包過程,拆分多個業務包打包時間成倍增加。

    動態導入

    作為一個前端想到的另一方案自然就是動態導入(Dynamic import)了,基于其動態特性對于多業務包的眾多缺點,此方案都可避免。此外擁有了動態導入我們就可以實現頁面按需加載,組件懶加載等等能力。但是 Metro 官方并不支持動態導入,因此需要對 Metro 進行深度定制,這也是本文即將介紹的在 React Native 中實現動態導入。

    Metro 打包原理

    在介紹具體方案之前我們先看下 Metro 的打包機制及其構建產物。

    打包過程

    如下圖所示Metro打包會經過三個階段,分別是 Resolution、Transformation、Serialization。

    React?Native中怎么實現動態導入

    image

    Resolution 的作用是從入口開始構建依賴圖;Transformation 是和 Resolution 階段同時執行的,其目的是將所有 module(一個模塊就是一個 module ) 轉換為目標平臺可識別語言,這里面既有高級 JavaCript 語法的轉換(依賴 BaBel),也有對特定平臺,比如安卓的特殊 polyfills。這兩個階段主要是生產中間產物 IR 為最后一階段所消費。

    Serialization 則是將所有 module 組合起來生成 bundle,這里需要特別注意 Metro API 文檔中 Serializer Options 中的兩個配置:

    • 簽名為 createModuleIdFactory, type 為 () => (path: string) => number。 這個函數為每個 module 生成一個唯一的 moduleId,默認情況下是自增的數字。所有的依賴關系都依仗此 moduleId。

    • 簽名為 processModuleFilter, type 為 (module: Array) => boolean。這個函數用來過濾模塊,決定是否打入 bundle。

    bundle 分析

    一個 React Native 典型的 bundle 從上到下可以分為三個部分:

    • 第一部分為 polyfills,主要是一些全局變量如 DEV;以及通過 IIFE 聲明的一些重要全局函數,如: __d、 __r 等;

    • 第二部分是各個 module 的定義,以 __d 開頭,業務代碼全部在這一塊;

    • 第三部分是應用的初始化 __r(react-native/Libraries/Core/InitializeCore.js moduleId) 和 __r(${入口 moduleId})
      我們看下具體函數的分析

    __d函數

    function define(factory, moduleId, dependencyMap) {
        const mod = {
            dependencyMap,
            factory,
            hasError: false,
            importedAll: EMPTY,
            importedDefault: EMPTY,
            isInitialized: false,
            publicModule: {
                exports: {}
            }
        };
        modules[moduleId] = mod;
    }

    __d 其實就是 define 函數,可以看到其實現很簡單,做的就是聲明一個 mode,同時 moduleId 與 mode 做了一層映射,這樣通過 moduleId 就可以拿到 module 實現。我們看下 __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。這里解釋下入參,第一個是個函數,就是 module 的工廠函數,所有的業務邏輯都在這里面,其是在 __r 之后調用的;第二個是 moduleId,模塊的唯一標識;第三部分是其依賴的模塊的 moduleId;第四個是此模塊的文件名稱。

    __r函數

    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 函數。如上精簡后的代碼所示,require 方法首先判斷所要加載的模塊是否已經存在并初始化完成,若是則直接返回模塊,否則調用 guardedLoadModule 方法,最終調用的是 loadModuleImplementation 方法。loadModuleImplementation 方法獲得模塊定義時傳入的 factory 方法并調用,最后返回。

    方案設計

    基于以上對 Metro 工作原理及其產物 bundle 的分析,我們可以大致得出這樣一個結論:React Native 啟動時,JS 測(即 bundle)會先初始化一些變量,接著通過 IIFE 聲明核心方法 define 和 require;接著通過 define 方法定義所有的模塊,各個模塊的依賴關系通moduleId 維系,維系的紐帶就是 require;最后通過 require 應用的注冊方法實現啟動。

    實現動態導入自然需要將目前的 bundle 進行重新拆分和組合,整個方案的關鍵點在于:分和合,分就是 bundle 如何拆分,什么樣的 module 需要拆分出去,什么時候進行拆分,拆分之后的 bundle 存儲在哪里(涉及到后續如何獲取);合就是拆出去的 bundle 如何獲取,并在獲取之后仍在正確的上下文內執行。

    前面有說過 Metro 工作的三個階段,其中之一就是 Resolution,這一階段的主要任務是從入口開始構建整個應用依賴圖,這里為了方便示意以樹來代替。

    React?Native中怎么實現動態導入

    image

    識別入口

    如上所示就是一個依賴樹,正常情況下會打出一個 bundle,包含模塊 A、B、C、D、E、F、G。現在我想對模塊 B 和 F 做動態導入。怎么做呢第一步當然是標識,既然叫動態導入自然而然的想到了 JavaScript 語法上的動態導入。

    只需要將 import A from '.A' 改成 const A = import('A') 即可,這就需要引入 Babel 插件()了,事實上官方 Metro 相關配置包 metro-config 已經集成了此插件。官方做的不僅僅于此,在 Transformation 階段還對采用動態導入的 module 增加了唯一標識 Async = true

    此外在最終產物 bundle 上 Metro 提供了一個名叫 AsyncRequire.js 的文件模版來做動態導入的語法的 polyfill,具體實現如下

    const dynamicRequire = require;
    
    module.exports = function(moduleID) {
        return Promise.resolve().then(() => dynamicRequire.importAll(moduleID));
    };

    總結一下 Metro 默認會如何處理動態導入:在 Transformation 通過 Babel 插件處理動態導入語法,并在中間產物上增加標識 Async,在 Serialization 階段用 Asyncrequire.js 作為模板替換動態導入的語法,即

    const A = import(A);
    
    //變為
    
    const A = function(moduleID) {
        return Promise.resolve().then(() => dynamicRequire.importAll(moduleID));
    };

    Asyncrequire.js 不僅關乎我們如何拆分,還和我們最后的合息息相關,留待后續再談。

    樹拆分

    通過上文我們知道構建過程中會生成一顆依賴樹,并對其中使用動態的導入的模塊做了標識,接下來就是樹如何進行拆分了。對于樹的通用處理辦法就是 DFS,通過對上圖依賴樹做 DFS 分析之后可以得到如下做了拆分的樹,包含一顆主樹和兩顆異步樹。對于每棵樹的依賴進行收集即可得到如下三組 module 集合:A、E、C;B、D、E、G;F、G。

    React?Native中怎么實現動態導入

    image

    當然在實際場景中,各個模塊的依賴遠比這個復雜,甚至存在循環依賴的情況,在做 DFS 的過程中需要遵循兩個原則:

    • 已經在處理過的 module,后續遇到直接退出循環

    • 各個異步樹依賴的非主樹 module 都需要包含進來

    bundle 生成

    通過這三組 module 集合即可得到三個bundle(我們將主樹生成的 bundle 稱為主 bundle;異步樹生成的稱為異步 bundle)。至于如何生成,直接借助前文提到的 Metro 中 processBasicModuleFilter 方法即可。Metro 原本在一次構建過程中,只會經過一次 Serialization 階段生成一個 bundle。現在我們需要對每一組 module 都進行一次 bundle 生成。

    這里需要注意幾個問題:

    • 去重,一種是已經打入主 bundle 的 module 異步 bundle 不需要打入;一種是同時存在于不同異步樹內的 module,對于這種 module,我們可以將其標記為動態導入單獨打包,見下圖

    React?Native中怎么實現動態導入

    image

    • 生成順序,需要先生成異步 bundle,再生成主 bundle。因為需要將異步 bundle 的信息(比如文件名稱、地址)與 moduleId 做映射填入主 bundle,這樣在真正需要的時候可以通過 moduleId 的映射拿到異步 bundle 的地址信息。

    • 緩存控制,為了保證每個異步 bundle 在能夠享受緩存機制的同時能夠及時更新,需要對異步 bundle 做 content hash 添加到文件名上

    • 存儲,異步 bundle 如何存儲,是和主 bundle 一起,還是單獨存儲,需要時再去獲取呢。這個需要具體分析:對于采用了bundle 預加載的可以將異步 bundle 和主 bundle 放到一起,需要時直接從本地拿即可(所謂預加載就是在客戶端啟動時就已經將所有 bundle 下載下來了,在用戶打開 React Native 頁面時無需再去下載 bundle)。對于大部分沒有采用預加載技術的則分開存儲更合適。

    至此我們已經獲得了主 bundle 和異步 bundle,大致結構如下:

    /* 主 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);
    
    //  業務模塊
    __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");
    
    ...
    
    // 應用啟動
    __r(91);
    __r(0);
    
    /* 異步 bundle */
    
    // 業務模塊
    __d(function (global, _$$_REQUIRE, _$$_IMPORT_DEFAULT, _$$_IMPORT_ALL, module, exports, _dependencyMap) {
      var _reactNative = _$$_REQUIRE(_dependencyMap[0], "react-native");
      ...
    },855,[956, 1126],"asyncModule.js");

    大部分工作其實在分這一階段已經做完了,接下來就是如何合了,前面有提到過動態導入的語法在生成的 bundle 中會被 AsyncRequire.js 中的模板所替代。仔細研究下其代碼發現其是用 Promise 包裹了一層 require(moduleId) 來實現。

    現在我們直接 require(moduleId) 必然是拿不到真正的 module 實現了,因為異步 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
        });
    };

    接下來一行行進行分析

    • 行1將之前 mock 的 Promise 替換為真正的 Promise 請求,先去獲取 bundle 資源,REMOTE_SOURCE_MAP 是在生成階段寫入主 bundle 的 moduleId 與異步 bundle 資源地址的映射。fetch 根據異步 bundle 的存儲方式的不同選擇不同的方式獲取真正的代碼資源;

    • 行2通過 Function 方法執行獲取到的代碼,即是模塊的聲明,這樣最后返回 module 的時候就已經是定義過的了;

    • 行3 返回真正的模塊實現。
      這樣我們就實現了合,異步 bundle 的獲取、執行就都在 AsyncRequire.js 內完成了。

    關于“React Native中怎么實現動態導入”的內容就介紹到這里了,感謝大家的閱讀。如果想了解更多行業相關的知識,可以關注億速云行業資訊頻道,小編每天都會為大家更新不同的知識點。

    向AI問一下細節

    免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。

    AI

    黄山市| 永丰县| 精河县| 渝北区| 沙雅县| 中西区| 石屏县| 华坪县| 来宾市| 沁阳市| 木兰县| 正定县| 嘉峪关市| 三亚市| 罗定市| 武义县| 巴南区| 宿迁市| 隆子县| 邛崃市| 永新县| 达孜县| 西青区| 行唐县| 辽宁省| 德清县| 滨州市| 武平县| 庆城县| 亳州市| 包头市| 五常市| 方山县| 丹凤县| 长子县| 略阳县| 德安县| 玉环县| 札达县| 长丰县| 张掖市|