metro
metro v0.66.2 react-native-community/cli v6.4.0 mozilla/source-map 0.7.3
Metro打包分为三个阶段
- 解析 resolver,构建图
- 转换 transform,转换为目标格式
- 序列化
metro API
编译
const config = await Metro.loadConfig();
await Metro.runBuild(config, {
entry: 'index.js',
platform: 'ios',
minify: true,
out: '/Users/Metro/metro-ios.js'
});
loadConfig函数为倒入metro.config.js文件,runBuild就是编译,调用的就是Server.js的build函数
可以设置sourcemap是内置,还是分离出来
cli中metro的使用
打包命令,参见文档
npx react-native bundle --entry-file ./index.js --bundle-output AwesomeProject.bundle --minify false
metro的打包操作见buildBundleWithConfig函数
const bundle = await output.build(server, requestOpts);
await output.save(bundle, args, logger.info);
// Save the assets of the bundle
const outputAssets: AssetData[] = await server.getAssets({
...Server.DEFAULT_BUNDLE_OPTIONS,
...requestOpts,
bundleType: 'todo',
});
// When we're done saving bundle output and the assets, we're done.
return await saveAssets(outputAssets, args.platform, args.assetsDest);
output两种模式 metro/src/shared/output/bundle or metro/src/shared/output/RamBundle
具体见对应buildBundle函数,这里会调用metro/src/Server.js的build函数,这是打包的核心逻辑
bundle
build
Server.js的build函数
输入是BundleOptions,输出是bundle code和bundle map
- IncrementalBundler的buildGraph函数,输入entryfile,输出graph,然后调用的是buildGraphForEntries函数,最后调用的是DeltaBundler的buildGraph函数
- DeltaBundler的buildGraph主要是DeltaCalculator初始化,并且调用getDelta,最后返回graph
- baseJSBundle(输入Graph,输出Bundle)
- processModules(输入graph.dependencies.values(),输出Bundle的modules)
- bundleToString, Bundle to string,对于bundle 的modules进行字符串拼接
- sourceMapString,生产sourcemap,具体的实现在sourceMapGenerator函数
- 先调用在sourceMapString.js的getSourceMapInfosImpl函数,这里主要是对传入的modules进行过滤,并且组装成
- 然后调用metro-source-map的fromRawMappings函数,传入raw mappings 获得source map
buildGraph函数部分,这中间会进行resolve和transform的处理
对于getDelta函数,它会调用_getChangedDependencies,然后调用initialTraverseDependencies,这个函数可以生成graph.dependencies
async function initialTraverseDependencies<T>(
graph: Graph<T>,
options: Options<T>,
): Promise<Result<T>> {
const delta = {
added: new Set(),
modified: new Set(),
deleted: new Set(),
inverseDependencies: new Map(),
};
const internalOptions = getInternalOptions(options);
// entryPoints 入口文件 如index.js
// traverseDependenciesForSingleFile这里调用 processModule
await Promise.all(
graph.entryPoints.map((path: string) =>
traverseDependenciesForSingleFile(path, graph, delta, internalOptions),
),
);
reorderGraph(graph, {
shallow: options.shallow,
});
return {
added: graph.dependencies,
modified: new Map(),
deleted: new Set(),
};
}
traverseDependenciesForSingleFile内调用的函数是processModule。
processModule这个会调用resolveDependencies,这里会进行resolve操作,resolve的函数在buildGraph的参数内定义
processModule也会调用transform,transform的函数在buildGraph的参数内定义
resolve
resolve见node-haste/DependencyGraph.js 的 resolveDependency函数
resolve的是在buildGraph流程中的processModule时候发起调用的
resolveDependency 的入参 from to,分别表示给定路径,以及给定文件路径的依赖路径
例如index.js
import App from '.App'
那么from是**/index.js(绝对路径),to是./App
resolveDependency的返回值是根据from,生成to的绝对路径,最终实现在metro-resolve的resolve.js的resolve函数
metro config resolver
metro config可以自定义resolver,在resolve.js里会调用
const defaultResolver = require('metro-resolver').resolve;
module.exports = {
resolver: {
resolveRequest: (context, moduleName, platform, realModuleName) => {
console.log(`log module name ${moduleName}`);
if (moduleName === './test') {
return {
filePath: 'testDir/test.js',
type: 'sourceFile',
};
} else {
return defaultResolver(
{
...context,
resolveRequest: null,
},
moduleName,
platform,
realModuleName,
);
}
},
},
};
transform
processModule里会调用transform函数,用于获取给定路径下所有依赖,以便之后逐个对每个依赖进行resolve
具体实现在Bundler.js的transformFile函数,这里会调用到Transformer.js的transformFile函数,最终调用到Worker.js的transform函数,这里会根据transformerPath生成真正的Transformer。
默认的transformerPath为metro-transform-worker,见metro-config/index.js
saveBundleAndMap
- bundle.code 写入指定路径
- sourcemap(bundle.map)写入指定路径,先遍历sourcemap替换为相对路径,然后再写入
RamBundle
包格式
RamBundle有两种模式
- Indexed RAM bundle
- File RAM bundle
逻辑见RamBundle/as-assets.js和RamBundle/as-indexed-file.js
Indexed ram bundle 的格式如,出处见https://metrobundler.dev/docs/bundling
` 0 1 2 3 4 5 6
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Magic number | Header size |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Startup code size | Module 0 offset |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Module 0 length | |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +
| |
+ ... +
| |
+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| | Module n offset |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Module n length | Module 0 code | Module 0 code | ... | \0 |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Module 1 code | Module 1 code | ... | \0 | |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +
| |
+ ... +
| |
+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| | Module n code | Module n code | ... | \0 |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+`
File RAM bundle
每一个模块都存储为一个单独的文件,文件名js-modules/${id}.js
,打包后会产生一个UNBUNDLE的文件,内容为magic number 0xFB0BD1E5
build
ram bundle 的build 是调用的是getRamBundleInfo函数,见RamBundle.js
async function build(packagerClient, requestOptions) {
const options = {
...Server.DEFAULT_BUNDLE_OPTIONS,
...requestOptions,
bundleType: "ram"
};
return await packagerClient.getRamBundleInfo(options);
}
这里会调用Server.js 的getRamBundleInfo,先调用buildGraph(同bundle一样也是IncrementalBundler的buildGraph),然后调用getRamBundleInfo.js的getRamBundleInfo
getRamBundleInfo函数
这里会根据输入的pre,graph.dependencies等组装modules
每一个module的module id是根据path 计算的自增id,调用的是createModuleId函数,见createModuleIdFactory.js,这里会保存path和id的映射
getRamBundleInfo函数最后会返回
export type RamBundleInfo = {|
getDependencies: string => Set<string>,
startupModules: $ReadOnlyArray<ModuleTransportLike>,
lazyModules: $ReadOnlyArray<ModuleTransportLike>,
groups: Map<number, Set<number>>,
|};
save
ram bundle 的save函数为saveAsIndexedFile,saveAsIndexedFile的作用是保存所有的JS module为一个单独的文件,文件开始一个偏移表,包含module id 和它们的偏移。
- buildTableAndContents,构建偏移表和内容
- buildSourcemapWithMetadata 构建sourcemap
- writeSourceMap,写sourcemap文件
buildTableAndContents代码如下
function buildTableAndContents(
startupCode: string,
modules: $ReadOnlyArray<ModuleTransportLike>,
moduleGroups: ModuleGroups,
encoding?: 'utf8' | 'utf16le' | 'ascii',
): Array<Buffer> {
// file contents layout:
// - magic number char[4] 0xE5 0xD1 0x0B 0xFB (0xFB0BD1E5 uint32 LE)
// - offset table table see `buildModuleTables`
// - code blob char[] null-terminated code strings, starting with
// the startup code
const startupCodeBuffer = nullTerminatedBuffer(startupCode, encoding);
const moduleBuffers = buildModuleBuffers(modules, moduleGroups, encoding);
const table = buildModuleTable(
startupCodeBuffer,
moduleBuffers,
moduleGroups,
);
return [fileHeader, table, startupCodeBuffer].concat(
moduleBuffers.map(({buffer}) => buffer),
);
}
fileHeader的内容是 magic-number.js 的 module.exports = 0xfb0bd1e5;
nullByteBuffer是填充了0
const nullByteBuffer = Buffer.alloc(1).fill(0);
nullTerminatedBuffer函数的作用是在content后拼接了nullByteBuffer
buildModuleTable
function buildModuleTable(
startupCode: Buffer,
moduleBuffers: Array<{
buffer: Buffer,
id: number,
...
}>,
moduleGroups: ModuleGroups,
): Buffer {
// table format:
// - num_entries: uint_32 number of entries
// - startup_code_len: uint_32 length of the startup section
// - entries: entry...
//
// entry:
// - module_offset: uint_32 offset into the modules blob
// - module_length: uint_32 length of the module code in bytes
const moduleIds = [...moduleGroups.modulesById.keys()];
const maxId = moduleIds.reduce((max: number, id: number) =>
Math.max(max, id),
);
const numEntries = maxId + 1;
const table: Buffer = Buffer.alloc(entryOffset(numEntries)).fill(0);
// num_entries
table.writeUInt32LE(numEntries, 0);
// startup_code_len
table.writeUInt32LE(startupCode.length, SIZEOF_UINT32);
// entries
let codeOffset = startupCode.length;
moduleBuffers.forEach(({id, buffer}) => {
const group = moduleGroups.groups.get(id);
const idsInGroup: Array<number> = group
? [id].concat(Array.from(group))
: [id];
idsInGroup.forEach((moduleId: number) => {
const offset = entryOffset(moduleId);
// module_offset
table.writeUInt32LE(codeOffset, offset);
// module_length
table.writeUInt32LE(buffer.length, offset + SIZEOF_UINT32);
});
codeOffset += buffer.length;
});
return table;
}
buildSourcemapWithMetadata函数见buildSourcemapWithMetadata.js文件,调用combineSourceMapsAddingOffsets函数,这里会通过combineMaps函数组装sections,combineMaps内会递归调用combineMaps,所以最终形成的map文件也是一个section嵌套一个section
每个section的数据结构为
const Section = (line: number, column: number, map: MixedSourceMap) => ({
map,
offset: {line, column},
});
android 下默认写入的是file ram bundle,其中startupModules写入一个单独的js文件,lazyModules分别写入js-modules目录下的每一个文件
在build的时候,module type 为 js/script、以及预加载的module是startupModules,type为js/module的是lazyModules。其中预加载的module 可以在metro.config.js配置
例如 就是js/script
node_modules/metro-runtime/src/polyfills/require.js
这部分会加入getPrependedScripts进行build,配置在metro-config下的defaults.js
exports.moduleSystem = (require.resolve(
'metro-runtime/src/polyfills/require.js',
): string);
runServer
react-native start调用的就是runServer函数
- connect函数调用
- createConnectMiddleware
- InspectorProxy 创建
- createServer
- httpServer.listen
collectDependencies.js
collectDependencies函数,主要用于对require和import的转换,比如 require('Foo')
会被转换为require(_depMap[3], 'Foo')
,_depMap
来自外部作用域。
- 构造visitor
- vistor里CallExpression逻辑,这里会调用processImportCall或processRequireCall
- 调用traverse函数,这里调用的是@babel/traverse
metro-runtime
require.js
调用nativeRequire函数的逻辑
metroRequire->guardedLoadModule->loadModuleImplementation->nativeRequire
metroRequire的入参为moduleId,这里的modules会存储moduleId 和 module的映射,根据moduleId取出module
loadModuleImplementation的实现
- 如果存在nativeRequire,则调用nativeRequire
- define定义的function进行调用,这是关键逻辑
define,保存moduleId的map modules,入参,第一个为function,第二个为moduleId,第三个为依赖的moduleId数组
function define(
factory: FactoryFn,
moduleId: number,
dependencyMap?: DependencyMap,
): void {
const mod: ModuleDefinition = {
dependencyMap,
factory,
hasError: false,
importedAll: EMPTY,
importedDefault: EMPTY,
isInitialized: false,
publicModule: {exports: {}},
};
modules[moduleId] = mod;
}
require引用的模块会被添加到bundle中,require的参数是一个编译时的常量(其逻辑见transform里的collectDependencies.js如下函数)。
function getModuleNameFromCallArgs(path: NodePath<CallExpression>): ?string {
const expectedCount =
path.node.callee.name === '__conditionallySplitJSResource' ? 2 : 1;
const args = path.get('arguments');
if (!Array.isArray(args) || args.length !== expectedCount) {
throw new InvalidRequireCallError(path);
}
const result = args[0].evaluate();
if (result.confident && typeof result.value === 'string') {
return result.value;
}
return null;
}
metro-transform-worker
transform->transformJSON / transformAsset / transformJSWithBabel->transformJS->collectDependencies
transformJSWithBabel,入参为file,含有file的路径,大小,以及源码
- require metro-react-native-babel-transformer ,创建transform,并且调用transform,获取ast
- 如果存在HermesParser,则调用HermesParser的parse,否则调用parseSync(@babel/core),最后产生sourceAst
- transformFromAstSync(@babel/core),根据sourceAst产生返回值ast
- generateFunctionMap
- 调用transformJS
- collectDependencies,见worker/collectDependencies.js
- registerDependency函数,负责管理dependency
- collectDependencies,见worker/collectDependencies.js
babel版本7.21.3
babel是一个JS编译器,负责transform。
example如下
const config = {
ast: true
}
const sourceCode = "if (true) console.log('hello');";
const parsedAst = parseSync(sourceCode, config)
const transformResult = transformFromAstSync(parsedAst, sourceCode, config);
输出的result含code、map、ast
code:if(true)return
ast
{
"type": "File",
"start": 0,
"end": 31,
"loc": {
"start": {
"line": 1,
"column": 0,
"index": 0
},
"end": {
"line": 1,
"column": 31,
"index": 31
}
},
"errors": [
],
"program": {
"type": "Program",
"start": 0,
"end": 31,
"loc": {
"start": {
"line": 1,
"column": 0,
"index": 0
},
"end": {
"line": 1,
"column": 31,
"index": 31
}
},
"sourceType": "script",
"interpreter": null,
"body": [
{
"type": "IfStatement",
"start": 0,
"end": 31,
"loc": {
"start": {
"line": 1,
"column": 0,
"index": 0
},
"end": {
"line": 1,
"column": 31,
"index": 31
}
},
"test": {
"type": "BooleanLiteral",
"start": 4,
"end": 8,
"loc": {
"start": {
"line": 1,
"column": 4,
"index": 4
},
"end": {
"line": 1,
"column": 8,
"index": 8
}
},
"value": true
},
"consequent": {
"type": "ExpressionStatement",
"start": 10,
"end": 31,
"loc": {
"start": {
"line": 1,
"column": 10,
"index": 10
},
"end": {
"line": 1,
"column": 31,
"index": 31
}
},
"expression": {
"type": "CallExpression",
"start": 10,
"end": 30,
"loc": {
"start": {
"line": 1,
"column": 10,
"index": 10
},
"end": {
"line": 1,
"column": 30,
"index": 30
}
},
"callee": {
"type": "MemberExpression",
"start": 10,
"end": 21,
"loc": {
"start": {
"line": 1,
"column": 10,
"index": 10
},
"end": {
"line": 1,
"column": 21,
"index": 21
}
},
"object": {
"type": "Identifier",
"start": 10,
"end": 17,
"loc": {
"start": {
"line": 1,
"column": 10,
"index": 10
},
"end": {
"line": 1,
"column": 17,
"index": 17
},
"identifierName": "console"
},
"name": "console"
},
"computed": false,
"property": {
"type": "Identifier",
"start": 18,
"end": 21,
"loc": {
"start": {
"line": 1,
"column": 18,
"index": 18
},
"end": {
"line": 1,
"column": 21,
"index": 21
},
"identifierName": "log"
},
"name": "log"
}
},
"arguments": [
{
"type": "StringLiteral",
"start": 22,
"end": 29,
"loc": {
"start": {
"line": 1,
"column": 22,
"index": 22
},
"end": {
"line": 1,
"column": 29,
"index": 29
}
},
"extra": {
"rawValue": "hello",
"raw": "'hello'"
},
"value": "hello"
}
],
"typeArguments": null
}
},
"alternate": null
}
],
"directives": [
]
},
"comments": [
]
}
具体ast的 spec 可以查看,https://github.com/babel/babel/blob/main/packages/babel-parser/ast/spec.md
metro-transform-plugins
type TransformPlugins = {
addParamsToDefineCall(string, ...Array<mixed>): string,
constantFoldingPlugin: ConstantFoldingPlugin,
importExportPlugin: ImportExportPlugin,
inlinePlugin: InlinePlugin,
normalizePseudoGlobals: NormalizePseudoGlobalsFn,
getTransformPluginCacheKeyFiles(): $ReadOnlyArray<string>,
};
babel的transformFromAstSync函数可以传入plugin数组
举例一个最简单的plugin
export default function() {
return {
visitor: {
Identifier(path) {
const name = path.node.name;
// reverse the name: JavaScript -> tpircSavaJ
path.node.name = name
.split("")
.reverse()
.join("");
},
},
};
}
metro-resolver
resolve流程
- 主要调用resolveModulePath函数
- 调用redirectModulePath,获取到redirectedPath
- 调用resolveFileOrDir,拼接路径
- 如果是allowHaste为true,则调用resolveHasteName函数
- 如果自定义resolve,则调用resolveRequest
- 最后处理node_modules的内容