本文涉及 react-native及 metro 版本
-
react-native@0.63.2 -
metro@0.58.0
先来看一波本文的实例代码:很简单吧,一个你好,世界
1 | // App.js |
一、前言
众所周知,
react-native(下文简称rn)需要打成bundle包供android,ios加载;通常我们的打包命令为react-native bundle --entry-file index.js --bundle-output ./bundle/ios.bundle --platform ios --assets-dest ./bundle --dev false;运行上述命令之后,rn 会默认使用metro作为打包工具,生成bundle包。
生成的 bundle 包大致分为四层:
- var 声明层: 对当前运行环境, bundle 启动时间,以及进程相关信息;
- polyfill 层:
!(function(r){}), 定义了对define(__d)、require(__r)、clear(__c)的支持,以及 module(react-native 及第三方 dependences 依赖的 module) 的加载逻辑; - 模块定义层: __d 定义的代码块,包括 RN 框架源码 js 部分、自定义 js 代码部分、图片资源信息,供 require 引入使用
- require 层: r 定义的代码块,找到 d 定义的代码块 并执行
格式如下:
1 | // var声明层 |
看完上面的代码不知你是否疑问?
var定义层和polyfill的代码是在什么时机生成的?我们知道
_d()有三个参数,分别是对应factory函数,当前moduleId以及module依赖关系-
metro使用什么去做整个工程的依赖分析? -
moduleId如何生成?
-
metro如何打包?
日常开发中我们可能并么有在意,整个 rn 打包逻辑;现在就让笔者带您走入 rn 打包的世界!
二、metro 打包流程
通过翻阅源码和 Metro 官网,我们知道 metro 打包的整个流程大致分为:
命令参数解析metro 打包服务启动打包 js 和资源文件- 解析,转化和生成
停止打包服务

1. 命令参数解析
首先我们来看看 react-native bundle的实现以及参数如何解析;由于 bundle 是 react-native 的一个子命令,那么我们寻找的思路可以从 react-native 包入手;其文件路径如下
1 | // node_modules/react-native/local-cli/cli.js |
由于本文主要分析 react-native 打包流程,所以只需查看react-native/node_modules/@react-native-community/cli/build/commands/bundle/bundle.js即可。
在 bundle.js 文件中主要注册了 bundle 命令,但是具体的实现却使用了buildBundle.js.
1 | // node_modules/react-native/node_modules/@react-native-community/cli/build/commands/bundle/bundle.js |
2. Metro Server 启动
在node_modules/react-native/node_modules/@react-native-community/cli/build/commands/bundle/buildBundle.js文件中默认导出的 buildBundle 方法才是整个react-native bundle执行的入口。在入口中主要做了如下几件事情:
- 合并 metro 默认配置和自定义配置,并设置 maxWorkers,resetCache
- 根据解析得到参数,构建 requestOptions,传递给打包函数
- 实例化 metro Server
- 启动 metro 构建 bundle
- 处理资源文件,解析
- 关闭 Metro Server
1 | // node_modules/react-native/node_modules/@react-native-community/cli/build/commands/bundle/buildBundle.js |
从上述代码可以看到具体的打包实现都在output.build(server, requestOpts)中,output是outputBundle类型,这部分代码在 Metro JS` 中,具体的路径为:node_modules/metro/src/shared/output/bundle.js
1 | // node_modules/metro/src/shared/output/bundle.js |
可以看到虽说使用的output.build(server, requestOpts)进行打包,其实是使用传入的packagerClient.build进行打包。而packagerClient是我们刚传入的Server。而Server就是下面我们要分析打包流程。其源码位置为:node_modules/metro/src/Server.js
metro 构建 bundle: 流程入口
通过上面的分析,我们已经知晓整个react-native bundle 打包服务的启动在node_modules/metro/src/Server.js的build方法中:
1 | class Server { |
在这个 build 函数中,首先执行了 buildGraph,而 this._bundler 的初始化发生在 Server 的 constructor 中。
1 | this._bundler = new IncrementalBundler(config, { |
此处的_bundler是 IncrementalBundler 的实例,它的 buildGraph 函数完成了打包过程中前两步 Resolution 和 Transformation 。 下面我们就来详细查看一下 Metro 解析,转换过程。
metro 构建 bundle: 解析和转换
在上面一节我们知道 metro 使用IncrementalBundler进行 js 代码的解析和转换,在 Metro 使用IncrementalBundler进行解析转换的主要作用是:
- 返回了以入口文件为入口的所有相关依赖文件的依赖图谱和 babel 转换后的代码;
- 返回了var 定义部分及 polyfill 部分所有相关依赖文件的依赖图谱和 babel 转换后的代码;
整体流程如图所示:

通过上述的流程我们总结如下几点:
- 整个 metro 进行依赖分析和 babel 转换主要通过了JestHasteMap 去做依赖分析;
- 在做依赖分析的通过,metro 会监听当前目录的文件变化,然后以最小变化生成最终依赖关系图谱;
- 不管是入口文件解析还是 polyfill 文件的依赖解析都是使用了JestHasteMap ;
下面,我们来分析其具体过程如下:
1 | // node_modules/metro/src/IncrementalBundler.js |
require 和模块定义部分解析和依赖生成
在 buildGraphForEntries中利用_deltaBundler.buildGraph生成 graph,
1 | // node_modules/metro/src/IncrementalBundler.js |
经过DependencyGraph.load和DeltaCalculator之后,生成的依赖图谱格式如下:
1 | { |
var 及 polyfill 部分解析
前面看到在IncrementalBundler.js的 buildGraph中通过getPrependedScripts获取到var 和 polyfill部分的代码;下面我们一些查看一下getPrependedScripts:
1 | // node_modules/metro/src/lib/getPreludeCode.js |
此处还有一个部分作者没有详细进行讲述,那就是使用JestHasteMap 进行文件依赖解析详细部分;后续笔者会单独出一篇文章进行讲解,关于查阅。
至此,metro 对入口文件及 polyfills 依赖分析及代码生成以及讲述完毕,回过头再看一下此章节的开头部分,不知您是否已豁然开朗。讲述了 Metro 的解析和转换,下面部分将讲述 Metro 如果通过转换后的文件依赖图谱生成最终的 bundle 代码。
metro 构建 bundle: 生成
回到最开始的 Server 服务启动代码部分,我们发现经过buildGraph之后得到了prepend: var及polyfill部分的代码和依赖关系以及graph: 入口文件的依赖关系及代码;在没有提供自定义生成的情况下 metro 使用了baseJSBundle将依赖关系图谱和每个模块的代码经过一系列的操作最终使用 bundleToString 转换成最终的代码。
1 | // metro打包核心:解析(Resolution)和转换(Transformation) |
在关注baseJSBundle之前,我们先来回顾一下,graph 和 prepend 的数据结构:其主要包括如下几个信息:
- 文件相关的依赖关系
- 指定 module 经过 babel 之后的代码
1 | // graph |
baseJSBundle
下面我们我们重点关注一下baseJSBundle是如何处理上述的数据结构的:
-
baseJSBundle整体调用了三次processModules分别用于解析出:preCode,postCode和modules其对应的分别是var 和 polyfills 部分的代码 , require 部分的代码 , _d 部分的代码 -
processModules经过两次filter过滤出所有类型为js/类型的数据,第二次过滤使用用户自定义filter函数;过滤完成之后使用wrapModule转换成_d(factory,moduleId,dependencies)的代码 - baseJSBundle
1 | // node_modules/metro/src/DeltaBundler/Serializers/baseJSBundle.js |
- processModules
processModules 经过两次 filter 过滤出所有类型为 js/类型的数据,第二次过滤使用用户自定义 filter 函数;过滤完成之后使用 wrapModule 转换成_d(factory,moduleId,dependencies)的代码
1 | // node_modules/metro/src/DeltaBundler/Serializers/helpers/processModules.js |
- getAppendScripts
上面讲到 getAppendScripts 主要作用是: 获取入口文件及所有的 runBeforeMainModule 文件的依赖图谱和 使用 getRunModuleStatement 方法生成_r(moduleId)的代码
1 | function getAppendScripts(entryPoint, modules, importBundleNames, options) { |
至此 baseJSBundle我们已经分析完成。
bundleToString
经过前面一个步骤bundleToBundle我们分别获取到了: preCode , postCode 和 modules 其对应的分别是var 和 polyfills 部分的代码 , require 部分的代码 , _d 部分的代码 而 bundleToString的作用如下:
- 先将 var 及 polyfill 部分的代码使用\n 进行字符串拼接;
- 然后将
_d部分的代码使用moduleId进行升序排列并使用字符串拼接的方式构造_d部分的代码; - 最后合如
_r部分的代码
1 | function bundleToString(bundle) { |
总结
- react-native 使用 metro 打包之后的 bundle 大致分为四层
bundle 包大致分为四层:
- var 声明层: 对当前运行环境, bundle 启动时间,以及进程相关信息;
- poyfill 层:
!(function(r){}), 定义了对define(__d)、require(__r)、clear(__c)的支持,以及 module(react-native 及第三方 dependences 依赖的 module) 的加载逻辑; - 模块定义层:
__d定义的代码块,包括 RN 框架源码 js 部分、自定义 js 代码部分、图片资源信息,供 require 引入使用 - require 层: r 定义的代码块,找到 d 定义的代码块 并执行
-
react-native使用metro进行打包主要分为三个步骤: 解析,转化和生成;
- 解析和转化部分: Metro Server 使用
IncrementalBundler进行 js 代码的解析和转换
在 Metro 使用IncrementalBundler进行解析转换的主要作用是:
- 返回了以入口文件为入口的所有相关依赖文件的依赖图谱和 babel 转换后的代码;
- 返回了var 定义部分及 polyfill 部分所有相关依赖文件的依赖图谱和 babel 转换后的代码;
整体流程如图所示:

通过上述的流程我们总结如下几点:
- 整个 metro 进行依赖分析和 babel 转换主要通过了JestHasteMap 去做依赖分析;
- 在做依赖分析的通过,metro 会监听当前目录的文件变化,然后以最小变化生成最终依赖关系图谱;
- 不管是入口文件解析还是 polyfill 文件的依赖解析都是使用了JestHasteMap ;
生成的对应依赖关系图谱格式如下:
1 | // graph |
- metro 代码生成部分使用
baseJSBundle得到代码,并使用baseToString拼接最终Bundle代码
在 baseJSBundle 中:
-
baseJSBundle整体调用了三次processModules分别用于解析出:preCode,postCode和modules其对应的分别是var 和 polyfills 部分的代码 , require 部分的代码 ,_d部分的代码 -
processModules经过两次filter过滤出所有类型为js/类型的数据,第二次过滤使用用户自定义filter函数;过滤完成之后使用wrapModule转换成_d(factory,moduleId,dependencies)的代码
在baseToString中:
- 先将 var 及 polyfill 部分的代码使用\n 进行字符串拼接;
- 然后将
_d部分的代码使用moduleId进行升序排列并使用字符串拼接的方式构造_d部分的代码; - 最后合如
_r部分的代码