Metro
众所周知,react-native
需要打包成bundle
包供Android
和iOS加
载,rn默认会使用metro
作为打包工具,生成bundle
包。 metro
是一个针对 React Native
的JavaScript
模块打包器,他接收一个entry file
(入口文件) 和一些配置作为参数,返回给你一个
单独的JavaScript
文件,这个文件包含了你写的所有的JavaScript
代码和所有的依赖。
也就是说Metro
把你写的几十上百个js
文件和几百个node_modules
的依赖,打包成了一个文件。
Metro配置
我们在运行npm install
安装react-native
依赖的时候其实已经默认安装了Metro
只不过可能并不是最新版的,这个跟react-native
的版本有关。
metro 配置有三种方法,分别为metro.config.js
、metro.config.json
和package.json
中添加metro
字段,react-naive
默认会在根目录有一个metro.config.js
如下:
/**
* Metro configuration for React Native
* https://github.com/facebook/react-native
*
* @format
*/
module.exports = {
resolver: {
/* resolver options */
},
transformer: {
getTransformOptions: async () => ({
transform: {
experimentalImportSupport: false,
inlineRequires: true,
},
}),
},
serializer: {
/* serializer options */
},
server: {
/* server options */
}
};
metro打包bundle文件
- 在项目工程中新建一个bundle文件
- 在终端执行打包的命令
react-native bundle --entry-file index.js --bundle-output ./bundle/ios.bundle --platform ios --assets-dest ./bundle --dev false
-
platform: 对应的平台, android/ios
-
entry-file: 入口文件
-
bundle-output: 生成的bundle文件输出的位置
-
asset-dest: 图片等资源文件输出的位置
-
sourcemap-output:sourcemap映射文件输出位置
-
config: 额外配置,拆包有用到
...
具体的打包命令可以参考 React-Native 离线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 定义的代码块 并执行
// var声明层
var __BUNDLE_START_TIME__=this.nativePerformanceNow?nativePerformanceNow():Date.now(),__DEV__=false,process=this.process||{},__METRO_GLOBAL_PREFIX__='';process.env=process.env||{};process.env.NODE_ENV=process.env.NODE_ENV||"production";
// polyfill层
!(function(r){"use strict";r.__r=o,r.__d=function(r,i,n){if(null!=e[i])return;var o={dependencyMap:n,factory:r,hasError:!1,importedAll:t,importedDefault:t,isInitialized:!1,publicModule:{exports:{}}};e[i]=o}
...
// 模型定义层
__d(function(g,r,i,a,m,e,d){var n=r(d[0]),t=r(d[1])(r(d[2]));n.AppRegistry.registerComponent(r(d[3]).name,function(){return t.default})},0,[1,3,401,413]);
...
// require层
__r(45);
__r(0);
metro 打包流程
通过metro官网的介绍,metro打包的流程大致可以分为:
- 命令参数解析
- metro打包服务启动
- 打包js盒资源文件(解析、转化、生产)
- 停止打包服务
-
命令参数解析
react-native bundle
是react-native的一个子命令,从react-native/cli.js
,@react-native-community/cli/build/commands
层层深入,我们可以发现node_modules/@react-native-community/cli-plugin-metro/build/commands/bundle/bundle.js中注册了bundle命令,具体实现是在buildBundle.js
中。var _buildBundle = _interopRequireDefault(require("./buildBundle")); var _bundleCommandLineArgs = _interopRequireDefault(require("./bundleCommandLineArgs")); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } function bundleWithOutput(_, config, args, output) { // bundle 打包的具体实现 return (0, _buildBundle.default)(args, config, output); } var _default = { name: 'bundle', description: 'builds the javascript bundle for offline use', func: bundleWithOutput, options: _bundleCommandLineArgs.default, // Used by `ramBundle.js` withOutput: bundleWithOutput }; exports.default = _default; const withOutput = bundleWithOutput; exports.withOutput = withOutput;
-
Metro Server 启动
在 node_modules/@react-native-community/cli-plugin-metro/build/commands/bundle/buildBundle.js中默认导出的
buildBundle
方法才是整个 react-native bundle 执行的入口。主要做了几件事:- 合并metro默认配置和自定义配置。
- 根据解析得到参数,构建 requestoption, 传递给打包函数
- 实例化metro Server
- 启动metro 构建 bundle
- 处理资源文件,解析
- 关闭 metro server
// node_modules/@react-native-community/cli-plugin-metro/build/commands/bundle/buildBundle.js // metro 打包服务,核心模块 function _Server() { const data = _interopRequireDefault(require("metro/src/Server")); _Server = function () { return data; }; return data; } ... // 保存资源文件 var _saveAssets = _interopRequireDefault(require("./saveAssets")); // metro的默认配置 var _loadMetroConfig = _interopRequireDefault(require("../../tools/loadMetroConfig")); ... // 打包的实现模块 const outputBundle = require('metro/src/shared/output/bundle'); async function buildBundle(args, ctx, output = outputBundle) { // 合并 metro 默认配置和自定义配,并设置maxWorkers resetCache const config = await (0, _loadMetroConfig.default)(ctx, { maxWorkers: args.maxWorkers, resetCache: args.resetCache, config: args.config }); return buildBundleWithConfig(args, config, output); } async function buildBundleWithConfig(args, config, output = outputBundle) { ... process.env.NODE_ENV = args.dev ? 'development' : 'production'; // 根据命令行的入参 --sourcemap-output 构建 sourceMapUrl let sourceMapUrl = args.sourcemapOutput; if (sourceMapUrl && !args.sourcemapUseAbsolutePath) { sourceMapUrl = _path().default.basename(sourceMapUrl); } // 根据解析得到参数,构建requestOptions,传递给打包函数 const requestOpts = { entryFile: args.entryFile, sourceMapUrl, dev: args.dev, minify: args.minify !== undefined ? args.minify : !args.dev, platform: args.platform, unstable_transformProfile: args.unstableTransformProfile }; // 实例化metro 服务 const server = new (_Server().default)(config); try { // 启动打包 const bundle = await output.build(server, requestOpts); // 将打包生成的bundle保存到对应的目录 await output.save(bundle, args, _cliTools().logger.info); // Save the assets of the bundle // 处理资源文件,解析,并在下一步保存在--assets-dest指定的位置 const outputAssets = await server.getAssets({ ..._Server().default.DEFAULT_BUNDLE_OPTIONS, ...requestOpts, bundleType: 'todo' }); // When we're done saving bundle output and the assets, we're done. // 保存资源文件到指定目录 return await (0, _saveAssets.default)(outputAssets, args.platform, args.assetsDest); } finally { server.end(); } } var _default = buildBundle; exports.default = _default;
从
buildBandule.js
中可以看到具体的打包实现是output.build(server, requestOpts)
中,具体代码为:// node_modules/metro/src/shared/output/bundle.js ... function buildBundle(packagerClient, requestOptions) { return packagerClient.build({ ...Server.DEFAULT_BUNDLE_OPTIONS, ...requestOptions, bundleType: "bundle", }); } ... exports.build = buildBundle; exports.save = saveBundleAndMap; exports.formatName = "bundle";
其实使用的是packagerClient.build,也就是刚刚传入的 server模块。
metro构建bundle: 流程入口
通过之前的分析,我们已经知晓整个
react-native bundle
打包服务的启动在node_modules/metro/src/Server.js
中的build
方法中。// node_modules/metro/src/Server.js class Server { // 构造函数,初始化属性 constructor(config, options) { this._config = config; this._serverOptions = options; if (this._config.resetCache) { this._config.cacheStores.forEach((store) => store.clear()); this._config.reporter.update({ type: "transform_cache_reset", }); } this._reporter = config.reporter; this._logger = Logger; this._platforms = new Set(this._config.resolver.platforms); this._isEnded = false; this._createModuleId = config.serializer.createModuleIdFactory(); this._bundler = new IncrementalBundler(config, { hasReducedPerformance: options && options.hasReducedPerformance, watch: options ? options.watch : undefined, }); this._nextBundleBuildID = 1; } ... async build(options) { // 将传递进来的参数进行格式化,按照模块拆分 const { entryFile, graphOptions, onProgress, serializerOptions, transformOptions, } = splitBundleOptions(options); // metro打包核心:解析(Resolution)和转换(Transformation) const { prepend, graph } = await this._bundler.buildGraph( entryFile, transformOptions, { onProgress, shallow: graphOptions.shallow, } ); // 获取构建入口文件路径 const entryPoint = this._getEntryPointAbsolutePath(entryFile); // 初始化构建参数,此处的参数来源于: 命令行 && 自定义metro配置metro.config.js && 默认的metro配置 const bundleOptions = { asyncRequireModulePath: await this._resolveRelativePath( this._config.transformer.asyncRequireModulePath, { transformOptions, relativeTo: "project", } ), processModuleFilter: this._config.serializer.processModuleFilter, // 默认的createModuleIdFactory给每个module生成id; 其默认生成规则详情请见: node_modules/metro/src/lib/createModuleIdFactory.js createModuleId: this._createModuleId, // 给方法签名 默认值为 getRunModuleStatement: moduleId => `__r(${JSON.stringify(moduleId)});`, // 详情请见: node_modules/metro-config/src/defaults/index.js getRunModuleStatement: this._config.serializer.getRunModuleStatement, dev: transformOptions.dev, projectRoot: this._config.projectRoot, modulesOnly: serializerOptions.modulesOnly, // 指定在主模块前运行的模块, 默认值: getModulesRunBeforeMainModule: () => [] // 详情请见: node_modules/metro-config/src/defaults/index.js runBeforeMainModule: this._config.serializer.getModulesRunBeforeMainModule( path.relative(this._config.projectRoot, entryPoint) ), runModule: serializerOptions.runModule, sourceMapUrl: serializerOptions.sourceMapUrl, sourceUrl: serializerOptions.sourceUrl, inlineSourceMap: serializerOptions.inlineSourceMap, }; let bundleCode = null; let bundleMap = null; // 是否使用自定义生成,如果是,则调用自定义生成的函数,获取最终代码 if (this._config.serializer.customSerializer) { const bundle = await this._config.serializer.customSerializer( entryPoint, prepend, graph, bundleOptions ); if (typeof bundle === "string") { bundleCode = bundle; } else { bundleCode = bundle.code; bundleMap = bundle.map; } } else { // 这边转化成两个步骤 // 将解析及转化之后的数据,生成如下格式化的数据 // { // pre: string, // var定义部分及poyfill部分的代码 // post: string, // require部分代码 // modules: [[number, string]], // 模块定义部分,第一个参数为number,第二个参数为具体的代码 // } const base = baseJSBundle(entryPoint, prepend, graph, bundleOptions) // 将js module进行排序并进行字符串拼接生成最终的代码 bundleCode = bundleToString(base).code; // bundleCode = bundleToString( // baseJSBundle(entryPoint, prepend, graph, bundleOptions) // ).code; } if (!bundleMap) { bundleMap = sourceMapString( [...prepend, ...this._getSortedModules(graph)], { excludeSource: serializerOptions.excludeSource, processModuleFilter: this._config.serializer.processModuleFilter, } ); } return { code: bundleCode, map: bundleMap, }; } ... }
在build函数中,首先执行了
buildGraph
而this_bundler
初始化是在Server的constructor
中IncrementalBunder
实例。它的buildGraph
函数完成了打包过程中前两步Resolution(解析)
和Transformation(转换)
Metro 构建bundle:解析和转换
metro中的
IncrementalBundler
进行解析和转化的主要作用:- 返回了以入口文件为入口的所有相关依赖文件的依赖图谱和bable转化后的代码;
- 返回了var定义部分及polyfill部分所有依赖文件的依赖图谱和bable转化后的代码;
整体的流程图:
通过 上图流程总结:
- 整个 metro 进行依赖分析和 babel 转换主要通过了JestHasteMap去做依赖分析;
- 在做依赖分析的通过,metro 会监听当前目录的文件变化,然后以最小变化生成最终依赖关系图谱;
- 不管是入口文件解析还是 polyfill 文件的依赖解析都是使用了JestHasteMap;
metro 构建 bundle: 生成
再看 Server 服务启动代码部分我们发现经过buildGraph
之后得到了
prepend: var及polyfill部分的代码和依赖关系
graph: 入口文件的依赖关系及代码
metro 使用了baseJSBundle
将依赖关系图谱和每个模块的代码经过一系列的操作最终使用 bundleToString
转换成最终的代码。