天天看點

TypeScript 裡的 module 解析過程 - Module Resolution

Module Resolution

子產品解析是編譯器用來确定導入所指内容的過程。考慮像 import { a } from "moduleA"; 這樣的導入語句。為了檢查 a 的任何使用,編譯器需要确切地知道它代表什麼,并且需要檢查它的定義 moduleA。

此時,編譯器會問“moduleA 的形狀是什麼?”雖然這聽起來很簡單,但 moduleA 可以在您自己的 .ts/.tsx 檔案之一中定義,或者在您的代碼所依賴的 .d.ts 中定義。

首先,編譯器會嘗試定位一個代表導入子產品的檔案。為此,編譯器遵循兩種不同政策之一:classical 或 Node。這些政策告訴編譯器去哪裡尋找 moduleA。

如果這不起作用并且子產品名稱是非相關的(在“moduleA”的情況下,它是),那麼編譯器将嘗試定位一個環境子產品聲明。接下來我們将介紹非相對導入。

最後,如果編譯器無法解析子產品,它将記錄一個錯誤。在這種情況下,錯誤類似于錯誤 TS2307:找不到子產品 'moduleA'。

Relative vs. Non-relative module imports

根據子產品引用是相對的還是非相對的,子產品導入的解析方式不同。

相對導入是以 /、./ 或 ../ 開頭的導入。 一些例子包括:

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

任何其他導入都被認為是非相關的。 一些例子包括:

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

相對導入是相對于導入檔案解析的,無法解析為環境子產品聲明。 您應該對自己的子產品使用相對導入,以保證在運作時保持其相對位置。

Module Resolution Strategies

有兩種可能的子產品解析政策:Node 和 Classic。 您可以使用 --moduleResolution 标志來指定子產品解析政策。 如果未指定,則 --module commonjs 預設為 Node,否則預設為 Classic(包括 --module 設定為 amd、system、umd、es2015、esnext 等時)。

注意:Node 子產品解析是 TypeScript 社群中最常用的,推薦用于大多數項目。 如果您在 TypeScript 中遇到導入和導出的解析問題,請嘗試設定 moduleResolution: "node" 以檢視它是否解決了問題。

Classical 解析政策

這曾經是 TypeScript 的預設解析政策。 如今,這種政策主要是為了向後相容。

相對導入将相對于導入檔案進行解析。 是以,在源檔案 /root/src/folder/A.ts 中 import { b } from "./moduleB" 将導緻以下查找:

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

然而,對于非相關子產品導入,編譯器從包含導入檔案的目錄開始沿着目錄樹向上走,試圖找到比對的定義檔案。

例如:

在源檔案 /root/src/folder/A.ts 中,對 moduleB 的非相對導入,例如 import { b } from "moduleB",将導緻嘗試使用以下位置來定位 "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 子產品解析機制。 Node.js 子產品文檔中概述了完整的 Node.js 解析算法。

Node.js 如何解析子產品?

要了解 TS 編譯器将遵循哪些步驟,了解 Node.js 子產品非常重要。傳統上,Node.js 中的導入是通過調用名為 require 的函數來執行的。 Node.js 采取的行為将根據 require 是相對路徑還是非相對路徑而有所不同。

相對路徑相當簡單。例如,讓我們考慮一個位于 /root/src/moduleA.js 的檔案,其中包含 import var x = require("./moduleB"); Node.js 按以下順序解析該導入:

  • 詢問名為 /root/src/moduleB.js 的檔案是否存在。
  • 詢問檔案夾 /root/src/moduleB 是否包含指定“主”子產品的名為 package.json 的檔案。在我們的示例中,如果 Node.js 發現檔案 /root/src/moduleB/package.json 包含 { "main": "lib/mainModule.js" },那麼 Node.js 将引用 /root/src/moduleB/lib/mainModule.js。
  • 詢問檔案夾 /root/src/moduleB 是否包含名為 index.js 的檔案。該檔案被隐式視為該檔案夾的“主”子產品。

但是,對非相關子產品名稱的解析以不同的方式執行。 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(如果它指定了“main”屬性)

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

(4) /root/node_modules/moduleB.js

(5) /root/node_modules/moduleB/package.json(如果它指定了“main”屬性)

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

(7) /node_modules/moduleB.js

(8) /node_modules/moduleB/package.json(如果它指定了“main”屬性)

(9) /node_modules/moduleB/index.js

請注意,Node.js 在步驟 (4) 和 (7) 中跳轉了一個目錄。

您可以在 Node.js 文檔中閱讀有關從 node_modules 加載子產品的更多資訊。

How TypeScript resolves modules

TypeScript 将模仿 Node.js 運作時解析政策,以便在編譯時定位子產品的定義檔案。為此,TypeScript 在 Node 的解析邏輯上覆寫了 TypeScript 源檔案擴充名(.ts、.tsx 和 .d.ts)。 TypeScript 還将使用 package.json 中名為“types”的字段來反映“main”的用途——編譯器将使用它來查找要查閱的“main”定義檔案。

例如,像 /root/src/moduleA.ts 中的 import { b } from "./moduleB" 這樣的導入語句将導緻嘗試以下位置來定位 "./moduleB":

(1)/root/src/moduleB.ts

(2)/root/src/moduleB.tsx

(3)/root/src/moduleB.d.ts

(4)/root/src/moduleB/package.json(如果它指定了“types”屬性)

(5)/root/src/moduleB/index.ts

(6)/root/src/moduleB/index.tsx

(7)/root/src/moduleB/index.d.ts

回想一下,Node.js 查找名為 moduleB.js 的檔案,然後是适用的 package.json,然後是 index.js。

同樣,非相對導入将遵循 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/@types/moduleB.d.ts
/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/@types/moduleB.d.ts
/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/@types/moduleB.d.ts
/node_modules/moduleB/index.ts
/node_modules/moduleB/index.tsx
/node_modules/moduleB/index.d.ts
           

不要被這裡的步驟數吓倒——TypeScript 仍然隻在步驟 (9) 和 (17) 中兩次跳轉目錄。這實際上并不比 Node.js 本身所做的更複雜。

Additional module resolution flags

項目源布局有時與輸出布局不比對。 通常一組建構步驟會生成最終輸出。 其中包括将 .ts 檔案編譯為 .js,以及将依賴項從不同的源位置複制到單個輸出位置。 最終結果是子產品在運作時的名稱可能與包含其定義的源檔案的名稱不同。 或者最終輸出中的子產品路徑可能在編譯時與其對應的源檔案路徑不比對。

TypeScript 編譯器有一組額外的标志來通知編譯器預期發生在源上的轉換以生成最終輸出。

需要注意的是,編譯器不會執行任何這些轉換; 它隻是使用這些資訊來指導将子產品導入解析到其定義檔案的過程。

Base Url

在使用 AMD 子產品加載器的應用程式中,使用 baseUrl 是一種常見做法,其中子產品在運作時“部署”到單個檔案夾。 這些子產品的源代碼可以位于不同的目錄中,但是建構腳本會将它們放在一起。

設定 baseUrl 通知編譯器在哪裡可以找到子產品。 假定所有具有非相對名稱的子產品導入都與 baseUrl 相關。

baseUrl 的值确定為:

(1)baseUrl 指令行參數的值(如果給定的路徑是相對的,則根據目前目錄計算)

(2)'tsconfig.json' 中 baseUrl 屬性的值(如果給定的路徑是相對的,則根據 'tsconfig.json' 的位置計算)

請注意,相對子產品導入不會受到設定 baseUrl 的影響,因為它們總是相對于它們的導入檔案進行解析。

您可以在 RequireJS 和 SystemJS 文檔中找到有關 baseUrl 的更多文檔。

path mapping

有時子產品并不直接位于 baseUrl 下。例如,對子產品“jquery”的導入将在運作時轉換為“node_modules/jquery/dist/jquery.slim.min.js”。加載器使用映射配置在運作時将子產品名稱映射到檔案,請參閱 RequireJs 文檔和 SystemJS 文檔。

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

如何檢查 TypeScript 子產品解析過程

如前所述,編譯器在解析子產品時可以通路目前檔案夾之外的檔案。 在診斷子產品未解析的原因或解析為不正确的定義時,這可能很困難。 使用 --traceResolution 啟用編譯器子產品解析跟蹤可以深入了解子產品解析過程中發生的情況。

假設我們有一個使用 typescript 子產品的示例應用程式。

app.ts has an import like import * as ts from "typescript".

│   tsconfig.json
├───node_modules
│   └───typescript
│       └───lib
│               typescript.d.ts
└───src
        app.ts
           

使用如下指令行編譯:

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’.

Loading of types from npm packages: