跳至主要內容

模块解析

樱桃茶大约 26 分钟

模块解析

模块解析是 TypeScript 中的一个机制,用来确定当你在代码中使用import语句时,TypeScript 应该如何查找和识别模块。这个过程对于保证代码能正确地编译和运行至关重要。

  • 模块解析策略: TypeScript 提供两种模块解析策略:NodeClassic。默认情况下,如果你在 tsconfig.json 中指定了"module": "commonjs",就会使用 Node 解析策略;否则使用 Classic。大多数情况下,我们推荐使用 Node 解析策略,因为它更接近 Node.js 和 Webpack 等工具的工作方式。

  • Node 解析机制:

    • Node 解析机制试图模拟 Node.js 模块解析的方式。按照以下顺序查找模块:
      • 检查是否有相应的.ts.tsx文件。
      • 检查是否有相应的.d.ts类型声明文件。
      • 如果没有找到文件,但是有一个文件夹匹配模块名,那么 Node.js 会尝试找到该文件夹中的package.json文件并查找"main"字段指定的文件。没有"main"字段,则默认查找名为index.tsindex.d.ts的文件。
  • 路径映射:

    • 如果你的项目结构比较复杂,可能需要配置别名来简化模块的导入路径。可以在 tsconfig.json 中使用"paths"属性来设置这些别名。
    • 例如,可以将长路径简化成短路径或别名:
{
  "compilerOptions": {
    "baseUrl": ".", // 这里设置基准路径
    "paths": {
      "*": ["types/*"], // 所有模块都会被映射到types目录下查找
      "utils/*": ["src/utils/*"] // 可以直接通过 utils/ 别名引入src/utils下的文件
    }
  }
}
  • 根据baseUrlpaths配置解析:

    • 当设置了baseUrl后,非相对模块名的导入将相对于baseUrl解析。
    • paths允许你声明重写模块解析的路由。使用paths是管理大型应用程序中模块引用的一种强大方式。
  • 示例:

    • 假设有个文件math.ts位于项目的utils文件夹内,而你的baseUrl设置为src:
// math.ts 文件
export function add(a: number, b: number): number {
  return a + b;
}
  • tsconfig.json中配置paths后,你可以这样导入math.ts
// someOtherFile.ts 文件
import { add } from "utils/math";

let result = add(1, 2);
console.log(result); // 输出 3

理解模块解析可以帮助你组织和管理 TypeScript 项目中的文件引用,确保构建系统可以正确地找到和打包各个模块。

相对 vs. 非相对模块导入

模块解析中的“相对 vs. 非相对模块导入”指的是在 TypeScript 中导入模块时使用的路径格式。当你要导入一个模块时,可以用两种路径方式来指定你想要加载的模块:相对路径和非相对路径。

  • 相对导入:

    • 是以./../开头的。
    • 表明被导入的模块与当前文件有着直接的文件系统关系。
    • 主要用于你自己的项目中的模块或者当文件之间有明确的层级结构时。
    • 例如:
      • import { myFunction } from "./myModule"; // 同一目录下的文件
      • import { anotherFunction } from "../parentFolder/anotherModule"; // 父目录中的文件
  • 非相对导入:

    • 不以./../开头。
    • 通常用于加载 node_modules 中的库或者配置中设置的基础目录下的模块。
    • TypeScript 会根据tsconfig.json文件中的配置项来解析这些模块。
    • 例如:
      • import * as React from "react"; // 从 node_modules 中导入 React
      • import { Observable } from "rxjs"; // 从 node_modules 中导入 RxJS 中的 Observable

TypeScript 解析这两种模块路径的机制不同。对于相对导入,TypeScript 会按照相对路径寻找对应的文件。然而,对于非相对导入,TypeScript 需要根据配置文件(tsconfig.json)或者 Node.js 的解析策略来查找相应的模块。因此,理解这两者的区别对于组织项目结构和引用外部模块很重要。

举个例子,假设你有如下的项目结构:

project/
├── src/
│   ├── components/
│   │   └── button.tsx
│   └── index.ts
└── node_modules/
    ├── react/
    └── lodash/

button.tsx中,如果你想导入index.ts中的功能,你应该使用相对路径:

// 在src/components/button.tsx中
import { mainFeature } from "../index";

而如果你想在button.tsx中使用 React 或 Lodash 这样的第三方库,你应该使用非相对路径:

// 在src/components/button.tsx中
import React from "react";
import _ from "lodash";

通过理解并正确使用相对和非相对路径,你可以更合理地组织代码,并确保模块的正确导入。

模块解析策略

模块解析策略是 TypeScript 查找模块导入语句所指向文件的过程。当你使用 import 从另一个文件导入一些代码时,TypeScript 需要知道这个文件在哪里,以及如何加载它。

  • Node 解析策略:

    • 这种策略模仿了 Node.js 模块解析机制。
    • TypeScript 尝试找到 .ts, .tsx, 或 .d.ts 文件。
    • 如果直接给出文件名没有找到,TypeScript 会尝试添加这些扩展名。
    • 如果还是找不到,TypeScript 会查找同名的目录,并尝试加载该目录中的 index.tsindex.tsx
    • 示例:
      • import { example } from './example';
        • TypeScript 会按顺序搜索 example.ts, example.tsx, example.d.ts, example/index.ts, example/index.tsx
  • Classic 解析策略:

    • 主要用于兼容旧版 TypeScript。
    • TypeScript 仅查找相对或绝对路径下的 .ts.d.ts
    • 不会像 Node 策略那样自动寻找 index.ts
    • 这种策略不太常用并且不建议新项目中使用。
    • 示例:
      • import { example } from './example';
        • TypeScript 仅搜索 example.tsexample.d.ts

在项目的 tsconfig.json 文件中,可以通过设置 "moduleResolution" 字段为 "node""classic" 来选择使用哪种模块解析策略。通常情况下,默认的 Node 解析策略更适合现代 JavaScript 项目。

  • 在实际项目开发中,理解和正确配置模块解析对于编译器能否正确找到并包含项目中所有的模块非常重要。
  • 错误的模块解析配置可能导致编译错误,因为编译器无法找到导入的模块。
  • 为了遵循最佳实践,应该采用 Node 解析策略,并确保项目结构与 TypeScript 的期望相匹配。

Classic

模块解析策略中的 "Classic" 是 TypeScript 模块解析的一种方式。当 TypeScript 编译器尝试导入模块时,它会使用一种策略来找到对应的 .ts 文件。Classic 策略是比较早期和简单的一种解析方式。

  • 模块导入举例

    • 假设你有一个文件路径为 ./myModule.ts,并且在另一个文件中你写下了 import { myFunction } from './myModule'
    • 在 Classic 模块解析策略下,编译器会尝试按照以下顺序查找相应的 .ts 文件:
      1. 直接检查 ./myModule.ts 文件是否存在。
      2. 如果没有找到,那么编译器会检查 ./myModule.d.ts 是否存在。
  • 非相对导入举例

    • 对于不是以 "./" 或 "../" 开始的模块导入(即非相对模块导入),例如 import { myFunction } from 'myModule'
      1. 编译器首先尝试在 node_modules 目录中查找 myModule.tsmyModule.d.ts 文件。
      2. 如果没有找到,并且你的文件是在子目录中,编译器将转向父级目录继续查找。
  • 限制和不足

    • Classic 解析策略不考虑 node_modules 包中的依赖项,并且它可能会导致在有较为复杂项目结构时出现问题。
    • 因为这个原因,TypeScript 默认使用的是更现代的 "Node" 解析策略,它能够更好地与 Node.js 的模块解析机制兼容。
  • 实际选择

    • 通常情况下,不建议使用 Classic 解析策略,除非在处理老旧项目或者有特殊需求。
    • 新项目应该使用 Node 模块解析策略,它提供了对 npm 包管理和 Node.js 生态系统的直接支持。

Node

模块解析策略是 TypeScript 用来确定在你的代码中 import 语句应该如何解释的一套规则。当你使用 import 从一个模块中导入某些绑定时,TypeScript 需要知道这个模块文件的具体位置。

Node 模块解析策略是特定于 Node.js 运行时的解析机制。它遵循 CommonJS 模块规范。下面是这种策略的一些关键点:

  • 相对路径导入:当模块导入语句中的路径是相对路径(例如./module../module)时,TypeScript 会按照这个相对路径去查找.ts, .tsx.d.ts文件。

    // 导入同一目录下的模块
    import { myFunction } from "./myModule";
    
    // 导入父目录的模块
    import { myFunction } from "../myParentFolder/myModule";
    
  • 非相对路径导入:当模块导入语句中的路径不是相对路径时,例如import { myFunction } from "my-module";,Node 解析逻辑会尝试在项目的node_modules文件夹下寻找相应的模块。

    // 在node_modules中查找
    import { myFunction } from "my-module";
    
  • 解析顺序:Node.js 在查找模块时会先查找.js文件,如果不存在会继续查找.json.node文件。TypeScript 类似地会先查找.ts文件,然后是.tsx,最后是.d.ts

  • node_modules 跳转:如果在当前目录的node_modules中没有找到相应模块,Node 会向上级目录继续搜索,直至根目录下的node_modules为止。

  • package.json:在 node_modules 中的包通常会有一个package.json文件,它包含了一个main字段指定了模块的入口文件。TypeScript 也会参考这个字段,但它会优先查找"typings""types"字段指定的声明文件。

    // package.json中可能包含的字段
    {
      "name": "some-library",
      "version": "1.0.0",
      "main": "./lib/main.js",
      "typings": "./lib/main.d.ts"
    }
    
  • index.ts:如果导入的是一个目录,TypeScript 会尝试查找这个目录下的index.ts文件作为模块入口。

    // 将会查找 './some-folder/index.ts'
    import { myFunction } from "./some-folder";
    

理解模块解析策略对于管理和维护 TypeScript 项目中的依赖关系非常重要,确保无论是开发还是发布后的代码都能正确地找到并加载所需的模块。

Node.js 如何解析模块

在 Node.js 中,模块解析是指当你使用require或者在 TypeScript 里面使用import来加载其他 JavaScript 文件或模块时,Node.js 如何决定要加载哪个文件或模块。以下是 Node.js 解析模块的基本步骤和规则:

  • 如果你提供了一个相对路径或绝对路径,Node.js 会直接按照这个路径查找模块。
    • 例如,require('./myModule')会在当前目录下查找myModule.jsmyModule/index.js
  • 如果路径不包含文件扩展名,Node 会依次尝试添加.js.json.node扩展名来解析。
    • require('./myModule')可能会解析为./myModule.js
  • 如果你提供的是一个文件夹名称作为模块,Node.js 会查找该文件夹内的package.json文件,并尝试加载文件中指定的main字段对应的文件。
    • 如果文件夹/someFolder有一个package.json文件,且其 main 字段指定了lib/mainModule.js,那么require('/someFolder')将会解析并加载/someFolder/lib/mainModule.js
  • 如果没有package.json或没有main字段,Node.js 会尝试加载该文件夹下的index.jsindex.node
    • 所以require('/someFolder')也可能会解析为/someFolder/index.js

如果提供的是一个模块名而非路径,Node.js 将按照特定的顺序查找node_modules目录:

  • Node.js 会在当前文件所在目录开始,查找node_modules文件夹,尝试加载模块。
  • 如果没有找到,它会移动到父目录,再查找那里的node_modules文件夹。
  • 这个过程会一直持续,直至到达文件系统的根目录。

这就意味着你可以安装模块到项目的本地node_modules目录,并通过简单的模块名来引入它们,无需指定复杂的相对或绝对路径。

例如,如果你执行了npm install lodash,那么你可以在项目文件中通过require('lodash')来引入 lodash 模块。

理解模块解析机制对于有效地组织和引用项目中的代码模块至关重要。这也帮助你避免常见的错误,比如文件路径拼错导致的模块未找到问题。

TypeScript 如何解析模块

模块解析是指 TypeScript 如何查找模块的过程。当你在代码中使用importrequire关键字时,TypeScript 需要知道这个模块文件具体在哪里。下面解释了 TypeScript 在 Node 环境中是如何解析模块的:

  • TypeScript 模块解析遵循两种策略:NodeClassic。Node 策略与 Node.js 模块解析机制类似,是默认选项。
  • 当模块导入语句被执行时,例如import { something } from 'module-name';,TypeScript 会尝试找到'module-name'。
  • TypeScript 首先检查是否有module-name.tsmodule-name.d.ts文件。
  • 如果没有找到,TypeScript 会查找名为module-name/package.json的文件来定位主入口文件。检查这个 JSON 文件中的"types"字段(假设存在)以确定类型声明的位置。
  • 如果还没有找到,TypeScript 会尝试加载module-name/index.tsmodule-name/index.d.ts作为最后的手段。

以下是一些实际的例子:

// 假设我们有一个模块文件 "example.ts"
import { MyClass } from "./example";

// TypeScript会这样查找:
// 1. 尝试寻找 './example.ts' 或 './example.d.ts'
// 2. 如果上面的文件未找到,会尝试读取 './example/package.json'
// 3. 如果package.json中有"type"字段,根据字段信息查找声明文件
// 4. 如果没有"type"字段或者找不到声明文件,则尝试加载'./example/index.ts'或'./example/index.d.ts'

如果你配置了自定义的路径或基础目录,TypeScript 还会据此来解析模块。但记住,默认情况下,它会按照 Node.js 的模块解析方式来进行查找。通过了解这个过程,你能更好地理解如何组织你的 TypeScript 项目结构,并且确保你的模块可以正确地被解析和使用。

附加的模块解析标记

模块解析中的"附加的模块解析标记"指的是 TypeScript 编译器在尝试定位导入模块文件时使用的一些额外规则。这些规则定义了编译器如何根据模块导入语句找到相应的模块定义文件(比如.ts, .tsx, 或者.d.ts文件)。

  • 基础路径(Base URL): 可以设置一个基础目录,所有非相对模块导入都会以这个目录作为基准来解析。

    • // tsconfig.json中配置
      {
        "compilerOptions": {
          "baseUrl": "./src"
        }
      }
      // 这样任何非相对路径的导入都会从./src目录开始查找。
      import { ModuleA } from 'moduleA'; // 实际将从./src/moduleA导入
      
  • 路径映射(Path mapping): 用于设置别名来代替长的模块路径。

    • // tsconfig.json中配置
      {
        "compilerOptions": {
          "baseUrl": ".",
          "paths": {
            "*": ["types/*"],
            "example/*": ["./src/example/*"]
          }
        }
      }
      // 别名"*"将匹配所有模块,并在types目录下寻找相应类型定义
      // "example/*"将匹配所有以"example/"开头的模块路径,从"./src/example/"目录下寻找实际文件
      import { something } from 'example/something'; // 实际将从'./src/example/something'导入
      
  • 根目录(RootDirs): 允许你将多个目录当作一个合并后的目录。

    • // tsconfig.json中配置
      {
        "compilerOptions": {
          "rootDirs": [
            "src/views",
            "src/components"
          ]
        }
      }
      // 模块可以从"views"和"components"两个目录中被解析,就好像它们被合并在一起一样
      import { ViewComponent } from 'viewComponent'; // 可能从'src/views/viewComponent'或'src/components/viewComponent'中导入
      
  • 模块后缀名(Trailing "/" in module names): 在模块名后面加上"/",来表示导入的是一个目录而不是一个模块文件。

    • // 如果模块名称后加了"/",编译器会试图解析目录而非文件
      import * as utilities from "utilities/"; // 将尝试找到'utilities/index'或符合目录导入模式的文件
      

以上所述特性通过tsconfig.json文件进行配置,可以极大地自定义模块解析逻辑,使得项目结构更加灵活,同时在大型项目中管理路径变得更加容易。

Base URL

模块解析中的"Base URL"是 TypeScript 编译器在解析非相对模块导入时使用的一个基础路径。

  • 在 TypeScript 中,导入模块可以通过相对路径(如./module)或非相对路径(如module)来进行。相对路径相对于当前文件,而非相对路径通常需要配置额外信息才能正确解析。
  • 设置baseUrl是告诉 TypeScript 编译器在哪里查找模块。所有非相对模块导入都会被当作相对于这个baseUrl来解析。
  • baseUrl可以在tsconfig.json文件中的compilerOptions部分设置。

假设你的项目结构如下:

my-project
├── src
│   ├── utils.ts
│   └── main.ts
└── tsconfig.json

tsconfig.json中设置baseUrl:

{
  "compilerOptions": {
    "baseUrl": "./src"
  }
}

当在main.ts中导入utils.ts时,可以这样写:

import { someFunction } from "utils";

编译器会根据baseUrl查找到src/utils.ts文件。

如果没有设置baseUrl,则必须使用相对路径:

import { someFunction } from "./utils";

利用baseUrl可以简化模块的导入,使代码更清晰,尤其是在大型项目中,避免了复杂的相对路径导入。

路径映射

模块解析中的"路径映射"(Path Mapping)是 TypeScript 中一个非常有用的特性,它允许开发者通过别名来引用模块,这样可以避免在项目中使用长而复杂的相对或绝对文件路径。在tsconfig.json文件中配置路径映射,这通常会让代码更加整洁和易于管理。

以下是关于路径映射的一些关键点:

  • 配置: 在tsconfig.jsoncompilerOptions里设置baseUrlpaths属性。

    {
      "compilerOptions": {
        "baseUrl": ".", // 这里设置了基准目录
        "paths": {
          "mylib/*": ["some/path/*"] // 映射规则
        }
      }
    }
    

    在上面的例子中,任何导入模块路径以mylib/开始的,都会被重定向到some/path/目录下的相应模块。

  • 作用: 当通过指定的别名引用模块时,TypeScript 编译器会根据paths配置查找正确的文件。

    假设有以下的文件结构:

    projectRoot/
    ├─ some/
    │  ├─ path/
    │  │  ├─ myModule.ts
    ├─ tsconfig.json
    ├─ src/
    │  ├─ example.ts
    

    example.ts文件中,你可能想这样导入myModule.ts

    import { myFunction } from "mylib/myModule";
    

    因为设置了路径映射,TypeScript 知道去some/path/myModule.ts查找实际文件。

  • 好处: 它可以提升代码的可维护性,因为如果模块位置变了,你只需要更新tsconfig.json中的paths配置,而无需修改每一个引用该别名的文件。

  • 注意: 编译后的 JavaScript 可能不直接支持路径映射。通常需要通过构建工具(例如 Webpack)或运行时解析工具(如 Node.js 的module-alias)来正确地解析这些路径。

记住,路径映射仅改变 TypeScript 编译器如何解析导入路径,并不影响生成的 JS 代码中的模块解析逻辑。因此,为了确保编译后的代码能够正常工作,还可能需要相应的运行时模块解析支持。

利用 rootDirs 指定虚拟目录

模块解析中的 "利用 rootDirs 指定虚拟目录" 是 TypeScript 高级配置选项之一,它允许你将多个目录指定为相对导入时的根目录。这意味着可以把编译时存在于不同文件夹的文件当做是在同一个目录下。

使用 rootDirs 的好处是,在项目中组织代码时提供了更多的灵活性。比如,如果你有多个团队分别工作在不同的目录下,但构建系统会将它们视为同一个目录,就可以使用 rootDirs 来告诉 TypeScript 如何处理这些目录。

  • 当解析模块导入语句时,TypeScript 将查看 rootDirs 下所有的目录,尝试找到匹配的模块。
  • 这样可以创建出虚拟的目录结构,它在运行时由构建脚本或其它机制来实现。

以下是 rootDirs 的示例配置:

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

在这个例子中:

  • 如果你在代码中写了 import { MyComponent } from './myComponent',TypeScript 解析器将依次查找 src/views/myComponent.tssrc/components/myComponent.tsgenerated/templates/myComponent.ts
  • 如果找到了对应的文件,它就会使用该文件。
  • 实际上,虽然这些目录在文件系统中是分开的,但在 TypeScript 看来它们被视为一个合并的大目录。

通过这种方式,即使源代码物理上分散在不同的目录,也可以很方便地组织和引用模块。

跟踪模块解析

模块解析是 TypeScript 查找导入模块时所遵循的一系列步骤。当你在 TypeScript 文件中使用 import 语句导入一个模块时,TypeScript 需要确定这个模块文件的位置。"跟踪模块解析" 就是了解 TypeScript 如何查找和识别模块的过程。以下是对 "跟踪模块解析" 的解释:

  • 当你尝试导入一个模块时,例如通过 import { something } from 'some-module',TypeScript 将按照一定规则去寻找 'some-module'
  • TypeScript 先查看是否有相应的 .ts 文件或 .tsx (如果是在 React 环境下)。
  • 如果没有找到,它会尝试加载包含了模块声明的 .d.ts 类型定义文件。
  • TypeScript 还提供了基于 Node.js 模块解析策略和经典(Classic)解析策略两种模块解析方式。Node 解析模式是默认且常用的,它模仿了 Node.js 中的模块解析机制。

为了追踪模块解析过程,可以使用 TypeScript 编译器的 --traceResolution 标志。该标志将输出编译器尝试查找模块的详细信息,帮助开发者理解模块是如何被查找到的。这对于调试模块路径问题非常有用。

举例来说:

// someModule.ts
export const someVariable = "Hello World";

// index.ts
import { someVariable } from "./someModule";
console.log(someVariable);

运行 TypeScript 编译器时,使用 --traceResolution

tsc --traceResolution

编译器会显示有关 ./someModule 是如何被解析和查找的信息。

请注意,模块解析可能因配置不同而表现出不同的行为,如 tsconfig.json 中的 "baseUrl", "paths", "rootDirs" 等选项都会影响模块解析的结果。

需要留意的地方

模块解析是 TypeScript 查找模块导入语句对应的文件的过程。当你在 TypeScript 代码中使用import语句时,TypeScript 需要知道这个模块的具体位置。"跟踪模块解析"是指理解 TypeScript 是如何查找和识别这些模块的路径。

"需要留意的地方"可能指的是在配置或使用模块解析时,你应该注意的一些常见问题和最佳实践,例如:

  • 路径必须正确:导入的模块路径要与文件系统上的路径相匹配。大小写错误或拼写错误都可能会导致模块无法正确解析。

  • 文件扩展名:TypeScript 默认情况下会尝试解析.ts, .tsx.d.ts扩展名的文件。如果你尝试导入一个.js文件,可能需要特定的配置。

  • 基于 node 的解析策略:TypeScript 默认遵循 Node.js 的模块解析策略,即首先检查相应的文件,然后是带有index.ts的文件夹。

  • baseUrl 和 paths 配置:在tsconfig.json中可以设置baseUrl来改变非相对模块的解析基准路径,以及使用paths来设置导入路径的别名。

  • 类型声明文件(.d.ts):如果你正在使用 JavaScript 库,则可能需要引入类型声明文件,以便 TypeScript 了解库中对象的类型。

举例说明:

  • 如果你有以下导入语句 import { myFunction } from './utils';,TypeScript 将会寻找当前目录下名为utils.ts, utils.tsx, 或者utils.d.ts的文件。

  • 当配置了baseUrl: "./src"paths: { "@models/*": ["models/*"] },则导入语句import { User } from '@models/user';会被解析到./src/models/user.ts

理解这些细节将有助于你更好地组织代码和避免模块解析相关的错误。

使用--noResolve

模块解析中的 --noResolve 选项是 TypeScript 的一个编译选项,这个选项告诉 TypeScript 编译器在编译过程中不要执行模块的解析。以下是关于使用 --noResolve 的一些点:

  • 默认行为:通常,当你导入模块时(如使用 import { something } from './myModule';),TypeScript 编译器会尝试解析并查找这个模块的声明文件(例如 .ts, .tsx, 或者 .d.ts 文件)以了解模块的结构和类型信息。

  • 开启 --noResolve:如果你在编译时添加了 --noResolve 选项,编译器将不会尝试去解析或查找任何导入模块的文件。换句话说,编译器只处理当前文件所引用的文件,而不管这些文件中的任何 import 或者 <reference> 标签。

  • 影响

    • 如果有文件被排除在解析之外,那么它们的类型信息也不会被包含在编译结果中。
    • 这可能导致编译错误,因为编译器找不到导入模块的声明。
  • 例子

    // 假设我们有两个文件
    // fileA.ts
    export const numberA = 10;
    
    // fileB.ts
    import { numberA } from "./fileA";
    
    console.log(numberA);
    

    如果你使用 tsc --noResolve fileB.ts 去编译 fileB.ts,编译器将不会去查找 fileA.ts 的内容,因此无法知道 numberA 的类型或值,可能会导致编译错误。

  • 适用场景:该选项很少使用,但可能在某些特定的构建流程中有用,比如当你已经知道依赖的文件不需要再次解析,或者在进行增量构建时,在其他工具控制下确保文件解析的正确性。

总的来说,--noResolve 是一个高级选项,新手通常不需要使用它。大部分情况下,你应该让 TypeScript 自动解析模块,确保所有相关的类型信息都能正确地包含在最终的编译结果中。

app.ts

模块解析中的 使用 --noResolve 选项会影响 TypeScript 如何处理模块依赖关系。在 TypeScript 中,当你导入一个模块时,默认情况下编译器会试图解析这个模块的所有依赖项,这意味着编译器会查找并分析这些依赖项的导入语句。

  • 当你在命令行或者 tsconfig.json 设置了 --noResolve 选项后,TypeScript 编译器在处理导入模块时不再试图寻找和解析它的依赖项。

    {
      "compilerOptions": {
        "noResolve": true
      }
    }
    
  • 这意味着如果模块 A 导入了模块 B,而模块 B 又导入了模块 C,在开启 --noResolve 的情况下,编译器只处理模块 A 对模块 B 的直接导入,但不会处理模块 B 对模块 C 的导入。

  • 开启 --noResolve 选项之后,你需要确保通过其他方式(比如额外的脚本或者构建步骤)来处理模块间的依赖关系,因为 TypeScript 不会自动帮你解决它们。

  • 使用 --noResolve 的场景通常很少见,因为大多数项目希望编译器能够自动处理所有的依赖关系。但是,如果你有特定的构建过程,或者想要完全控制模块的解析流程,那么可以考虑使用这个选项。

例子: 假设有以下文件:

app.ts:

import { b } from "./moduleB";

moduleB.ts:

import { c } from "./moduleC";
export const b = c;

moduleC.ts:

export const c = "Hello World!";
  • 在正常情况下(没有 --noResolve),编译 app.ts 时,TypeScript 会解析 moduleB 和 moduleC 的依赖关系。

  • 如果使用 --noResolve 编译 app.ts,TypeScript 将只会处理对 moduleB.ts 的导入,并忽略 moduleB.ts 内部对 moduleC.ts 的导入。你将需要自己确保 moduleC.ts 被正确加载和处理。

常见问题

模块解析中的常见问题主要关注 TypeScript 如何寻找和识别模块文件。以下是一些基本概念和示例。

  • 相对与非相对模块导入

    • 相对导入是以 ./../ 开始的,用于引入项目内部的模块。
    • 非相对导入是其他形式的路径,用于引入外部依赖,如 import { something } from 'some-external-module'
  • 模块解析策略

    • TypeScript 提供两种模块解析策略:NodeClassic
    • Node 解析策略试图模拟 Node.js 模块解析机制,会查找 node_modules 文件夹和 package.json"main" 字段。
    • Classic 解析策略是 TypeScript 默认的老旧策略,不太常用,它向上级目录递归查找模块。
  • 问题解决示例

    • 如果你遇到一个模块不能正确解析的问题,首先确认你的导入语句是否正确指向了模块文件。
    • 确保文件扩展名正确。TypeScript 默认识别 .ts.tsx.d.ts
    • 查看 tsconfig.json 中的 "moduleResolution" 字段是否设置为你期望的解析策略(通常是 "node")。
    • 如果是在使用第三方库,确保该库的类型定义文件(如果有的话)已经被安装,并且 node_modules/@types 包含这些类型定义文件。
  • 路径映射

    • tsconfig.json 中使用 "baseUrl""paths" 可以设置复杂的模块导入路径映射。
    • 这对于大型应用中管理内部模块非常有用。
  • 跟踪模块解析

    • tsc --traceResolution 命令运行编译器,可以输出 TypeScript 是如何解析模块的详细信息,非常有助于调试问题。

下面是一些具体例子:

// 相对导入示例,假设文件结构如下:
// src/
//   moduleA.ts
//   folder/
//     moduleB.ts

// 在 moduleB.ts 中导入 moduleA.ts 的语法
import { something } from '../moduleA';

// 非相对导入示例,假定某个npm包已安装
import { somethingElse } from 'npm-package-name';

// tsconfig.json 中设置路径映射的示例:
{
  "compilerOptions": {
    "baseUrl": "./src", // 所有非相对模块导入都会基于这个基础路径
    "paths": {
      "utilities/*": ["utils/*"] // 这里指定了“utilities”开头的模块路径实际上映射到 "utils" 目录下
    }
  }
}

通过理解上述内容,新手可以更好地掌握 TypeScript 中模块的导入和解析,避免常见的模块解析错误,并有效地组织和管理项目中的模块。

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

在 TypeScript 中, exclude 配置选项允许你告诉 TypeScript 编译器忽略编译过程中的某些文件。然而,即使你将一些模块列入 exclude 列表,它们仍然可能会被编译器使用。这主要是因为下面的几个原因:

  • 引用解析: 如果项目中的其他没有被排除的文件引用了 exclude 列表中的模块,TypeScript 编译器还是会去处理这些模块。当一个模块被引用时,无论它是否被排除,TypeScript 都需要了解这个模块的类型信息来进行类型检查。

    例子: 假设你有以下文件结构:

    /src
      /excluded
        - excluded-file.ts
      - included-file.ts
    tsconfig.json
    

    tsconfig.json 中配置了:

    {
      "exclude": ["src/excluded"]
    }
    

    如果 included-file.ts 文件里导入了 excluded-file.ts

    import { someFunction } from "./excluded/excluded-file";
    

    即使 excluded-file.tsexclude 列表中,TypeScript 依然需要处理这个导入以确保类型正确。

  • filesinclude 配置: 如果你在 tsconfig.jsonfilesinclude 选项中明确包含了某些文件,那么即使这些文件也出现在了 exclude 列表中,它们仍会被编译器考虑进来。filesinclude 选项具有更高的优先级。

    例子: 继续上面的文件结构示例,在 tsconfig.json 中配置了:

    {
      "files": ["src/excluded/excluded-file.ts"],
      "exclude": ["src/excluded"]
    }
    

    这里虽然 excluded-file.ts 被排除了,但它又通过 files 明确地被包括进来,所以最终还是会被编译器处理。

  • 内部机制: TypeScript 编译器有自己的模块解析逻辑,这意味着如果 exclude 没有正确配置或存在模块解析路径的问题,编译器可能仍会包含不期望编译的文件。

要正确处理排除文件的情况,可以确保没有任何入口点或未被排除的文件引用那些排除的模块,并且 excludeincludefiles 之间没有冲突的配置。