Skip to main content
overcache

Dive into Metro: Metro Resolver

打包器的重要组成之一就是 resolver, 它需要确定引入的每一个依赖在磁盘中的绝对位置.

打包器的作用

在打包的过程中, 从入口文件开始往下, 所有的依赖织成了一张依赖关系网. 引入依赖的方式如 import/require 即声明了这种依赖关系. 但是开发者可以采取多种方式来写依赖声明语句:

  • import foo
  • import ./relative/path/foo.js
  • import /absolute/path/foo.js
  • import ./relative/assset/foo.png

resolver 的作用就是按照一定的规则, 根据上下文的信息来确定依赖在文件系统上的绝对路径地址.

统一依赖的路径

从路径的视角, 依赖的引入大体分为三种形式:

  • 声明依赖的相对路径
  • 声明依赖的绝对路径
  • 仅仅声明依赖名称, 如 import foo

对于包含路径的依赖, 查找起来比较简单, 根据当前路径拼接拿到绝对路径就大差不差了. 对于最后一种方式, metro resovler 一般按照如下的规则转化成带路径的形式:

  • 和 nodejs 一致, 从当前文件路径开始依次向根目录迭代, 每个途经的目录都拼上 node_modules, 加入到搜索路径的集合中(姑且称为 nodeModulesPaths)
  • 提供入参 extraNodeModules, 调用者可以为特定的依赖指定路径. 如 { 'react-native': '/path/to/your/custom/react-native/path' }, 如果依赖在 extraNodeModules 中已经指定了路径, 那么将该路径添加至 extraPaths 中
  • 最终由 nodeModulesPaths 和 extraPaths 构成搜索路径集合:
    const allDirPaths = nodeModulesPaths
        .map(nodeModulePath => path.join(nodeModulePath, moduleName))
        .concat(extraPaths);
    

如此, 接下来就只需要考虑绝对路径或者相对路径的依赖形式就好了

如何从相对或者绝对路径确定唯一的文件

路径可能是以下情况之一:

  • 文件夹路径
  • 带后缀的文件路径
  • 不带后缀的文件路径

为了得到一个确定的文件, metro resolver 依次做一下尝试:

  1. 把它当成文件解析
  2. 把它当成文件夹解析

当成文件处理

在 resolver 眼中, 文件类型只有两种:  assetFile 或者 sourceFile, 通过文件后缀即可确定是不是 assetFile.

  • 对 assetFile 直接交给入参的 resolveAsset 方法去解析

  • 对 sourceFile 则在文件路径上添加各种平台中缀(ios/android/web/native 等)以及各种后缀(js/ts/jsx/tsx 等), 以文件存在作为解析成功的标志

当成文件夹处理

先检查路径下是否存在 package.json 文件:

  • 存在. 调用 getPackageMainPath 得到包的入口路径
    • 用 resolveFile 解析入口路径
    • 若失败, 则按照"当成文件解析"的步骤处理目录下的 index 文件
    • 若仍然失败, 抛出 InvalidPackageError 错误
  • 不存在. 按照"当成文件解析"的步骤处理目录下的 index 文件

外部传入的功能函数

metro-resolver 是一个很纯粹的函数式模块, 它内部没有副作用. 但是检查文件/文件夹是否存在总得涉及磁盘 IO, 这些脏活累活由调用者(Metro Bundler)传入的函数来完成:

  • resolveAsset
  • getPackageMainPath
  • redirectModulePath

resolveAsset 返回资源的路径集合, metro 在调用 metro resolver 的时候, 传入的实现为:

resolveAsset: (
      dirPath: string,
      assetName: string,
      extension: string,
    ): ?$ReadOnlyArray<string> => {
      const basePath = dirPath + path.sep + assetName;
      const assets = [
        basePath + extension,
        ...assetResolutions.map(
          resolution => basePath + '@' + resolution + 'x' + extension,
        ),
      ].filter(candidate => hasteFS.exists(candidate));
      return assets.length ? assets : null;
    }

getPackageMainPath 根据依赖的 package.json 路径, 返回入口文件地址.

redirectModulePath 是针对 package-browser-field-spec 进行的处理, 简而言之, 就是根据 package.json 中的 browser 字段的信息进行重定向或者忽略. 为了满足该 spec, 在解析依赖的过程中, 多次调用了 redirectModulePath 来判断是否需要对依赖进行重定向:

  • 带路径的依赖, 把依赖处理成绝对路径后, 调用它得到 redirectedPath
  • 不带路径的依赖, 调用它得到 realModuleName
  • 在向根目录迭代搜索 node_modules 时, 对于每个拼接的路径(如 /path/to/node_modules/realModuleName), 都调用它得到一个 candidate
  • 在根据可能得扩展名确定文件路径时, 如果扩展名非空, 调用它得到 redirectedPath

使用缓存加速 resolve

Metro 对模块的抽象, 分为 Package 和 Module. Package 可以理解为 npm 包, 一个 npm 包含了元信息文件 package.json 以及相关的代码文件. Module 就是源码文件, 一个 js/ts/jsx 等文件都可以称之为 Module, 一个 Module 必有一个 Package 与之关联, 而一个 Package 则关联一个或多个 Module.

一个 Module 被不被采用被不被重定向为其他 Module, 需要根据 Package 的信息(browser字段)处理, 所以必须要建立 Package/Module 之间的关联, 方便进行查询. 且为了加快查询的过程, 使用了 ModuleCache 进行缓存.

ModuleCache 是一个简单的工具类, 内部通过若干个 map 数据结构:

- _moduleCache: { [filePath: string]: Module }
- _packageCache: { [filePath: string]: Package }
- _packagePathByModulePath: { [filePath: string]: string }
- _modulePathsByPackagePath: { [filePath: string]: Set<string> }

如此可以加速Package和Module的关联查询的过程:

- getModule: (filePath: string) => Module
- getPackage: (filePath: string) => Package
- getPackageForModule: (module: Module) => Package?
- getPackageOf: (modulePath: string) => Package?

总结

以上, 就是 Metro resolver 的基本实现, 更多的细节, 还需要阅读源码进行了解:

  • metro-resolver 的源码, 在 metro/packages/metro-resolver
  • Metro 对 metro-resolver 的调用, 在文件 metro/packages/metro/src/node-haste/DependencyGraph/ModuleResolution.js
  • Metro 对 ModuleResolution 的调用, 在文件 metro/packages/metro/src/node-haste/DependencyGraph.js