模块解析是指编译器在查找导入模块内容时所遵循的流程。假设有一个导入语句 import { a } from "moduleA";
为了去检查任何对 a
的使用,编译器需要准确的知道它表示什么,并且需要检查它的定义 moduleA
。
这时候,编译器会有个疑问“moduleA的结构是怎样的?” 这听上去很简单,但 moduleA可能在你写的某个.ts/.tsx文件里或者在你的代码所依赖的 .d.ts
里。
模块解析策略
typescript
有两种模块解析策略: Node
和 Classic
. 通过下面的分析你会知道我更推进使用 Node
策略, 因为他和我们熟悉的 nodejs
加载模块的方式是一致的, 即通过模块名字查找模块时是从 node_modules
文件夹内查找.
我们可以使用 --moduleResolution
标记来指定使用哪种模块解析策略,取值为 Node
或者 Classic
。
若未指定moduleResolution
(即默认情况下),--module AMD | System | ES2015(ES6)
时 moduleResolution
的默认值为 Classic
,其它情况时则为 Node
。
进一步再来看看 module
的默认取值情况, target === "ES6" ? "ES6" : "commonjs"
即 --target
为ES6
时, module
的默认值为 ES6
,其他情况默认值为 commonjs
. module
的所有取值 "None", "CommonJS", "AMD", "System", "UMD", "ES6"或 "ES2015"
再进一步看看, target
的默认值为 ES3
, 其他取值有 "ES5", "ES6"/ "ES2015", "ES2016", "ES2017"或 "ESNext"。
建议: 显示设置 moduleResolution
为 Node
Classic
策略
注意: 相对导入是以 /,./或../
开头的, 所有其它形式的导入被当作非相对的.
相对路径模块导入
这种策略在以前是TypeScript默认的解析策略。 现在,它存在的理由主要是为了向后兼容。
相对路径导入的模块是相对于导入它的文件进行解析的。 因此 /root/src/folder/A.ts
文件里的 import { b } from "./moduleB"
会使用下面的查找流程:
/root/src/folder/moduleB.ts
/root/src/folder/moduleB.d.ts
非相对模块导入
有一个对 moduleB
的非相对导入import { b } from "moduleB"
,它是在/root/src/folder/A.ts
文件里,会以如下的方式来定位"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
模块解析机制。
TypeScript
是模仿Node.js
运行时的解析策略来在编译阶段定位模块定义文件。 因此,TypeScript
在Node
解析逻辑基础上增加了TypeScript
源文件的扩展名(.ts,.tsx和.d.ts
)。 同时,TypeScript
在 package.json
里使用字段"types"
来表示类似"main"
的意义。
相对模块导入
比如,有一个导入语句 import { b } from "./moduleB"
在 /root/src/moduleA.ts
里,会以下面的流程来定位"./moduleB"
:
/root/src/moduleB.ts
/root/src/moduleB.tsx
/root/src/moduleB.d.ts
/root/src/moduleB/package.json (如果指定了"types"属性)
/root/src/moduleB/index.ts
/root/src/moduleB/index.tsx
/root/src/moduleB/index.d.ts
回想一下Node.js
先查找moduleB.js
文件,然后是合适的package.json
,再之后是index.js
。
非相对导入(去node_modules) 内查找
类似地,非相对的导入会遵循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/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/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/moduleB/index.ts
/node_modules/moduleB/index.tsx
/node_modules/moduleB/index.d.ts
不要被这里步骤的数量吓到 - TypeScript
只是在步骤(8)和(15)向上跳了两次目录。 这并不比Node.js
里的流程复杂。
附加的模块解析标记(baseUrl与paths的配置)
有时工程源码结构与输出结构不同。 通常是要经过一系统的构建步骤最后生成输出。 它们包括将 .ts
编译成.js
,将不同位置的依赖拷贝至一个输出位置。 最终结果就是运行时的模块名与包含它们声明的源文件里的模块名不同。 或者最终输出文件里的模块路径与编译时的源文件路径不同了。
TypeScript
编译器有一些额外的标记用来通知编译器在源码编译成最终输出的过程中都发生了哪个转换。
Base URL
在利用AMD
模块加载器的应用里使用baseUrl
是常见做法,它要求在运行时模块都被放到了一个文件夹里。 这些模块的源码可以在不同的目录下,但是构建脚本会将它们集中到一起。
设置baseUrl
来告诉编译器到哪里去查找模块。 所有非相对模块导入都会被当做相对于 baseUrl
。
baseUrl
的值由以下两者之一决定:
- 命令行中baseUrl的值(如果给定的路径是相对的,那么将相对于当前路径进行计算)
-
tsconfig.json
里的baseUrl
属性(如果给定的路径是相对的,那么将相对于‘tsconfig.json’路径进行计算)
注意: 相对模块的导入不会被设置的baseUrl
所影响,因为它们总是相对于导入它们的文件。
路径映射
有时模块不是直接放在 baseUrl
下面。 比如,充分 "jquery"模块地导入,在运行时可能被解释为"node_modules/jquery/dist/jquery.slim.min.js"。
TypeScript
编译器通过使用tsconfig.json
文件里的"paths"
来支持这样的声明映射。 下面是一个如何指定 jquery
的"paths"
的例子。
{
"compilerOptions": {
"baseUrl": ".", // This must be specified if "paths" is.
"paths": {
"jquery": ["node_modules/jquery/dist/jquery"] // 此处映射是相对于"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/*"
]
}
}
}
它告诉编译器所有匹配"*"
(所有的值)模式的模块导入会在以下两个位置查找:
-
"*"
:表示名字不发生改变,所以映射为=> / -
"generated/*"
表示模块名添加了“generated”
前缀,所以映射为=> /generated/
按照这个逻辑,编译器将会如下尝试解析这两个导入:
- 导入'folder1/file2'
- 匹配'*'模式且通配符捕获到整个名字。
- 尝试列表里的第一个替换:'*' -> folder1/file2。
- 替换结果为非相对名 - 与baseUrl合并 -> projectRoot/folder1/file2.ts。
- 文件存在。完成。
- 导入'folder2/file3'
- 匹配'*'模式且通配符捕获到整个名字。
- 尝试列表里的第一个替换:'*' -> folder2/file3。
- 替换结果为非相对名 - 与baseUrl合并 -> projectRoot/folder2/file3.ts。
- 文件不存在,跳到第二个替换。
- 第二个替换:'generated/*' -> generated/folder2/file3。
- 替换结果为非相对名 - 与baseUrl合并 -> projectRoot/generated/folder2/file3.ts。
- 文件存在。完成。