typescript 的模块解析策略介绍

Module Resolution

模块解析 是编译器用来确认一个 import 语句指向的过程。考虑一个类似于 import { a } from "moduleA" ;为了检查是否有对 a 的使用,编译器需要准去知道这句话的意思,并且需要检查 moduleA 的定义。

这时候,编译器会问 “ moduleA 长什么样?” ,这听起来很直接,因为 moduleA 可能是在 .ts/.tsx 文件里定义的,或者是在代码依赖的 .d.ts 文件中。

首先,编译器会尝试定位表示该被引用的模块的位置,为此编译器需要依据两种原则中的一个 : Classic 或者 Node 。这两个策略告诉编译器去哪里寻找 moduleA.

如果没有成功,并且模块名称是非相对( non-relative )路径(比如说 moduleA ),那么编译器会尝试定位一个 临近模块定义( ambient module declaration) . 马上就会说道非相对路径引用。

最后,如果编译器不能解析这个模块的话,会打印出一个错误提示。这种情况下,错误提示会是 error TS2307: Cannot find module 'moduleA'.

相对或者非相对模块引用 ( Relative vs. Non-relative module imports )

项目目录结构:

-- src
 -- outgame
 	-- index.ts
 index.ts

/src/Index.ts 里写的是 import 'outgame/index' , 运行时解析不到。

修改为 import './outgame/index' 后可以

模块引用根据模块是否是相对路径决定。

/. ./ 或者 ../ 开头的引用,就是相对引用。举例:

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

其他的引用都被认为是 非相对引用. Some examples include:

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

A relative import is resolved relative to the importing file and cannot resolve to an ambient module declaration. You should use relative imports for your own modules that are guaranteed to maintain their relative location at runtime.

相对引用引用文件的路径进行相对路径解析,并且不可以解析到外部模块。应该使用相对引用来引用那些运行时肯定在这个“相对路径”上的模块。

A non-relative import can be resolved relative to baseUrl, or through path mapping, which we’ll cover below. They can also resolve to ambient module declarations. Use non-relative paths when importing any of your external dependencies.

非相对引用可以相对于 baseUrl 被解析,或者通过路径映射(等下会说)。这些也可以被解析成 临近模块声明。在引入外部模块的依赖的时候,应该使用非相对路径。

模块解析策略

有两种可选的模块解析策略 : Node 和 Classic 。可以使用 --moduleResolution 标记来指定模块解析策略。如果未指定的话, --module commonjs 使用 Node 策略解析,其余情况使用 Classic 策略解析(包括 --module 设置为 amd , system , umd , es2015 , esnext 等情况)

注意 : node 模块解析是 TypeScript 社区最经常被使用的解析方式,也推荐绝大多数项目使用。如果使用 TypeScript 里的 importexport 有问题的话,可以尝试设置 moduleResolution : node 解决问题。

Classic

这曾是 TypeScript 的默认解析策略。现在,这个解析策略主要用来做向前兼容。

相对引入会相对于引入文件的路径进行解析。所以在文件 /root/src/folder/A.ts 里的 import { b } from "./moduleB" 语句会导致在下列路径下的寻找:

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

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

    🤔 .d.ts 什么意思

不过对于非相对路径,编译器会向上遍历引入文件的目录树,尝试寻找匹配的文件。

比如:

/root/src/folder/A.ts 文件里,通过 import { b } from "moduleB" 来使用非相对引用引用 moduleB ,这导致编译器会在下列位置定位 "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

这个解析方式尝试模仿 Node.js 的运行时模块解析策略机制。完整的 Node.js 的模块解析算法在 Node.js module documentation.

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. 询问 /rrot/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 的文件,这个文件隐式地被当做文件的 main 模块。

You can read more about this in Node.js documentation on file modules and folder modules.

不过,解析非相对模块名称是不一样的。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 (if it specifies a "main" property)

  3. /root/src/node_modules/moduleB/index.js

  4. /root/node_modules/moduleB.js

  5. /root/node_modules/moduleB/package.json (if it specifies a "main" property)

  6. /root/node_modules/moduleB/index.js

  7. /node_modules/moduleB.js

  8. /node_modules/moduleB/package.json (if it specifies a "main" property)

  9. /node_modules/moduleB/index.js

注意 (4) 和 (7) 的目录变化

You can read more about the process in Node.js documentation on loading modules from node_modules.

TypeScript 如何解析模块

TypeScript 模拟 Node.js 的模块寻找逻辑,同时覆盖了 TypeScript 的源文件后缀类型 (.ts , .tsx , .d.ts )。TypeScript 同时在 package.json 里有一个 "types" 属性来模拟 "main" ,这个属性用来声明模块的 main 文件。

比如说,/root/src/moduleA.ts 里有一句 import { b } from "./moduleB",会导致在如下位置寻找 "./moduleB"

先想想再看[答案](#答案 : TS 如何寻找相对路径)

非相对引入也会模拟 Node.js 的解析逻辑,往上寻找合适的目录。所以 /root/src/moduleA.ts 中的 import { b } from "moduleB" 会导致这样的寻找路径:

思考下,查看 [答案](#答案 : TS 如何寻找非相对路径)

附加模块解析标志

项目源码的稳健布局有时候和输出布局( output )是不同的。通常一系列构建步骤会生成最终的输出布局。这包括 编译 .ts 文件为 .js ,从不同的目录位置把依赖项复制到单个输出目录,等。这综合的结果就是运行时的模块可能和源文件的名字不同。或者输出布局里的模块路径和编译前的原文件路径不匹配。

TypeScript 编译器有一些附加标志(additional flags),可以告诉编译器希望怎么从源文件生成最终输出。

BaseUrl

使用 AMD 模块加载器的时候,模块会在运行时被“部署”到一个单独的文件夹。这时候, baseUrl 是一个比较常见的操作。源文件可以在不同的目录,但是构建脚本会把它们放到一块。

设置 baseUrl 告诉编译器去哪里寻找模块。所有的非相对路径引用都会被认为是相对于 baseUrl

测试下, 是不是相当于是主动设置的 node_modules ?

baseUrl 可以这样定义:

  • basrUrl 命令行参数的值(如果是相对路径,相对于当前目录)
  • tsconfig.json 中的 baseUrl 的值(如果是相对路径,相对于 tsconfig.json 的目录)

注意相对路径引用模块不受 baseUrl 的影响,因为他们总是按照引入文件的路径做相对解析的。

You can find more documentation on baseUrl in RequireJS and SystemJS documentation.

路径映射( Path Mapping)

有时候模块不是直接在 baseUrl 指向的目录下的。比如,引入 ”jquery“ 模块的时候,在运行时会被转成 "node_modules/jquery/dist/jquery.slim.min.js" 。 加载器使用一个映射配置来把模块名映射成运行时的文件,see RequireJs documentation and SystemJS documentation.

请 dengke 解释一下

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"
    }
  }
}

注意 "paths" 也是相对于 "baseUrl" 的。在设置了 "baseUrl" 的值不是 "." 的时候, tsconfig.json 里设置的映射目录也需要对应地改变。比如说,如果上例中设置了 "baseUrl" : "./src" 的话, jquery 应该映射为 "../node_modules/jquery/dist/jquery"

"paths" 也允许一些常见的映射的写法,比如说多重后退位置(multiple fall back locations)。对于一个项目,在一个位置只有一部分模块,其余的部分在另一个位置。构建中会有一步把它们放到同一个地方。项目的文件布局可能类似:

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/*"]
    }
  }
}

这就告诉编译器对于任何模块,如果其 import 了 "*" 可以匹配的模块(也就是所有模块),就去两个地方寻找:

  1. "*" : 表示原来的字符串,所以映射 <moduleName><baseUrl>/<moduleName>
  2. "generated/*" 表示模块名称附加一个前缀 "generated",就会映射 <moduleName><baseUrl>/generated/<moduleName>

按照这个逻辑,编译器对于 import 'folder1/file2' 会尝试这样解析:

  1. 模式(pattern) “*” 匹配成功,并且捕获到了整个模块名
  2. 尝试以第一个替换进行 : ’*’ -> folder1/file2
  3. 替换接过是非相对路径,将其和 baseUrl 拼接 -> projectRoot/folder1/file2.ts
  4. 该文件存在。寻找结束

对于 import folder2/file3 :

查看答案

包含 rootDirs 的虚拟目录

有时候项目里的源文件在不同的目录下,但是运行时这些文件被生成到一个单独的输出目录中。这些可以被看作一些源目录创建了一个 “虚拟” 目录。

使用 'rootDirs' ,可以告诉编译器这个 “虚拟” 目录的根目录(root)。所以编译器可以解析这些“虚拟”目录下的模块引用,就好像文件都合并到一个目录中一样。

比如说如下的项目结构:

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

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

src/views 下的文件是一些用于 UI 的。generated/templated 下的文件是构建过程中的模板生成器自动生成的一些 UI 胶水代码。构建中会有一步把 /src/views/generated/templates/views 里的文件复制到同一个目录下。在运行时, view 可以认为它的 template 是在其旁边的,所以可以直接使用相对引用(./template

为了让编译器知道这个关系,使用 "rootDirs" 来制定一些列 根目录 列表,列表中的元素会在运行时的时候合并到一个目录中。在我们的例子中,tsconfig.json 会类似于:

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

每当编译器看到引用的相对路径的模块是 rootDirs 的一个子目录下的,就会尝试在 rootDirs 下的每一个目录下寻找。

rootDirs 不仅是指定一组物理源目录被合并到一个目录。其中的元素还可能包括任何数量的特别的目录名,不管这些目录是否存在。这允许编译器实现一些

答案

答案 : TS 如何寻找相对路径

  1. /root/src/moduleB.ts`
  2. /root/src/moduleB.tsx
  3. /root/src/moduleB.d.ts
  4. /root/src/moduleB/package.json (if it specifies a "types" property)
  5. /root/src/moduleB/index.ts
  6. /root/src/moduleB/index.tsx
  7. /root/src/moduleB/index.d.ts

顺序 :

  1. ./moduleB.ext
  2. ./moduleB/main.ext
  3. ./moduleB/index.ext

答案 : 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 (if it specifies a "types" property)

  5. /root/src/node_modules/@types/moduleB.d.ts

  6. /root/src/node_modules/moduleB/index.ts

  7. /root/src/node_modules/moduleB/index.tsx

  8. /root/src/node_modules/moduleB/index.d.ts

  9. /root/node_modules/moduleB.ts

  10. /root/node_modules/moduleB.tsx

  11. /root/node_modules/moduleB.d.ts

  12. /root/node_modules/moduleB/package.json (if it specifies a "types" property)

  13. /root/node_modules/@types/moduleB.d.ts

  14. /root/node_modules/moduleB/index.ts

  15. /root/node_modules/moduleB/index.tsx

  16. /root/node_modules/moduleB/index.d.ts

  17. /node_modules/moduleB.ts

  18. /node_modules/moduleB.tsx

  19. /node_modules/moduleB.d.ts

  20. /node_modules/moduleB/package.json (if it specifies a "types" property)

  21. /node_modules/@types/moduleB.d.ts

  22. /node_modules/moduleB/index.ts

  23. /node_modules/moduleB/index.tsx

  24. /node_modules/moduleB/index.d.ts

顺序:

  1. ./node_modules/moduleB.ext
  2. ./node_modules/moduleB/main.ext
  3. ./node_modules/moduleB/index.ext
  4. 向上

答案:路径映射二次尝试

  1. pattern ’*’ is matched and wildcard captures the whole module name
  2. try first substitution in the list: ’*’ -> folder2/file3
  3. result of substitution is non-relative name - combine it with baseUrl -> projectRoot/folder2/file3.ts.
  4. File does not exist, move to the second substitution
  5. second substitution ‘generated/*’ -> generated/folder2/file3
  6. result of substitution is non-relative name - combine it with baseUrl -> projectRoot/generated/folder2/file3.ts.
  7. File exists. Done.
posted @ 2021-05-07 22:56  wkmcyz  阅读(195)  评论(0编辑  收藏  举报