非常教程

Typescript参考手册

模块解析 | Module Resolution

Module Resolution

本节假设了一些有关模块的基本知识。请参阅模块文档以获取更多信息。

模块解析是编译器用来确定输入是指什么的过程。考虑一下类似的导入语句import { a } from "moduleA";为了检查它的使用a,编译器需要确切地知道它代表什么,并且需要检查它的定义moduleA

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

首先,编译器会尝试找到一个代表导入模块的文件。为此,编译器遵循两种不同的策略之一:经典或节点。这些策略告诉编译器在哪里寻找moduleA

如果这不起作用,并且如果模块名称是非相对的(以及它的情况"moduleA"),则编译器将尝试定位环境模块声明。接下来我们将介绍非相对导入。

最后,如果编译器无法解析模块,它会记录一个错误。在这种情况下,错误会是类似的error TS2307: Cannot find module 'moduleA'.

相对与非相对模块导入

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

一个相对进口是一个开头/./../。一些例子包括:

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

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

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

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

非相对导入可以通过baseUrl路径映射来解析,我们将在下面介绍。它们也可以解析为环境模块声明。导入任何外部依赖关系时使用非相对路径。

模块解决策略

有两种可能的模块分辨率策略:节点和经典。您可以使用该--moduleResolution标志来指定模块分辨率策略。如果未指定,则默认为“经典” --module AMD | System | ES2015或“否”。

经典

这曾经是 TypeScript 的默认分辨率策略。目前,这种策略主要是为了向后兼容。

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

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

但是,对于非相对模块导入,编译器逐步从包含导入文件的目录开始,尝试查找匹配的定义文件。

例如:

非相对向进口moduleB,如import { b } from "moduleB",在源文件中/root/src/folder/A.ts,将导致在试图用于定位在以下位置"moduleB"

  1. /root/src/folder/moduleB.ts
  2. /root/src/folder/moduleB.d.ts
  3. /root/src/moduleB.ts
  4. /root/src/moduleB.d.ts
  5. /root/moduleB.ts
  6. /root/moduleB.d.ts
  7. /moduleB.ts
  8. /moduleB.d.ts

节点

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

Node.js 如何解析模块

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

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

  1. 作为文件命名/root/src/moduleB.js,如果存在。
  2. 作为文件夹,/root/src/moduleB如果它包含名为package.json指定一个"main"模块的文件。在我们的例子中,如果 Node.js 找到了/root/src/moduleB/package.json包含的文件{ "main": "lib/mainModule.js" },那么 Node.js 会引用它/root/src/moduleB/lib/mainModule.js
  3. 作为文件夹,/root/src/moduleB如果它包含名为的文件index.js。该文件隐式被认为是该文件夹的“主”模块。

您可以在文件模块文件夹模块的 Node.js文档中阅读更多信息。

但是,解析非相对模块名称的方式不同。节点将在名为node_modules的特殊文件夹中查找您的模块。一个node_modules文件夹可以是在同一水平上作为当前文件或目录中的链越往上。Node 将遍历目录链,查看每个node_modules目录链,直到找到您尝试加载的模块。

遵循上面的示例,考虑是否/root/src/moduleA.js使用非相对路径并进行导入var x = require("moduleB");。然后,节点将尝试解析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_modules Node.js文档中了解有关从中加载模块的更多信息。

TypeScript 如何解析模块

TypeScript 将模仿 Node.js 运行时解析策略,以便在编译时查找模块的定义文件。要做到这一点,打字稿覆盖的打字稿源文件扩展名(.ts.tsx,和.d.ts在节点的分辨率逻辑)。TypeScript 还将使用package.jsonnamed中的字段"types"来反映目的"main"- 编译器将使用它来查找“主要”定义文件以进行协商。

例如,像import { b } from "./moduleB"in /root/src/moduleA.ts这样的导入语句会导致尝试以下位置进行定位"./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 解析逻辑,首先查找文件,然后查找适用的文件夹。所以import { b } from "moduleB"在源文件中/root/src/moduleA.ts会导致以下查找:

  1. /root/src/node_modules/moduleB.ts
  2. /root/src/node_modules/moduleB.tsx
  3. /root/src/node_modules/moduleB.d.ts
  4. /root/src/node_modules/moduleB/package.json(如果它指定了一个"types"属性)
  5. /root/src/node_modules/moduleB/index.ts
  6. /root/src/node_modules/moduleB/index.tsx
  7. /root/src/node_modules/moduleB/index.d.ts
  8. /root/node_modules/moduleB.ts
  9. /root/node_modules/moduleB.tsx
  10. /root/node_modules/moduleB.d.ts
  11. /root/node_modules/moduleB/package.json(如果它指定了一个"types"属性)
  12. /root/node_modules/moduleB/index.ts
  13. /root/node_modules/moduleB/index.tsx
  14. /root/node_modules/moduleB/index.d.ts
  15. /node_modules/moduleB.ts
  16. /node_modules/moduleB.tsx
  17. /node_modules/moduleB.d.ts
  18. /node_modules/moduleB/package.json(如果它指定了一个"types"属性)
  19. /node_modules/moduleB/index.ts
  20. /node_modules/moduleB/index.tsx
  21. /node_modules/moduleB/index.d.ts

不要被这里的步骤吓倒 - TypeScript 仍然只在步骤(8)和(15)跳过两次目录。这实际上并不比 Node.js 本身所做的更复杂。

额外的模块解析标志

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

TypeScript 编译器具有一组附加标志,用于编译器通知预期发生在源的转换以生成最终输出。

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

基本网址

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

设置baseUrl通知编译器在哪里查找模块。所有使用非相对名称的模块导入都假定为相对于baseUrl

baseUrl的值被确定为:

  • baseUrl命令行参数的值(如果给定路径是相对的,则根据当前目录计算)
  • 的值的baseUrl在“tsconfig.json”属性(如果给定的路径是相对的,它的计算基于“tsconfig.json”的位置)

请注意,相对模块导入不会受设置baseUrl影响,因为它们始终相对于其导入文件进行了解析。

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

路径映射

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

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

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

请注意相关"paths"的解决方法"baseUrl"。当设置"baseUrl"为另一个值时".",即tsconfig.json映射的目录必须相应地改变。说,你"baseUrl": "./src"在上面的例子中设置,然后jQuery应该被映射到"../node_modules/jquery/dist/jquery"

使用"paths"还允许更复杂的映射,包括多个后退位置。考虑一个项目配置,其中只有一些模块在一个位置可用,其余的位于另一个位置。构建步骤将把它们放在一起。项目布局可能如下所示:

projectRoot
├── folder1
│   ├── file1.ts (imports 'folder1/file2' and 'folder2/file3')
│   └── file2.ts
├── generated
│   ├── folder1
│   └── folder2
│     └── file3.ts
└── tsconfig.json

相应的tsconfig.json将如下所示:

{
  "compilerOptions": {
  "baseUrl": ".",
  "paths": {
    "*": [
    "*",
    "generated/*"
    ]
  }
  }
}

这会告诉编译器任何与模式匹配的模块导入"*"(即所有值),以查找两个位置:

  1. "*":意思是不变的同名,所以 map <moduleName>=><baseUrl>/<moduleName>
  2. "generated/*"意思是带有附加前缀“生成”的模块名称,所以map <moduleName>=><baseUrl>/generated/<moduleName>

遵循这一逻辑,编译器将尝试解析这两个导入:

  • 导入'folder1 / file2'
    1. 模式'*'匹配并且通配符捕获整个模块名称
    2. 尝试列表中的第一个替换:'*' - > folder1/file2
    3. 替换结果是非相对名称 - 将其与baseUrl - > 结合使用projectRoot/folder1/file2.ts
    4. 文件已存在。完成。
  • 导入'folder2 / file3'
    1. 模式'*'匹配并且通配符捕获整个模块名称
    2. 尝试列表中的第一个替换:'*' - > folder2/file3
    3. 替换结果是非相对名称 - 将其与baseUrl - > 结合使用projectRoot/folder2/file3.ts
    4. 文件不存在,移动到第二个替换
    5. 第二替代'生成/ *' - > generated/folder2/file3
    6. 替换结果是非相对名称 - 将其与baseUrl - > 结合使用projectRoot/generated/folder2/file3.ts
    7. 文件已存在。完成。

带有 rootDirs虚拟目录

有时在编译时将多个目录中的项目源合并在一起以生成单个输出目录。这可以被看作是一组源目录创建一个“虚拟”目录。

使用'rootDirs',你可以通知编译器组成这个“虚拟”目录的 ; 因此编译器可以解析这些“虚拟”目录中的相关模块导入,就像在一个目录中合并在一起一样。

例如,考虑这个项目结构:

 src
 └── views
   └── view1.ts (imports './template1')
   └── view2.ts

 generated
 └── templates
     └── views
       └── template1.ts (imports './view2')

文件src/views是一些 UI 控件的用户代码。其中的文件generated/templates是由模板生成器自动生成的UI模板绑定代码,作为构建的一部分。一个构建步骤将文件复制/src/views,并/generated/templates/views在输出相同的目录。在运行时,视图可以期望其模板存在于其旁边,因此应该使用相对名称来导入它"./template"

要指定与编译器的这种关系,请使用"rootDirs""rootDirs"指定内容预计在运行时合并的的列表。所以按照我们的例子,tsconfig.json文件应该看起来像这样:

{
  "compilerOptions": {
  "rootDirs": [
    "src/views",
    "generated/templates/views"
  ]
  }
}

每次编译器在其中一个子文件夹中看到相对模块导入时rootDirs,它都会尝试在每个条目中查找该导入rootDirs

rootDirs灵活性不限于指定逻辑合并的物理源目录列表。提供的数组可能包含任意数量的特殊目录名称,无论它们是否存在。这允许编译器以类型安全的方式捕获复杂的捆绑和运行时特性,例如条件包含和项目特定的加载器插件。

考虑一种国际化方案,其中构建工具通过插入特殊路径令牌自动生成特定于语言环境的包,例如#{locale},作为相关模块路径的一部分,例如./#{locale}/messages。在这个假设的设置中,该工具枚举支持的语言环境,将抽象路径映射到./zh/messages./de/messages等等。

假设这些模块中的每一个都导出一组字符串。例如./zh/messages可能包含:

export default [
  "您好吗",
  "很高兴认识你"
];

通过利用,rootDirs我们可以通知编译器该映射,从而允许它安全地解析./#{locale}/messages,即使该目录永远不会存在。例如,具有以下内容tsconfig.json

{
  "compilerOptions": {
  "rootDirs": [
    "src/zh",
    "src/de",
    "src/#{locale}"
  ]
  }
}

编译器现在可以解决import messages from './#{locale}/messages'import messages from './zh/messages'达到工具目的,允许在不牺牲设计时间支持的情况下以本地不可知的方式进行开发。

跟踪模块分辨率

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

假设我们有一个使用该typescript模块的示例应用程序。app.ts有一个像进口import * as ts from "typescript"

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

--traceResolution调用编译器

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'. ========

需要注意的事情

  • 导入的名称和位置

======== 从'src / app.ts'解析模块' typescript'。========

  • 编译器遵循的策略

没有指定模块解析类型,使用'NodeJs'

  • 从 npm 包加载类型

'package.json'具有引用'node_modules / typescript / lib / typescript.d.ts'的'types'字段'./lib/typescript.d.ts'。

  • 最后结果

========模块名称“ typescript ”已成功解析为“node_modules / typescript / lib / typescript.d.ts”。========

运用 --noResolve

通常,编译器会在开始编译过程之前尝试解析所有模块导入。每次它成功解析import一个文件时,该文件就会被添加到编译器稍后将要处理的一组文件中。

--noResolve编译器选项指示编译器没有任何文件“添加”到没有在命令行上通过了编译。它仍会尝试将模块解析为文件,但如果未指定文件,则不会包含该文件。

例如:

app.ts

import * as A from "moduleA" // OK, 'moduleA' passed on the command-line
import * as B from "moduleB" // Error TS2307: Cannot find module 'moduleB'.
tsc app.ts moduleA.ts --noResolve

编译app.ts使用--noResolve应导致:

  • 正确地找到moduleA它在命令行中传递的信息。
  • 没有发现moduleB错误,因为它没有通过。

常见问题

为什么排除列表中的模块仍然被编译器拾取?

tsconfig.json将文件夹变成“项目”。如果没有指定任何“exclude”“files”条目,则包含该tsconfig.json目录及其所有子目录的文件夹中的所有文件都将包含在您的编译中。如果你想排除一些文件的使用“exclude”,如果你想指定所有的文件而不是让编译器查看它们,请使用“files”

这是tsconfig.json自动包含。这并不嵌入上面讨论的模块分辨率。如果编译器将某个文件标识为模块导入的目标,则它将包含在编译中,而不管它是否在前面的步骤中被排除。

因此,要从编译中排除文件,您需要排除它以及所有带有import/// <reference path="..." />指令的文件。

模块解析 | Module Resolution相关

Typescript

TypeScript 是 JavaScript 的类型的超集,它可以编译成纯 JavaScript。编译出来的 JavaScript 可以运行在任何浏览器上。

主页 https://www.typescriptlang.org
源码 https://github.com/Microsoft/TypeScript
发布版本 2.6.0