TypeScript 模块解析机制详解:从策略到调试

作者:拾光时间:2026-10-30 22:19:52浏览:108

本文深入解析 TypeScript 的模块解析机制,涵盖相对与非相对导入的区别、Node 与 Classic 两种解析策略的工作流程。通过对比 Node.js 运行时行为,说明 TypeScript 如何定位 .ts、.tsx 及 .d.ts 文件,并介绍 baseUrl 和 paths 配置的作用。最后提供使用 --traceResolution 进行调试的具体示例,帮助开发者解决模块找不到的问题。

模块解析

模块解析

模块解析是编译器确定导入语句所指代内容的过程。以 `import { a } from "moduleA";` 为例,为了验证对 `a` 的任何使用,编译器必须确切知道 `a` 代表什么,并检查其定义所在的 `moduleA`。

此时,编译器会询问“`moduleA` 的结构是什么?”这看似简单,但 `moduleA` 可能定义在您自己的 `.ts` 或 `.tsx` 文件中,也可能定义在代码所依赖的 `.d.ts` 环境模块声明中。

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

如果上述尝试失败,且模块名称是非相对的(如本例中的 `moduleA`),编译器将尝试定位一个环境模块声明。接下来我们将介绍非相对导入。

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

相对与非相对模块导入

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

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

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

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

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

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

模块解析策略

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

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

经典解析策略

这曾经是 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` 加载模块的更多信息。

TypeScript 如何解析模块

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 本身所做的更复杂。

额外的模块解析标志

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

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

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

Base Url

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

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

`baseUrl` 的值确定为:

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

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

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

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

路径映射

有时模块并不直接位于 `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’. ========

以上就是TypeScript Module Resolution解析过程的详细内容,更多关于TypeScript Module Resolution的资料请关注本站其它相关文章!

作者最新文章
相关文章
更多