TypeScript 里的 module 解析过程 - Module Resolution

Module Resolution

模块解析是编译器用来确定导入所指内容的过程。考虑像 import { a } from "moduleA"; 这样的导入语句。为了检查 a 的任何使用,编译器需要确切地知道它代表什么,并且需要检查它的定义 moduleA。

此时,编译器会问“moduleA 的形状是什么?”虽然这听起来很简单,但 moduleA 可以在您自己的 .ts/.tsx 文件之一中定义,或者在您的代码所依赖的 .d.ts 中定义。

首先,编译器会尝试定位一个代表导入模块的文件。为此,编译器遵循两种不同策略之一:classical 或 Node。这些策略告诉编译器去哪里寻找 moduleA。

如果这不起作用并且模块名称是非相关的(在“moduleA”的情况下,它是),那么编译器将尝试定位一个环境模块声明。接下来我们将介绍非相对导入。

最后,如果编译器无法解析模块,它将记录一个错误。在这种情况下,错误类似于错误 TS2307:找不到模块 'moduleA'。

Relative vs. Non-relative module imports

根据模块引用是相对的还是非相对的,模块导入的解析方式不同。

相对导入是以 /、./ 或 ../ 开头的导入。 一些例子包括:

  • import Entry from "./components/Entry";
  • import { DefaultHeaders } from "../constants/http";
  • import "/mod";

任何其他导入都被认为是非相关的。 一些例子包括:

  • import * as $ from "jquery";
  • import { Component } from "@angular/core";

相对导入是相对于导入文件解析的,无法解析为环境模块声明。 您应该对自己的模块使用相对导入,以保证在运行时保持其相对位置。

Module Resolution Strategies

有两种可能的模块解析策略:Node 和 Classic。 您可以使用 --moduleResolution 标志来指定模块解析策略。 如果未指定,则 --module commonjs 默认为 Node,否则默认为 Classic(包括 --module 设置为 amd、system、umd、es2015、esnext 等时)。

注意:Node 模块解析是 TypeScript 社区中最常用的,推荐用于大多数项目。 如果您在 TypeScript 中遇到导入和导出的解析问题,请尝试设置 moduleResolution: "node" 以查看它是否解决了问题。

Classical 解析策略

这曾经是 TypeScript 的默认解析策略。 如今,这种策略主要是为了向后兼容。

相对导入将相对于导入文件进行解析。 因此,在源文件 /root/src/folder/A.ts 中 import { b } from "./moduleB" 将导致以下查找:

  • /root/src/folder/moduleB.ts
  • /root/src/folder/moduleB.d.ts

然而,对于非相关模块导入,编译器从包含导入文件的目录开始沿着目录树向上走,试图找到匹配的定义文件。

例如:

在源文件 /root/src/folder/A.ts 中,对 moduleB 的非相对导入,例如 import { b } from "moduleB",将导致尝试使用以下位置来定位 "moduleB":

/root/src/folder/moduleB.ts
/root/src/folder/moduleB.d.ts
/root/src/moduleB.ts
/root/src/moduleB.d.ts
/root/moduleB.ts
/root/moduleB.d.ts
/moduleB.ts
/moduleB.d.ts

Node 模式

这种解析策略试图在运行时模仿 Node.js 模块解析机制。 Node.js 模块文档中概述了完整的 Node.js 解析算法。

Node.js 如何解析模块?

要了解 TS 编译器将遵循哪些步骤,了解 Node.js 模块非常重要。传统上,Node.js 中的导入是通过调用名为 require 的函数来执行的。 Node.js 采取的行为将根据 require 是相对路径还是非相对路径而有所不同。

相对路径相当简单。例如,让我们考虑一个位于 /root/src/moduleA.js 的文件,其中包含 import var x = require("./moduleB"); Node.js 按以下顺序解析该导入:

  • 询问名为 /root/src/moduleB.js 的文件是否存在。
  • 询问文件夹 /root/src/moduleB 是否包含指定“主”模块的名为 package.json 的文件。在我们的示例中,如果 Node.js 发现文件 /root/src/moduleB/package.json 包含 { "main": "lib/mainModule.js" },那么 Node.js 将引用 /root/src/moduleB/lib/mainModule.js。
  • 询问文件夹 /root/src/moduleB 是否包含名为 index.js 的文件。该文件被隐式视为该文件夹的“主”模块。

但是,对非相关模块名称的解析以不同的方式执行。 Node 将在名为 node_modules 的特殊文件夹中查找您的模块。 node_modules 文件夹可以与当前文件位于同一级别,也可以在目录链中的更高级别。 Node 将沿着目录链向上遍历,查看每个 node_modules,直到找到您尝试加载的模块。

按照我们上面的例子,考虑 /root/src/moduleA.js 是否使用非相对路径并导入 var x = require("moduleB");。然后,Node 会尝试将 moduleB 解析为每个位置,直到一个位置正常工作。

(1) /root/src/node_modules/moduleB.js
(2) /root/src/node_modules/moduleB/package.json(如果它指定了“main”属性)
(3) /root/src/node_modules/moduleB/index.js

(4) /root/node_modules/moduleB.js
(5) /root/node_modules/moduleB/package.json(如果它指定了“main”属性)
(6) /root/node_modules/moduleB/index.js

(7) /node_modules/moduleB.js
(8) /node_modules/moduleB/package.json(如果它指定了“main”属性)
(9) /node_modules/moduleB/index.js

请注意,Node.js 在步骤 (4) 和 (7) 中跳转了一个目录。

您可以在 Node.js 文档中阅读有关从 node_modules 加载模块的更多信息。

How TypeScript resolves modules

TypeScript 将模仿 Node.js 运行时解析策略,以便在编译时定位模块的定义文件。为此,TypeScript 在 Node 的解析逻辑上覆盖了 TypeScript 源文件扩展名(.ts、.tsx 和 .d.ts)。 TypeScript 还将使用 package.json 中名为“types”的字段来反映“main”的用途——编译器将使用它来查找要查阅的“main”定义文件。

例如,像 /root/src/moduleA.ts 中的 import { b } from "./moduleB" 这样的导入语句将导致尝试以下位置来定位 "./moduleB":

(1)/root/src/moduleB.ts
(2)/root/src/moduleB.tsx
(3)/root/src/moduleB.d.ts
(4)/root/src/moduleB/package.json(如果它指定了“types”属性)
(5)/root/src/moduleB/index.ts
(6)/root/src/moduleB/index.tsx
(7)/root/src/moduleB/index.d.ts

回想一下,Node.js 查找名为 moduleB.js 的文件,然后是适用的 package.json,然后是 index.js。

同样,非相对导入将遵循 Node.js 解析逻辑,首先查找文件,然后查找适用的文件夹。因此,在源文件 /root/src/moduleA.ts 中 import { b } from "moduleB" 将导致以下查找:

/root/src/node_modules/moduleB.ts
/root/src/node_modules/moduleB.tsx
/root/src/node_modules/moduleB.d.ts
/root/src/node_modules/moduleB/package.json(如果它指定了“types”属性)
/root/src/node_modules/@types/moduleB.d.ts
/root/src/node_modules/moduleB/index.ts
/root/src/node_modules/moduleB/index.tsx
/root/src/node_modules/moduleB/index.d.ts

/root/node_modules/moduleB.ts
/root/node_modules/moduleB.tsx
/root/node_modules/moduleB.d.ts
/root/node_modules/moduleB/package.json(如果它指定了“types”属性)
/root/node_modules/@types/moduleB.d.ts
/root/node_modules/moduleB/index.ts
/root/node_modules/moduleB/index.tsx
/root/node_modules/moduleB/index.d.ts

/node_modules/moduleB.ts
/node_modules/moduleB.tsx
/node_modules/moduleB.d.ts
/node_modules/moduleB/package.json(如果它指定了“types”属性)
/node_modules/@types/moduleB.d.ts
/node_modules/moduleB/index.ts
/node_modules/moduleB/index.tsx
/node_modules/moduleB/index.d.ts

不要被这里的步骤数吓倒——TypeScript 仍然只在步骤 (9) 和 (17) 中两次跳转目录。这实际上并不比 Node.js 本身所做的更复杂。

Additional module resolution flags

项目源布局有时与输出布局不匹配。 通常一组构建步骤会生成最终输出。 其中包括将 .ts 文件编译为 .js,以及将依赖项从不同的源位置复制到单个输出位置。 最终结果是模块在运行时的名称可能与包含其定义的源文件的名称不同。 或者最终输出中的模块路径可能在编译时与其对应的源文件路径不匹配。

TypeScript 编译器有一组额外的标志来通知编译器预期发生在源上的转换以生成最终输出。

需要注意的是,编译器不会执行任何这些转换; 它只是使用这些信息来指导将模块导入解析到其定义文件的过程。

Base Url

在使用 AMD 模块加载器的应用程序中,使用 baseUrl 是一种常见做法,其中模块在运行时“部署”到单个文件夹。 这些模块的源代码可以位于不同的目录中,但是构建脚本会将它们放在一起。

设置 baseUrl 通知编译器在哪里可以找到模块。 假定所有具有非相对名称的模块导入都与 baseUrl 相关。

baseUrl 的值确定为:

(1)baseUrl 命令行参数的值(如果给定的路径是相对的,则根据当前目录计算)

(2)'tsconfig.json' 中 baseUrl 属性的值(如果给定的路径是相对的,则根据 'tsconfig.json' 的位置计算)

请注意,相对模块导入不会受到设置 baseUrl 的影响,因为它们总是相对于它们的导入文件进行解析。

您可以在 RequireJS 和 SystemJS 文档中找到有关 baseUrl 的更多文档。

path mapping

有时模块并不直接位于 baseUrl 下。例如,对模块“jquery”的导入将在运行时转换为“node_modules/jquery/dist/jquery.slim.min.js”。加载器使用映射配置在运行时将模块名称映射到文件,请参阅 RequireJs 文档和 SystemJS 文档。

TypeScript 编译器支持使用 tsconfig.json 文件中的“paths”属性声明此类映射。 以下是如何为 jquery 指定“paths”属性的示例。

{
  "compilerOptions": {
    "baseUrl": ".", // This must be specified if "paths" is.
    "paths": {
      "jquery": ["node_modules/jquery/dist/jquery"] // This mapping is relative to "baseUrl"
    }
  }
}

如何检查 TypeScript 模块解析过程

如前所述,编译器在解析模块时可以访问当前文件夹之外的文件。 在诊断模块未解析的原因或解析为不正确的定义时,这可能很困难。 使用 --traceResolution 启用编译器模块解析跟踪可以深入了解模块解析过程中发生的情况。

假设我们有一个使用 typescript 模块的示例应用程序。

app.ts has an import like import * as ts from "typescript".


│   tsconfig.json
├───node_modules
│   └───typescript
│       └───lib
│               typescript.d.ts
└───src
        app.ts

使用如下命令行编译:

tsc --traceResolution

结果:

======== Resolving module 'typescript' from 'src/app.ts'. ========
Module resolution kind is not specified, using 'NodeJs'.
Loading module 'typescript' from 'node_modules' folder.
File 'src/node_modules/typescript.ts' does not exist.
File 'src/node_modules/typescript.tsx' does not exist.
File 'src/node_modules/typescript.d.ts' does not exist.
File 'src/node_modules/typescript/package.json' does not exist.
File 'node_modules/typescript.ts' does not exist.
File 'node_modules/typescript.tsx' does not exist.
File 'node_modules/typescript.d.ts' does not exist.
Found 'package.json' at 'node_modules/typescript/package.json'.
'package.json' has 'types' field './lib/typescript.d.ts' that references 'node_modules/typescript/lib/typescript.d.ts'.
File 'node_modules/typescript/lib/typescript.d.ts' exist - use it as a module resolution result.
======== Module name 'typescript' was successfully resolved to 'node_modules/typescript/lib/typescript.d.ts'. ========

触发模块解析的源代码位置:

======== Resolving module ‘typescript’ from ‘src/app.ts’. ========

模块解析策略:

Module resolution kind is not specified, using ‘NodeJs’.

Loading of types from npm packages:

‘package.json’ has ‘types’ field ‘./lib/typescript.d.ts’ that references ‘node_modules/typescript/lib/typescript.d.ts’.

最后成功解析的输出:

======== Module name ‘typescript’ was successfully resolved to ‘node_modules/typescript/lib/typescript.d.ts’. ========

上一篇:vue在安装时 node-sass报错


下一篇:飞天加速计划·高校学生校实践 使用体验