模塊解析是指編譯器在查找導(dǎo)入模塊內(nèi)容時所遵循的流程。假設(shè)有一個導(dǎo)入語句 import { a } from "moduleA"; 為了去檢查任何對 a 的使用,編譯器需要準(zhǔn)確的知道它表示什么,并且需要檢查它的定義 moduleA。
這時候,編譯器會有個疑問“moduleA的結(jié)構(gòu)是怎樣的?” 這聽上去很簡單,但 moduleA可能在你寫的某個.ts/.tsx文件里或者在你的代碼所依賴的 .d.ts里。
模塊解析策略
typescript 有兩種模塊解析策略: Node 和 Classic. 通過下面的分析你會知道我更推進(jìn)使用 Node 策略, 因為他和我們熟悉的 nodejs 加載模塊的方式是一致的, 即通過模塊名字查找模塊時是從 node_modules 文件夾內(nèi)查找.
我們可以使用 --moduleResolution 標(biāo)記來指定使用哪種模塊解析策略,取值為 Node 或者 Classic。
若未指定moduleResolution(即默認(rèn)情況下),--module AMD | System | ES2015(ES6) 時 moduleResolution 的默認(rèn)值為 Classic,其它情況時則為 Node。
進(jìn)一步再來看看 module 的默認(rèn)取值情況, target === "ES6" ? "ES6" : "commonjs"即 --target為ES6 時, module 的默認(rèn)值為 ES6,其他情況默認(rèn)值為 commonjs. module 的所有取值 "None", "CommonJS", "AMD", "System", "UMD", "ES6"或 "ES2015"
再進(jìn)一步看看, target 的默認(rèn)值為 ES3, 其他取值有 "ES5", "ES6"/ "ES2015", "ES2016", "ES2017"或 "ESNext"。
建議: 顯示設(shè)置 moduleResolution 為 Node
Classic 策略
注意: 相對導(dǎo)入是以 /,./或../ 開頭的, 所有其它形式的導(dǎo)入被當(dāng)作非相對的.
相對路徑模塊導(dǎo)入
這種策略在以前是TypeScript默認(rèn)的解析策略。 現(xiàn)在,它存在的理由主要是為了向后兼容。
相對路徑導(dǎo)入的模塊是相對于導(dǎo)入它的文件進(jìn)行解析的。 因此 /root/src/folder/A.ts文件里的 import { b } from "./moduleB" 會使用下面的查找流程:
/root/src/folder/moduleB.ts/root/src/folder/moduleB.d.ts
非相對模塊導(dǎo)入
有一個對 moduleB 的非相對導(dǎo)入import { b } from "moduleB",它是在/root/src/folder/A.ts文件里,會以如下的方式來定位"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模塊解析機制。
TypeScript是模仿Node.js運行時的解析策略來在編譯階段定位模塊定義文件。 因此,TypeScript在Node解析邏輯基礎(chǔ)上增加了TypeScript源文件的擴展名(.ts,.tsx和.d.ts)。 同時,TypeScript在 package.json里使用字段"types"來表示類似"main"的意義。
相對模塊導(dǎo)入
比如,有一個導(dǎo)入語句 import { b } from "./moduleB" 在 /root/src/moduleA.ts 里,會以下面的流程來定位"./moduleB":
/root/src/moduleB.ts/root/src/moduleB.tsx/root/src/moduleB.d.ts/root/src/moduleB/package.json (如果指定了"types"屬性)/root/src/moduleB/index.ts/root/src/moduleB/index.tsx/root/src/moduleB/index.d.ts
回想一下Node.js先查找moduleB.js文件,然后是合適的package.json,再之后是index.js。
非相對導(dǎo)入(去node_modules) 內(nèi)查找
類似地,非相對的導(dǎo)入會遵循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/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/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/moduleB/index.ts/node_modules/moduleB/index.tsx/node_modules/moduleB/index.d.ts
不要被這里步驟的數(shù)量嚇到 - TypeScript只是在步驟(8)和(15)向上跳了兩次目錄。 這并不比Node.js里的流程復(fù)雜。
附加的模塊解析標(biāo)記(baseUrl與paths的配置)
有時工程源碼結(jié)構(gòu)與輸出結(jié)構(gòu)不同。 通常是要經(jīng)過一系統(tǒng)的構(gòu)建步驟最后生成輸出。 它們包括將 .ts編譯成.js,將不同位置的依賴拷貝至一個輸出位置。 最終結(jié)果就是運行時的模塊名與包含它們聲明的源文件里的模塊名不同。 或者最終輸出文件里的模塊路徑與編譯時的源文件路徑不同了。
TypeScript編譯器有一些額外的標(biāo)記用來通知編譯器在源碼編譯成最終輸出的過程中都發(fā)生了哪個轉(zhuǎn)換。
Base URL
在利用AMD模塊加載器的應(yīng)用里使用baseUrl是常見做法,它要求在運行時模塊都被放到了一個文件夾里。 這些模塊的源碼可以在不同的目錄下,但是構(gòu)建腳本會將它們集中到一起。
設(shè)置baseUrl來告訴編譯器到哪里去查找模塊。 所有非相對模塊導(dǎo)入都會被當(dāng)做相對于 baseUrl。
baseUrl的值由以下兩者之一決定:
- 命令行中baseUrl的值(如果給定的路徑是相對的,那么將相對于當(dāng)前路徑進(jìn)行計算)
-
tsconfig.json里的baseUrl屬性(如果給定的路徑是相對的,那么將相對于‘tsconfig.json’路徑進(jìn)行計算)
注意: 相對模塊的導(dǎo)入不會被設(shè)置的baseUrl所影響,因為它們總是相對于導(dǎo)入它們的文件。
路徑映射
有時模塊不是直接放在 baseUrl下面。 比如,充分 "jquery"模塊地導(dǎo)入,在運行時可能被解釋為"node_modules/jquery/dist/jquery.slim.min.js"。
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"進(jìn)行解析。 如果 "baseUrl"被設(shè)置成了除"."外的其它值,比如tsconfig.json所在的目錄,那么映射必須要做相應(yīng)的改變。 如果你在上例中設(shè)置了 "baseUrl": "./src",那么jquery應(yīng)該映射到"../node_modules/jquery/dist/jquery"。
通過"paths"我們還可以指定復(fù)雜的映射,包括指定多個回退位置。 假設(shè)在一個工程配置里,有一些模塊位于一處,而其它的則在另個的位置。 構(gòu)建過程會將它們集中至一處。 工程結(jié)構(gòu)可能如下:
projectRoot
├── folder1
│ ├── file1.ts (imports 'folder1/file2' and 'folder2/file3')
│ └── file2.ts
├── generated
│ ├── folder1
│ └── folder2
│ └── file3.ts
└── tsconfig.json
相應(yīng)的tsconfig.json文件如下:
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"*": [
"*",
"generated/*"
]
}
}
}
它告訴編譯器所有匹配"*"(所有的值)模式的模塊導(dǎo)入會在以下兩個位置查找:
-
"*":表示名字不發(fā)生改變,所以映射為<moduleName> => <baseUrl>/<moduleName> -
"generated/*"表示模塊名添加了“generated”前綴,所以映射為<moduleName> => <baseUrl>/generated/<moduleName>
按照這個邏輯,編譯器將會如下嘗試解析這兩個導(dǎo)入:
- 導(dǎo)入'folder1/file2'
- 匹配'*'模式且通配符捕獲到整個名字。
- 嘗試列表里的第一個替換:'*' -> folder1/file2。
- 替換結(jié)果為非相對名 - 與baseUrl合并 -> projectRoot/folder1/file2.ts。
- 文件存在。完成。
- 導(dǎo)入'folder2/file3'
- 匹配'*'模式且通配符捕獲到整個名字。
- 嘗試列表里的第一個替換:'*' -> folder2/file3。
- 替換結(jié)果為非相對名 - 與baseUrl合并 -> projectRoot/folder2/file3.ts。
- 文件不存在,跳到第二個替換。
- 第二個替換:'generated/*' -> generated/folder2/file3。
- 替換結(jié)果為非相對名 - 與baseUrl合并 -> projectRoot/generated/folder2/file3.ts。
- 文件存在。完成。