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

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的内容

results matching ""

    No results matching ""