TypeScript 模塊解析

2022-04-21 09:25 更新

這節(jié)假設你已經了解了模塊的一些基本知識 請閱讀 模塊文檔了解更多信息。

模塊解析就是指編譯器所要依據的一個流程,用它來找出某個導入操作所引用的具體值。 假設有一個導入語句import { a } from "moduleA"; 為了去檢查任何對 a的使用,編譯器需要準確的知道它表示什么,并且會需要檢查它的定義moduleA。

這時候,編譯器會想知道“moduleA的shape是怎樣的?” 這聽上去很簡單, moduleA可能在你寫的某個.ts/.tsx文件里或者在你的代碼所依賴的.d.ts里。

首先,編譯器會嘗試定位表示導入模塊的文件。 編譯會遵循下列二種策略之一: ClassicNode。 這些策略會告訴編譯器到 哪里去查找moduleA

如果它們失敗了并且如果模塊名是非相對的(且是在"moduleA"的情況下),編譯器會嘗試定位一個外部模塊聲明。 我們接下來會講到非相對導入。

最后,如果編譯器還是不能解析這個模塊,它會記錄一個錯誤。 在這種情況下,錯誤可能為 error TS2307: Cannot find module 'moduleA'.

相對 vs. 非相對模塊導入

根據模塊引用是相對的還是非相對的,模塊導入會以不同的方式解析。

相對導入是以/,./../開頭的。 下面是一些例子:

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

所有其它形式的導入被當作非相對的。 下面是一些例子:

  • import * as $ from "jQuery";
  • import { Component } from "angular2/core";

相對導入解析時是相對于導入它的文件來的,并且不能解析為一個外部模塊聲明。 你應該為你自己寫的模塊使用相對導入,這樣能確保它們在運行時的相對位置。

模塊解析策略

共有兩種可用的模塊解析策略:NodeClassic。 你可以使用 --moduleResolution標記為指定使用哪個。 默認值為 Node。

Classic

這種策略以前是TypeScript默認的解析策略。 現在,它存在的理由主要是為了向后兼容。

相對導入的模塊是相對于導入它的文件進行解析的。 因此 /root/src/folder/A.ts文件里的import { b } from "./moduleB"會使用下面的查找流程:

  1. /root/src/folder/moduleB.ts
  2. /root/src/folder/moduleB.d.ts

對于非相對模塊的導入,編譯器則會從包含導入文件的目錄開始依次向上級目錄遍歷,嘗試定位匹配的聲明文件。

比如:

有一個對moduleB的非相對導入import { b } from "moduleB",它是在/root/src/folder/A.ts文件里,會以如下的方式來定位"moduleB"

  1. /root/src/folder/moduleB.ts
  2. /root/src/folder/moduleB.d.ts
  3. /root/src/moduleB.ts
  4. /root/src/moduleB.d.ts
  5. /root/moduleB.ts
  6. /root/moduleB.d.ts
  7. /moduleB.ts
  8. /moduleB.d.ts

Node

這個解析策略試圖在運行時模仿Node.js模塊解析機制。 完整的Node.js解析算法可以在 Node.js module documentation找到。

Node.js如何解析模塊

為了理解TypeScript編譯依照的解析步驟,先弄明白Node.js模塊是非常重要的。 通常,在Node.js里導入是通過require函數調用進行的。 Node.js會根據 require的是相對路徑還是非相對路徑做出不同的行為。

相對路徑很簡單。 例如,假設有一個文件路徑為 /root/src/moduleA.js,包含了一個導入var x = require("./moduleB"); Node.js以下面的順序解析這個導入:

  1. /root/src/moduleB.js視為文件,檢查是否存在。

  2. /root/src/moduleB視為目錄,檢查是否它包含package.json文件并且其指定了一個"main"模塊。 在我們的例子里,如果Node.js發(fā)現文件 /root/src/moduleB/package.json包含了{ "main": "lib/mainModule.js" },那么Node.js會引用/root/src/moduleB/lib/mainModule.js。

  3. /root/src/moduleB視為目錄,檢查它是否包含index.js文件。 這個文件會被隱式地當作那個文件夾下的"main"模塊。

你可以閱讀Node.js文檔了解更多詳細信息:file modules 和 folder modules。

但是,非相對模塊名的解析是個完全不同的過程。 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文檔了解更多詳細信息:loading modules from node_modules。

TypeScript如何解析模塊

TypeScript是模仿Node.js運行時的解析策略來在編譯階段定位模塊定義文件。 因此,TypeScript在Node解析邏輯基礎上增加了TypeScript源文件的擴展名( .ts,.tsx.d.ts)。 同時,TypeScript在 package.json里使用字段"typings"來表示類似"main"的意義 - 編譯器會使用它來找到要使用的"main"定義文件。

比如,有一個導入語句import { b } from "./moduleB"/root/src/moduleA.ts里,會以下面的流程來定位"./moduleB"

  1. /root/src/moduleB.ts
  2. /root/src/moduleB.tsx
  3. /root/src/moduleB.d.ts
  4. /root/src/moduleB/package.json (如果指定了"typings"屬性)
  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的解析邏輯,首先查找文件,然后是合適的文件夾。 因此/src/moduleA.ts文件里的import { b } from "moduleB"會以下面的查找順序解析:

  1. /root/src/node_modules/moduleB.ts
  2. /root/src/node_modules/moduleB.tsx
  3. /root/src/node_modules/moduleB.d.ts
  4. /root/src/node_modules/moduleB/package.json (如果指定了"typings"屬性)
  5. /root/src/node_modules/moduleB/index.ts
  6. /root/src/node_modules/moduleB/index.tsx
  7. /root/src/node_modules/moduleB/index.d.ts 

  8. /root/node_modules/moduleB.ts
  9. /root/node_modules/moduleB.tsx
  10. /root/node_modules/moduleB.d.ts
  11. /root/node_modules/moduleB/package.json (如果指定了"typings"屬性)
  12. /root/node_modules/moduleB/index.ts
  13. /root/node_modules/moduleB/index.tsx
  14. /root/node_modules/moduleB/index.d.ts 

  15. /node_modules/moduleB.ts
  16. /node_modules/moduleB.tsx
  17. /node_modules/moduleB.d.ts
  18. /node_modules/moduleB/package.json (如果指定了"typings"屬性)
  19. /node_modules/moduleB/index.ts
  20. /node_modules/moduleB/index.tsx
  21. /node_modules/moduleB/index.d.ts

不要被這里步驟的數量嚇到 - TypeScript只是在步驟(8)和(15)向上跳了兩次目錄。 這并不比Node.js里的流程復雜。

使用--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="..." />指令的文件。


以上內容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號