乐趣区

关于前端:React-Native-中实现动态导入

图片起源:https://unsplash.com/photos/9…

本文作者:ssskkk

背景

随着业务的倒退,每一个 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。

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<Module>) => 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,同时 moduleIdmode 做了一层映射,这样通过 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 申明外围办法 definerequire;接着通过 define 办法定义所有的模块,各个模块的依赖关系通moduleId 维系,维系的纽带就是 require;最初通过 require 利用的注册办法实现启动。

实现动静导入天然须要将目前的 bundle 进行从新拆分和组合,整个计划的关键点在于:分和合,分就是 bundle 如何拆分,什么样的 module 须要拆分进来,什么时候进行拆分,拆分之后的 bundle 存储在哪里(波及到后续如何获取);合就是拆出去的 bundle 如何获取,并在获取之后仍在正确的上下文内执行。

后面有说过 Metro 工作的三个阶段,其中之一就是 Resolution,这一阶段的次要工作是从入口开始构建整个利用依赖图,这里为了不便示意以树来代替。

辨认入口

如上所示就是一个依赖树,失常状况下会打出一个 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。

当然在理论场景中,各个模块的依赖远比这个简单,甚至存在循环依赖的状况,在做 DFS 的过程中须要遵循两个准则:

  • 曾经在解决过的 module,后续遇到间接退出循环
  • 各个异步树依赖的非主树 module 都须要蕴含进来

    bundle 生成

    通过这三组 module 汇合即可失去三个 bundle(咱们将主树生成的 bundle 称为主 bundle;异步树生成的称为异步 bundle)。至于如何生成,间接借助前文提到的 Metro 中 processBasicModuleFilter 办法即可。Metro 本来在一次构建过程中,只会通过一次 Serialization 阶段生成一个 bundle。当初咱们须要对每一组 module 都进行一次 bundle 生成。

这里须要留神几个问题:

  • 去重,一种是曾经打入主 bundle 的 module 异步 bundle 不须要打入;一种是同时存在于不同异步树内的 module,对于这种 module,咱们能够将其标记为动静导入独自打包,见下图
  • 生成程序,须要学生成异步 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 动静导入的革新。绝对于多业务包,因为其动静个性使得业务方应用的时候所有批改都在同一个 React Native 利用外部闭环实现,内部无感知,多业务包的泛滥缺点也就不存在了。与此同时构建时会充分利用第一次的生产的 IR,这样每一个 bundle 不须要再独自走 Metro 的残缺构建流程。

当然有一点是必须须要思考的,那就是咱们对 Metro 进行革新之后,对于后续的降级是否有影响,导致只能锁定 React Native 和 Metro 版本。这个其实齐全不必放心,从后面的剖析能够晓得,咱们对于整个流程的革新能够分为两局部:构建时、运行时。在构建时咱们的确新增了不少能力,比方新的分组算法、代码生成;然而运行时则是齐全基于现有版本能力的加强。这就使得动静导入的运行时无兼容性问题,即便降级到新版本仍然不会报错,只不过再咱们再次革新构建时之前失去了动静导入的能力。

最初真正在生产环境上应用还有一些工程上的革新,比方:构建平台适配、提供疾速接入组件等等限于篇幅就不在此详述了。

本文公布自网易云音乐技术团队,文章未经受权禁止任何模式的转载。咱们长年招收各类技术岗位,如果你筹备换工作,又恰好喜爱云音乐,那就退出咱们 grp.music-fe(at)corp.netease.com!

退出移动版