简述 ReactNative Bundle

简述 ReactNative Bundle本文因项目实际问题而起,简要分析了 RN Bundle 的结构。 原本计划在完成『ReactNative源码解析——渲染机制详解』一文后,暂停 RN 相关的总结分享,谁料项目中通过RN分包同时加载两个业务 bundle 时出错了!索性对 RN Bundle 研究一番,遂总结出此…

本文因项目实际问题而起,简要分析了 RN Bundle 的结构。

本文同时发表于我的个人博客


原本计划在完成『ReactNative源码解析——渲染机制详解』一文后,暂停 RN 相关的总结分享,谁料项目中通过RN分包同时加载两个业务 bundle 时出错了!索性对 RN Bundle 研究一番,遂总结出此文。

Overview


使用 ReactNative 开发的业务,无论是通过内置还是动态下发的方式发布,都需要将业务 JavaScript 代码打包成 bundle。 JavaScript 作为一门动态脚本语言,为何需要打包这个过程? 打包主要有以下几个用途:

  • 开发 RN 业务时,一般会使用 JSX 语法『糖』描述 UI 视图,然而标准的 JS 引擎显然不支持 JSX,所以需要将 JSX 语法转换成标准的 JS 语法;
  • 开发 RN 业务时,通常使用的是 ES 6,目前 iOS、Android 上的 JS 引擎还不支持 ES 6,因此需要转换;
  • JS 业务代码会依赖多个不同的模块(JS 文件),RN 在打包时将所有依赖的模块打包到一个 bundle 文件中,较好地解决了这种复杂的依赖关系;
  • JS 代码的混淆。

RN 打包过程中的转码主要依赖 Babel 实现。

ReactNative Bundle


RN Bundle 从本质上讲是一个 JS 文件,其主要由三部分组成:polyfills、module 定义、require 调用。 简述 ReactNative Bundle

Polyfills

polyfills 部分主要是在 JS context 中做一些准备工作,如:声明 ES 6 语法中新增接口、定义模块方法(如:模块声明方法__d、模块引用方法require等)、设置global.__DEV__变量等。 简述 ReactNative Bundle 如上图,polyfills 都闭包方法,定义的同时被调用。 polyfill具体规则定义在node_modules/metro-bundler/src/defaults.js中:

exports.polyfills = [
require.resolve('./Resolver/polyfills/Object.es6.js'),
require.resolve('./Resolver/polyfills/console.js'),
require.resolve('./Resolver/polyfills/error-guard.js'),
require.resolve('./Resolver/polyfills/Number.es6.js'),
require.resolve('./Resolver/polyfills/String.prototype.es6.js'),
require.resolve('./Resolver/polyfills/Array.prototype.es6.js'),
require.resolve('./Resolver/polyfills/Array.es6.js'),
require.resolve('./Resolver/polyfills/Object.es7.js'),
require.resolve('./Resolver/polyfills/babelHelpers.js')];

defaults.js中还有其他有意思的信息^_^

module definitions

为了更直观的了解 RN Bundle 中模块的定义,我们先来看一个例子: 简述 ReactNative Bundle 如上图一个非常简单的 RN Demo,在打包生成的 bundle 中变成如下的格式: 简述 ReactNative Bundle 很明显,为了看懂上图所示的打包结果,必须先了解一下__d为何物,细心的同学,可能已经在 polyfills 小节中发现了__d的定义(第12行),即__d就是define方法,其完整的源码定义在:node_modules/metro-bundler/src/Resolver/polyfills/require.js中(代码略有删减):

global.require = require;
global.__d = define;

const modules = Object.create(null);

function define(factory, moduleId, dependencyMap) {
    if (moduleId in modules) {
        // that are already loaded
        return;
    }
    modules[moduleId] = {
        dependencyMap,
        exports: undefined,
        factory,
        hasError: false,
        isInitialized: false };
}

可以看到define方法前三个参数分别为:factory 方法、module ID以及dependencyMap。 调用define方法定义模块,实质就是以 moduleID 为 key 向模块注册表(modules)中注册模块相关的信息(exports、模块factory方法、isInitialized等)。 好了,下面我们再次回到 RN Bundle 中 module definition。

module ID

在 RN 中,为了唯一标识每个模块,解决模块间的依赖问题,在打包生成 bundle 时,为每个 module 生成一个唯一的 moduleID,moudleID 为从0开始递增的数字。 另外,RN 在打包 bundle 时,按模块间依赖关系深度遍历(弦外之音就是,根组件的 moduleID 为0)。

module factory

从上图可知,module factory 方法主要做了以下几件事:

  • 所有 import 转换为require方法调用(import是 ES 6新增语法,需要转换)(第5860行);
  • 创建组件类(第62~83行),其中最关键的方法就是render
  • 导出(exports)组件类(第85行);
  • 注册根组件(第87~89行),详细信息请参看ReactNative源码解析——渲染机制详解一文。

通过 module factory 中的 render方法,再次看到 JSX 标签被转换成了createElement方法调用。

require calls

RN Bundle 最后部分是 require calls:

;require(50);  // InitializeCore.js
;require(0);

require方法参数为 moduleID,RN Bundle 最后这两个require调用分别加载了InitializeCore以及RNDemo(根组件)模块。 下面我们来看看,require具体做了哪些事(与模块定义方法define定义在同一文件中):

function require(moduleId) {
    const module = modules[moduleId];
    return module && module.isInitialized ?
    module.exports :
    guardedLoadModule(moduleIdReallyIsNumber, module);
}

function guardedLoadModule(moduleId, module) {
    return loadModuleImplementation(moduleId, module);
}

function loadModuleImplementation(moduleId, module) {
    module.isInitialized = true;
    const exports = module.exports = {};
    var _module = module;
    const factory = _module.factory, dependencyMap = _module.dependencyMap;
    const moduleObject = { exports };
    factory(global, require, moduleObject, exports, dependencyMap);
    return module.exports = moduleObject.exports;
}

如上代码所示,require方法首先判断所要加载的模块是否已经存在并初始化完成。若是,则直接返回模块的exports,否则调用guardedLoadModule方法(最终调用的是loadModuleImplementation方法)。 loadModuleImplementation方法获得模块的factory方法并调用,最终返回模块的exports

小结

从上文的分析可知,加载一个 RN Bundle,主要完成三件事:

  • 准备 JS 执行环境(polyfills);
  • 定义所有需要的模块(module define);
  • 加载InitializeCore以及根组件模块(require)。

通过上文分析,应该能清楚的区分模块定义与加载的关系:

  • 模块定义(define):将模块相关的信息(其中最重要的就是factory方法)添加到模块注册表中,仅此而已;
  • 模块加载(require):在 JS context 中调用模块factory方法,创建模块类并在组件注册表中注册根组件。

以盖房子作比喻:

  • 模块定义:将盖房子需要的材料运入场地;
  • 模块加载:真正地将房子盖起来。

ps:上文所示的 RN Bundle 都是开发环境下打出来的(打包命令中--dev为 true),这样的 Bundle 是没有经过混淆的,其可读性较好。经过混淆的 Bundle,大概长这样:

!function(e){e.__DEV__=!1,e.__BUNDLE_START_TIME__=e.nativePerformanceNow?e.nativePerformanceNow():Date.now()}("undefined"!=typeof global?global:"undefined"!=typeof self?self:this);
!function(r){"use strict";function e(r,e,t){e in u||(u[e]={dependencyMap:t,exports:void 0,factory:r,hasError:!1,isInitialized:!1})}function t(r){var e=r,t=u[e];return t&&t.isInitialized?t.exports:i(e,t)}function i(e,t){if(!c&&r.ErrorUtils){c=!0;var i=void 0;try{i=n(e,t)}catch(e){r.ErrorUtils.reportFatalError(e)}return c=!1,i}return n(e,t)}function n(e,i){var n=r.nativeRequire;if(!i&&n&&(n(e),i=u[e]),!i)throw o(e);if(i.hasError)throw a(e,i.error);i.isInitialized=!0;var c=i.exports={},d=i,s=d.factory,f=d.dependencyMap;try{var l={exports:c};return s(r,t,l,c,f),i.factory=void 0,i.dependencyMap=void 0,i.exports=l.exports}catch(r){throw i.hasError=!0,i.error=r,i.isInitialized=!1,i.exports=void 0,r}}function o(r){var e='Requiring unknown module "'+r+'".';return Error(e)}function a(r,e){var t=r;return Error('Requiring module "'+t+'", which threw an exception: '+e)}r.require=t,r.__d=e;var u=Object.create(null),c=!1}("undefined"!=typeof global?global:"undefined"!=typeof self?self:this);
__d(function(e,t,r,l){Object.defineProperty(l,"__esModule",{value:!0});var n=t(12),s=babelHelpers.interopRequireDefault(n),a=t(24),o=function(e){function t(){return babelHelpers.classCallCheck(this,t),babelHelpers.possibleConstructorReturn(this,(t.__proto__||Object.getPrototypeOf(t)).apply(this,arguments))}return babelHelpers.inherits(t,e),babelHelpers.createClass(t,[{key:"render",value:function(){return s.default.createElement(a.View,{style:styles.container},s.default.createElement(a.Text,null,"This is a demo"))}}]),t}(s.default.Component);l.default=o,a.AppRegistry.registerComponent("RNDemo",function(){return o})},0);

问题


好了,本文既然是因问题而起,最后简要介绍一下遇到的问题: 简述 ReactNative Bundle 问题的描述很清楚是根组件没有注册,由于刚刚分析完 RN 的渲染机制,知道这个错误描述的出处(来自AppRegistry模块的runApplication方法): 简述 ReactNative Bundle 问题也很明显是在组件注册表runnables中找不到要运行的根组件。 起初我们怀疑是因为两个业务 bundle 有冲突在加载时出错了。 但 debug 下来并没有出错提示。 最终请教相关人士,得知是两个业务 bundle ID 一样,导致第二个 bundle 没有被正确定义(define方法首先通过要定义的模块 ID 判断该 ID 是否已注册,若已注册直接返回)。

两个业务 bundle ID 会一样与我们使用的拆包方案有关。

问题清楚了,修改就简单了,在此不细述了。

通过这个问题,也说明了对底层实现方案了解的必要性。了解的越深,在遇到问题时思考的视角就会越广。

今天的文章简述 ReactNative Bundle分享到此就结束了,感谢您的阅读。

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
如需转载请保留出处:http://bianchenghao.cn/13916.html

(0)
编程小号编程小号

相关推荐

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注