本文涉及 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
部分的代码