模块解析基础

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

相对与非相对模块导入

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

  • 相对引入(relative import)以 /, ./ or ../ 开头
  • 非相对引入non-relative例如:

    • import * as $ from "jquery";
    • import { Component } from "@angular/core";
  • 相对导入是相对于导入文件进行解析的,不能解析为环境模块声明。你应该对你自己的模块使用相对导入,这些模块保证在运行时维护它们的相对位置。

  • 非相对导入可以相对于baseUrl进行解析,也可以通过路径映射进行解析,我们将在下面介绍这一点。它们还可以解析为环境模块声明( ambient module declarations)。在导入任何外部依赖项时使用非相对路径。

模块解析策略

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

Note: node模块解析在TypeScript社区中是最常用的,并且被推荐用于大多数项目。如果你在TypeScript的导入和导出中遇到了解析问题,试着设置modulerresolve: “node”来看看它是否解决了这个问题。

Classic

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

Node

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

其他模块解析标志

Base URL

设置baseUrl来告诉编译器到哪里去查找模块。
baseUrl的值由以下两者之一决定:

  • 命令行中baseUrl的值(如果给定的路径是相对的,那么将相对于当前路径进行计算)
  • tsconfig.json里的baseUrl属性(如果给定的路径是相对的,那么将相对于tsconfig.json路径进行计算)

Path mapping 路径映射

有时模块不是直接放在baseUrl下面。 比如 "jquery"模块的导入,在运行时可能被解释为"node_modules/jquery/dist/jquery.slim.min.js"。 加载器使用映射配置来将模块名映射到运行时的文件,查看 RequireJs documentationSystemJS documentation
TypeScript编译器通过使用tsconfig.json文件里的"paths"来支持这样的声明映射。 下面是一个如何指定 jquery的”paths”的例子。

  1. {
  2. "compilerOptions": {
  3. "baseUrl": ".", // This must be specified if "paths" is.
  4. "paths": {
  5. "jquery": ["node_modules/jquery/dist/jquery"] // 此处映射是相对于"baseUrl"
  6. }
  7. }
  8. }

请注意"paths"是相对于"baseUrl"进行解析。 如果 "baseUrl"被设置成了除"."外的其它值,比如tsconfig.json所在的目录,那么映射必须要做相应的改变。
如果你在上例中设置了 “baseUrl": "./src",那么jquery应该映射到"../node_modules/jquery/dist/jquery"

通过"paths"我们还可以指定复杂的映射,包括指定多个回退位置。 假设在一个工程配置里,有一些模块位于一处,而其它的则在另个的位置。 构建过程会将它们集中至一处。 工程结构可能如下:

  1. projectRoot
  2. ├── folder1
  3. ├── file1.ts (imports 'folder1/file2' and 'folder2/file3')
  4. └── file2.ts
  5. ├── generated
  6. ├── folder1
  7. └── folder2
  8. └── file3.ts
  9. └── tsconfig.json
  1. {
  2. "compilerOptions": {
  3. "baseUrl": ".",
  4. "paths": {
  5. "*": ["*", "generated/*"]
  6. }
  7. }
  8. }

利用rootDirs指定虚拟目录

有时多个目录下的工程源文件在编译时会进行合并放在某个输出目录下。 这可以看做一些源目录创建了一个“虚拟”目录。
利用rootDirs,可以告诉编译器生成这个虚拟目录的roots; 因此编译器可以在“虚拟”目录下解析相对模块导入,就 好像 它们被合并在了一起一样。

设想这样一个国际化的场景,构建工具自动插入特定的路径记号来生成针对不同区域的捆绑,比如将#{locale}做为相对模块路径./#{locale}/messages的一部分。在这个假定的设置下,工具会枚举支持的区域,将抽像的路径映射成./zh/messages./de/messages等。

假设每个模块都会导出一个字符串的数组。比如./zh/messages可能包含:

  1. export default [
  2. "您好吗",
  3. "很高兴认识你"
  4. ];

利用rootDirs我们可以让编译器了解这个映射关系,从而也允许编译器能够安全地解析./#{locale}/messages,就算这个目录永远都不存在。比如,使用下面的tsconfig.json

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

编译器现在可以将import messages from './#{locale}/messages'解析为import messages from './zh/messages'用做工具支持的目的,并允许在开发时不必了解区域信息。

跟踪模块解析

通过 --traceResolution启用编译器的模块解析跟踪,它会告诉我们在模块解析过程中发生了什么。

使用--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,因为没有在命令行上传递。

模块解析配置项

allowSyntheticDefaultImports

设置为true,如果没有默认导出,babel会创建一个。

// allowSyntheticDefaultImports 设置为true 
// 可以这样引用react 
import React from "react";

// 来替代这样的写法:
import * as React from "react";

allowUmdGlobalAccess

当设置为true时,allowUmdGlobalAccess允许您从模块文件内部作为全局变量访问UMD导出。模块文件是具有导入和/或导出的文件。如果没有这个标志,使用UMD模块的导出需要一个导入声明。

这个标志的一个例子是,在一个web项目中,你知道特定的库(如jQuery或Lodash)在运行时总是可用的,但你不能通过导入访问它。

Released:3.5

// In TypeScript 3.5, you can now reference UMD global declarations like
export as namespace foo;

export as namespace foo;

baseUrl

项目下设置 “baseUrl”: “./“ , TypeScript 会找与 tsconfig.json 相同层级下的文件:

baseUrl
├── ex.ts
├── hello
│   └── world.ts
└── tsconfig.json
import { helloWorld } from "hello/world";
console.log(helloWorld);

esModuleInterop

  • 形如 import * as moment from "moment" 这样的命名空间导入等价于 const moment = require("moment")
  • 形如 import moment from "moment" 这样的默认导入等价于 const moment = require("moment").default

这种错误的行为导致了这两个问题:

  • ES6 模块规范规定,命名空间导入(import * as x)只能是一个对象。TypeScript 把它处理成 = require(“x”) 的行为允许把导入当作一个可调用的函数,这样不符合规范。
  • 虽然 TypeScript 准确实现了 ES6 模块规范,但是大多数使用 CommonJS/AMD/UMD 模块的库并没有像 TypeScript 那样严格遵守。

开启 esModuleInterop 选项将会修复 TypeScript 转译中的这两个问题

当启用 esModuleInterop 时,将同时启用 allowSyntheticDefaultImports

moduleResolution

指定模块解析的策略'node' (Node.js) 或 'classic' (used in TypeScript before the release of 1.6).
现代代码中,也许已经不需要使用'classic'

paths

相对于baseUrl的一系列重新映射:
设置paths,baseUrl必须指定

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

这样就可以使用 import "jquery" 的写法,并在本地获得所有正确的输入。

你可以告诉TypeScript文件解析器支持一些自定义前缀来查找代码。此模式可用于避免代码库中的长相对路径。

{
  "compilerOptions": {
    "baseUrl": "src",
    "paths": {
        "app/*": ["app/*"],
        "config/*": ["app/_config/*"],
        "environment/*": ["environments/*"],
        "shared/*": ["app/_shared/*"],
        "helpers/*": ["helpers/*"],
        "tests/*": ["tests/*"]
    },
}

preserveSymlinks

这是为了匹配 Node.js 中相同的选项,它不解析符号链接的真实路径。
这个选项也表现出与 Webpack 中 resolve.symlinks 选项相反的行为(即设置 TypeScript 的 preserveSymlinks 为 true, 与之对应的 Webpack 的 resolve.symlinks 为 false。反之亦然)

启用后,对于模块和包的引用(例如 import/// <reference type="..." /> 指令都相对于符号链接所在的位置进行解析,而不是相对于符号链接解析后的路径。

rootDirs

使用rootDirs,您可以通知编译器,有许多“虚拟”目录充当单个根目录。这允许编译器解析这些“虚拟”目录中的相对模块导入,就像它们被合并到一个目录中一样。

例如:

 src
 └── views
     └── view1.ts (可以引入 "./template1", "./view2`)
     └── view2.ts (可以引入 "./template1", "./view1`)

 generated
 └── templates
         └── views
             └── template1.ts (可以引入 "./view1", "./view2")
{
  "compilerOptions": {
    "rootDirs": ["src/views", "generated/templates/views"]
  }
}

rootDirs can be used to provide a separate “type layer” to files that are not TypeScript or JavaScript by providing a home for generated .d.ts files in another folder. This is technique is useful for bundled applications where you use import of files that aren’t necessarily code
这个配置可以使类型层 “type layer” 使用单独的文件夹,避免不必要的代码:

 src
 └── index.ts
 └── css
     └── main.css
     └── navigation.css
 generated
 └── css
     └── main.css.d.ts
     └── navigation.css.d.ts
{
  "compilerOptions": {
    "rootDirs": ["src", "generated"]
  }
}

例如 ./src/index.ts 可以引入文件 ./src/css/main.css , TypeScript 也能感应到generated下的声明文件:
疑问:不是 import { appClass } from ".css/main.css"; ?

// @filename: index.ts
import { appClass } from "./main.css";

typeRoots

默认情况下,编译中包含所有可见的“@types”包。任何包含在node_modules/@types中的包都被认为是可见的。
例如,这意味着包在 ./node_modules/@types/../node_modules/@types/ 以此类推。

如果typeRoots 被指定,只有typeRoots下指定的包会被包含。例如:

{
  "compilerOptions": {
    "typeRoots": ["./typings", "./vendor/types"]
  }
}

这个配置只包含./typings./vendor/types 下的包,不包括./node_modules/@types。所有的路径都是相对tsconfig.json的。

types

当 types 被指定,则只有列出的包才会被包含在全局范围内。例如:

{
  "compilerOptions": {
    "types": ["node", "jest", "express"]
  }
}

此配置置会包括./node_modules/@types/node, ./node_modules/@types/jest./node_modules/@types/express。其他 node_modules/@types/* 下的包不会被包括。

此功能与 typeRoots 不同的是,types只指定你想要包含的具体类型,而 typeRoots 支持你想要特定的文件夹。


参考: