今天看啥  ›  专栏  ›  2o壹9

TypeScript——模块解析(2)

2o壹9  · 简书  ·  · 2019-12-17 09:41

路径映射

有时模块不是直接放在baseUrl下面。 比如,充分 "jquery"模块地导入,在运行时可能被解释为"node_modules/jquery/dist/jquery.slim.min.js"。 加载器使用映射配置来将模块名映射到运行时的文件,查看 RequireJs documentation和SystemJS documentation。

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

]

}

}

}

它告诉编译器所有匹配"*"(所有的值)模式的模块导入会在以下两个位置查找:

"*": 表示名字不发生改变,所以映射为<moduleName> => <baseUrl>/<moduleName>

"generated/*"表示模块名添加了“generated”前缀,所以映射为<moduleName> => <baseUrl>/generated/<moduleName>

按照这个逻辑,编译器将会如下尝试解析这两个导入:

导入'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。

文件存在。完成。

利用rootDirs指定虚拟目录

有时多个目录下的工程源文件在编译时会进行合并放在某个输出目录下。 这可以看做一些源目录创建了一个“虚拟”目录。

利用rootDirs,可以告诉编译器生成这个虚拟目录的roots; 因此编译器可以在“虚拟”目录下解析相对模块导入,就 好像它们被合并在了一起一样。

比如,有下面的工程结构:

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"指定了一个roots列表,列表里的内容会在运行时被合并。 因此,针对这个例子, tsconfig.json如下:

{

"compilerOptions": {

"rootDirs": [

"src/views",

"generated/templates/views"

]

}

}

每当编译器在某一rootDirs的子目录下发现了相对模块导入,它就会尝试从每一个rootDirs中导入。

rootDirs的灵活性不仅仅局限于其指定了要在逻辑上合并的物理目录列表。它提供的数组可以包含任意数量的任何名字的目录,不论它们是否存在。这允许编译器以类型安全的方式处理复杂捆绑(bundles)和运行时的特性,比如条件引入和工程特定的加载器插件。

设想这样一个国际化的场景,构建工具自动插入特定的路径记号来生成针对不同区域的捆绑,比如将#{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'. ========

需要留意的地方

导入的名字及位置

======== Resolving module 'typescript' from 'src/app.ts'. ========

编译器使用的策略

Module resolution kind is not specified, using 'NodeJs'.

从npm加载types

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

使用--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

使用--noResolve编译app.ts:

可能正确找到moduleA,因为它在命令行上指定了。

找不到moduleB,因为没有在命令行上传递。

常见问题

为什么在exclude列表里的模块还会被编译器使用

tsconfig.json将文件夹转变一个“工程” 如果不指定任何 “exclude”或“files”,文件夹里的所有文件包括tsconfig.json和所有的子目录都会在编译列表里。 如果你想利用 “exclude”排除某些文件,甚至你想指定所有要编译的文件列表,请使用“files”。

有些是被tsconfig.json自动加入的。 它不会涉及到上面讨论的模块解析。 如果编译器识别出一个文件是模块导入目标,它就会加到编译列表里,不管它是否被排除了。

因此,要从编译列表中排除一个文件,你需要在排除它的同时,还要排除所有对它进行import或使用了/// <reference path="..." />指令的文件。




原文地址:访问原文地址
快照地址: 访问文章快照