简述 ReactNative Bundle

5,339 阅读6分钟

本文因项目实际问题而起,简要分析了 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 调用。

Polyfills

polyfills 部分主要是在 JS context 中做一些准备工作,如:声明 ES 6 语法中新增接口、定义模块方法(如:模块声明方法__d、模块引用方法require等)、设置global.__DEV__变量等。 如上图,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 中模块的定义,我们先来看一个例子: 如上图一个非常简单的 RN Demo,在打包生成的 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);

问题


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

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

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

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