typescript路徑別名問題詳解與前世今生的故事
前言
最近在 typescript 項目上踩了不少坑,打算寫幾篇文章記錄一下。
本篇文章就來梳理一下 ts 的相關技術(shù)棧 tsc、ts-node、ts-node-dev 中的路徑別名問題,從開發(fā)到打包階,不僅告訴你坑在哪,怎么解決,還會還會告訴你為什么會有這個坑以及坑背后的故事。
開始探索
本篇文章會以一個空項目開始,由淺入深的告訴你會遇到的各種問題,有興趣的同學可以跟著往下做一下:
首先找一個空文件夾,請出本文的核心嘉賓 ts-node:
npm install ts-node typescript
然后完善一下項目,添加一個相當基礎的 tsconfig.json:
{
"compilerOptions": {
"outDir": "dist",
"skipLibCheck": true,
"strict": true,
"noEmit": false,
"module": "CommonJS",
"baseUrl": ".",
"paths": {
"@/*": ["./*"]
}
},
"include": [ "**/*.ts"],
}然后在 package.json 里加入我們的 ts-node 執(zhí)行命令,執(zhí)行后 ts-node 就會去讀 index.ts 并執(zhí)行:
"scripts": {
"dev": "ts-node --files ./index.ts"
}
這樣一個 typescript 開發(fā)環(huán)境就準備就緒了,然后我們新建 index.ts 和 utils.ts:
utils.ts
export const plus = (a: number, b: number): number => a + b;
index.ts
import { plus } from "./utils";
console.log(plus(1, 1))
這些代碼很簡單,導出了一個加法函數(shù),然后在 index.ts 里引用并打印了出來,現(xiàn)在我們執(zhí)行 npm run dev 就可以看到如下結(jié)果:

沒毛病,一切正常,現(xiàn)在我們通過路徑別名引入 utils:
// 把 utils 的引入換成了 @ 路徑別名
import { plus } from "@/utils";
然后再來執(zhí)行一下:

出問題了,找不到對應的模塊,這個問題其實不在 ts-node,而是因為 tsc 在編譯代碼時不會去把路徑別名替換成對應的相對路徑,所以 ts-node 用 tsc 編譯完然后轉(zhuǎn)交給 node 執(zhí)行的時候自然就找不到 @/ 這個目錄了。
解決起來很簡單,只需要安裝一個包:
npm install tsconfig-paths
然后在 package.json 里改一下 dev 命令即可,這里的 -r 實際上是 node 的命令行參數(shù),有興趣的可以去看一下這個:Command-line API | Node.js v16.15.1 Documentation (nodejs.org):
"dev": "ts-node -r tsconfig-paths/register --files ./index.ts"
然后再執(zhí)行就發(fā)現(xiàn)正常了:

ts-node-dev 使用路徑別名
很多同學在開發(fā) ts 項目的時候都是使用 nodemon 監(jiān)聽文件變更,然后使用 ts-node 執(zhí)行代碼。比如這樣:
"dev": "nodemon -e ts --exec ts-node -r tsconfig-paths/register --files ./index.ts"
不過我們可以用一個更簡單方便的工具來完成這個操作,那就是 ts-node-dev,npm install ts-node-dev 安裝好之后,我們就可以把上面這行改寫成這樣:
"dev": "tsnd -r tsconfig-paths/register --respawn ./index.ts"
官方文檔中也提到:
So you just combine node-dev and ts-node options (see docs of those packages)
所以我們可以和 ts-node 一樣用相同的方法解決路徑別名問題。
而且除了看起來更簡潔外,ts-node-dev 還有個好處就是 它會緩存 tsc 的編譯過程,所以熱更新速度比 nodemon + ts-node 快了很多。非常推薦大家試用一下。
關于路徑別名的打包問題
還沒完,我們現(xiàn)在只解決了開發(fā)時的問題。如果路徑別名是 tsc 負責的話,那么打包時也會遇到這個問題。現(xiàn)在我們就來看一下打包時會怎樣。
首先,在命令行執(zhí)行 npx tsc,然后去 ./dist/index.js 里看一下打包后的成果:

果然,tsc 沒有把我們的路徑別名轉(zhuǎn)換成實際的相對路徑。那么執(zhí)行 node ./dist/index.js 時肯定會報錯找不到 @/utils,所以該如何解決這個問題呢?
一般來說有兩種方法,先說菜一點的,我們剛才提到 tsconfig-paths/register 實際上是用于 node 的一個包,那么這里自然也可以這么用,在 package.json 里添加如下命令(記得安裝 cross-env ):
"scripts": {
"start": "cross-env TS_NODE_BASEURL=./dist node -r tsconfig-paths/register .\\dist\\index.js"
}
然后再執(zhí)行就可以了:

簡單解釋一下,這里用環(huán)境變量 TS_NODE_BASEURL 覆蓋了 tsconfig.json 里的 baseurl,來讓查找路徑別名指向的目錄時可以找到正確的(編譯后)的文件。后面就是正常的 node -r 引用對應包然后執(zhí)行文件。
但是這種操作屬于“運行時”處理,相信很多人都會覺得不舒服。所以更簡單的方法是在編譯代碼時就一勞永逸的替換掉路徑別名,就是下面這種方法(推薦):
我們可以安裝 npm install tsc-alias 這個包,用于替換路徑別名,用法也很簡單,跟在 tsc 后面就可以了:
"scripts": {
"build": "tsc && tsc-alias"
}
現(xiàn)在執(zhí)行 npm run build 之后就可以看到路徑別名已經(jīng)被替換成相對路徑了:

當然,這個只是比較簡單的解決方式之一,你也可以根據(jù)項目的實際情況來,比如使用 webpack 或者 vite,又或者你可以看下文末參考小節(jié)里的第一個鏈接來了解更多的解決方法。
tsc 為什么不會轉(zhuǎn)換路徑別名?
文章寫到這里,我們已經(jīng)解決了 ts 項目中的路徑別名問題,但是相信很多人依舊有一個困惑:tsc 為什么不在編譯的時候直接把路徑別名替換掉?
在網(wǎng)上搜索相關的問題,能找到的大多都是如何解決這個問題,而對這個問題起因的解釋卻很少,我在搜索解決方法的時候也只看到有人說,這件事啊,說來話長了。

這不由得勾起了我的興趣,在詳細搜索之后,我找到了這篇討論,卻沒想到居然是一篇橫跨三年的故事。下面我們就大致了解一下這個問題的前世今生,如果你感興趣的話也可以直接去看一下:Module path maps are not resolved in emitted code · Issue #10866
故事要從 2016 年 9 月說起,當時的 typescript 還在 2.1 開發(fā)版。有人在試用了 tsconfig 中的 paths 配置時也遭遇了相同的困惑,他便提交了一個 issue 來詢問這個問題(事實上,最早的 issue 記錄可以追溯到 2016 年 7 月份,不過問題相同,這里不再贅述 )。

沒過兩天,有兩位 ts 貢獻者先后回復到:你有沒有同時在用一些 webpack 之類的打包工具?另一位則簡單介紹了這么做是 有意為之的,paths 配置項的本意是想為一些第三方包提供特殊的引入渠道,使得 ts 可以獲得更多類型信息,所以這不是個問題。

但是很顯然,圍觀者的 ?? 表明大家都不是很能接受這個觀點。
在解釋之后,貢獻者給本 issue 添加了 Working as Intended(按預期工作)標簽并把問題標記為完成,同時 issue 作者也表明可以理解,并善意的給后來者介紹了自己已經(jīng)通過 module-alias 解決了問題。
但是圍觀者顯然沒有被說服,大家開始陸續(xù)在這個 issue 下評論,一些人在介紹自己使用的解決辦法,一些人在挺 ts,但是更多的人在吐槽這個設計太糟糕了,自己實在想不明白,希望有人能解釋一下。
這個討論足足持續(xù)了兩年多,期間很多人 @ ts 貢獻者并表示已經(jīng)過去兩年時間了,希望可以重新考慮下這個令人困惑的設計。甚至有人在 reddit 上發(fā)了帖子:Path maps cannot be resolved by tsc / Works as intended - what a joke。
時間來到 2019 年 2 月份,issue 最開始回復的一位貢獻者站出來發(fā)出了一段長文進行了解釋。大致意思就是,ts 在編譯中修改路徑這些字面值是不切實際的。ts 的工作重點應該是如何更好的和其他更擅長此類工作的模塊相互協(xié)作,而不是為了支持更靈活的配置來自己實現(xiàn)這些功能,更別提可能會因此生成出“崩壞”的代碼。

同一天,ts 組成員 DanielRosenwasser 再次明確表示了:哪怕將來可能會優(yōu)化這里的開發(fā)體驗,ts 也不會修改用戶寫下的路徑。隨后便鎖定了這個問題。

至此,這個故事便畫上了句號。
現(xiàn)在了解了事情的前因后果,我們就可以來總結(jié)一下這個問題的根本原因了。
tsc 不轉(zhuǎn)換路徑別名的根本原因
站在開發(fā)者的角度看,我們希望可以用更少的工具完成更多的事情,這無可厚非。但是站在 ts 本身的角度看,typescript 只是開發(fā)階段使用的一個工具。它的核心目的也是優(yōu)化開發(fā)體驗,在開發(fā)時以強類型對代碼進行約束。換句話說就是 ts 表示我是負責開發(fā)的,打包工作不是我的強項,我大概編譯一下,去掉我的類型,剩下的打包工作交給更專業(yè)的工具來做。
所以我們可以把 tsc 編譯后的代碼看作是一種“中間產(chǎn)物”,typescript 期望用戶可以使用更貼近自己需求的工具將這些代碼二次編譯為實際的生產(chǎn)代碼。
現(xiàn)在我們回頭看一下 ts hankbook 中關于路徑別名的介紹:

可以看到,paths 配置項的本意是為了給一些模塊提供特殊的引入渠道。注意,此處引入的包只是你開發(fā)時需要的,而不一定是生產(chǎn)時使用的。
例如,我們可以在開發(fā)時(使用 ts 開發(fā)時)引入一些開發(fā)模式的包,而在生產(chǎn)時則使用壓縮過的 .min.js 包,更甚者我們可能會選擇在生產(chǎn)時使用 cdn 來鏈接這些包而不是直接引入。
如果 tsc 自己會轉(zhuǎn)換這些路徑別名,那么后續(xù)的打包工具就無從得知這個包是需要特殊對待的。在這些打包工具眼里這些引入的第三方代碼和你自己寫的代碼是地位相同的。
更危險的情況下,如果 paths 引入的這個包是 只能 用在開發(fā)環(huán)境下的,那么直接轉(zhuǎn)換路徑別名就相當于把開發(fā)包編譯到代碼包然后發(fā)布到生產(chǎn)環(huán)境里,從而導致程序崩潰的風險。這也是為什么 kitsonk 最后說:
Just because TypeScript can be configured to resolve modules in flexible ways doesn't not mean that TypeScript emits "broken code".
Daniel Rosenwasser 最后也提到,paths 配置項運行用戶自己選擇引入的是何種格式的包,例如 AMD 或是 systemjs。在開發(fā)階段這無關緊要,而在打包階段,用哪種格式的包應該交給更專業(yè)的打包工具來選擇。
換一個角度看這個問題,使用者希望 ts 必須一絲不茍的把自己的 ts 代碼翻譯成 js 代碼,之前執(zhí)行什么樣,轉(zhuǎn)換成 js 代碼后執(zhí)行就得是什么樣。而自己替換路徑別名這件事就開了個口子,編譯器居然把一個根本不可能變化的字面值給改了?
現(xiàn)在好了,如果你是 tsc,現(xiàn)在甲方又想讓你保證代碼的準確性,又因為懶想讓你自己修改代碼。你怎么干?你能怎么干?
當然是不干。畢竟霸哥也說過:
我寧愿什么都不做,也不愿犯錯。
總結(jié)
本文介紹了怎么解決 ts 項目中的路徑別名問題以及為什么會有這個問題。本來打算簡單寫一下的,沒想到還是洋洋灑灑幾千字出去了。
其實原因上面說了這么多,總結(jié)起來也就一句話:路徑別名替換這件事我不干也有更專業(yè)的人來干,我要干了可能會出問題,出了問題你們還得罵我。
參考
- typescript - tsc - doesn't compile alias paths - Stack Overflow
- Module path maps are not resolved in emitted code · Issue #10866 · microsoft/TypeScript · GitHub
- Path maps cannot be resolved by tsc / Works as intended - what a joke : typescript (reddit.com)
- TypeScript: Documentation - Module Resolution (typescriptlang.org)
- 解決typescript 在 node.js 下使用別名(paths)無效的問題
到此這篇關于typescript路徑別名問題的文章就介紹到這了,更多相關typescript路徑別名問題內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
nest.js 使用express需要提供多個靜態(tài)目錄的操作方法
這篇文章主要介紹了nest.js 使用express需要提供多個靜態(tài)目錄的操作,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2019-10-10

