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 依次做一下尝试:
- 把它当成文件解析
- 把它当成文件夹解析
当成文件处理
在 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
中