typescript路徑別名問(wèn)題詳解與前世今生的故事
前言
最近在 typescript 項(xiàng)目上踩了不少坑,打算寫幾篇文章記錄一下。
本篇文章就來(lái)梳理一下 ts 的相關(guān)技術(shù)棧 tsc、ts-node、ts-node-dev 中的路徑別名問(wèn)題,從開(kāi)發(fā)到打包階,不僅告訴你坑在哪,怎么解決,還會(huì)還會(huì)告訴你為什么會(huì)有這個(gè)坑以及坑背后的故事。
開(kāi)始探索
本篇文章會(huì)以一個(gè)空項(xiàng)目開(kāi)始,由淺入深的告訴你會(huì)遇到的各種問(wèn)題,有興趣的同學(xué)可以跟著往下做一下:
首先找一個(gè)空文件夾,請(qǐng)出本文的核心嘉賓 ts-node:
npm install ts-node typescript
然后完善一下項(xiàng)目,添加一個(gè)相當(dāng)基礎(chǔ)的 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 就會(huì)去讀 index.ts 并執(zhí)行:
"scripts": { "dev": "ts-node --files ./index.ts" }
這樣一個(gè) typescript 開(kāi)發(fā)環(huán)境就準(zhǔ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))
這些代碼很簡(jiǎn)單,導(dǎo)出了一個(gè)加法函數(shù),然后在 index.ts 里引用并打印了出來(lái),現(xiàn)在我們執(zhí)行 npm run dev
就可以看到如下結(jié)果:
沒(méi)毛病,一切正常,現(xiàn)在我們通過(guò)路徑別名引入 utils:
// 把 utils 的引入換成了 @ 路徑別名 import { plus } from "@/utils";
然后再來(lái)執(zhí)行一下:
出問(wèn)題了,找不到對(duì)應(yīng)的模塊,這個(gè)問(wèn)題其實(shí)不在 ts-node,而是因?yàn)?tsc 在編譯代碼時(shí)不會(huì)去把路徑別名替換成對(duì)應(yīng)的相對(duì)路徑,所以 ts-node 用 tsc 編譯完然后轉(zhuǎn)交給 node 執(zhí)行的時(shí)候自然就找不到 @/
這個(gè)目錄了。
解決起來(lái)很簡(jiǎn)單,只需要安裝一個(gè)包:
npm install tsconfig-paths
然后在 package.json
里改一下 dev
命令即可,這里的 -r 實(shí)際上是 node 的命令行參數(shù),有興趣的可以去看一下這個(gè):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 使用路徑別名
很多同學(xué)在開(kāi)發(fā) ts 項(xiàng)目的時(shí)候都是使用 nodemon 監(jiān)聽(tīng)文件變更,然后使用 ts-node 執(zhí)行代碼。比如這樣:
"dev": "nodemon -e ts --exec ts-node -r tsconfig-paths/register --files ./index.ts"
不過(guò)我們可以用一個(gè)更簡(jiǎn)單方便的工具來(lái)完成這個(gè)操作,那就是 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 一樣用相同的方法解決路徑別名問(wèn)題。
而且除了看起來(lái)更簡(jiǎn)潔外,ts-node-dev 還有個(gè)好處就是 它會(huì)緩存 tsc 的編譯過(guò)程,所以熱更新速度比 nodemon + ts-node 快了很多。非常推薦大家試用一下。
關(guān)于路徑別名的打包問(wèn)題
還沒(méi)完,我們現(xiàn)在只解決了開(kāi)發(fā)時(shí)的問(wèn)題。如果路徑別名是 tsc 負(fù)責(zé)的話,那么打包時(shí)也會(huì)遇到這個(gè)問(wèn)題?,F(xiàn)在我們就來(lái)看一下打包時(shí)會(huì)怎樣。
首先,在命令行執(zhí)行 npx tsc
,然后去 ./dist/index.js
里看一下打包后的成果:
果然,tsc 沒(méi)有把我們的路徑別名轉(zhuǎn)換成實(shí)際的相對(duì)路徑。那么執(zhí)行 node ./dist/index.js
時(shí)肯定會(huì)報(bào)錯(cuò)找不到 @/utils
,所以該如何解決這個(gè)問(wèn)題呢?
一般來(lái)說(shuō)有兩種方法,先說(shuō)菜一點(diǎn)的,我們剛才提到 tsconfig-paths/register
實(shí)際上是用于 node 的一個(gè)包,那么這里自然也可以這么用,在 package.json 里添加如下命令(記得安裝 cross-env ):
"scripts": { "start": "cross-env TS_NODE_BASEURL=./dist node -r tsconfig-paths/register .\\dist\\index.js" }
然后再執(zhí)行就可以了:
簡(jiǎn)單解釋一下,這里用環(huán)境變量 TS_NODE_BASEURL
覆蓋了 tsconfig.json 里的 baseurl
,來(lái)讓查找路徑別名指向的目錄時(shí)可以找到正確的(編譯后)的文件。后面就是正常的 node -r 引用對(duì)應(yīng)包然后執(zhí)行文件。
但是這種操作屬于“運(yùn)行時(shí)”處理,相信很多人都會(huì)覺(jué)得不舒服。所以更簡(jiǎn)單的方法是在編譯代碼時(shí)就一勞永逸的替換掉路徑別名,就是下面這種方法(推薦):
我們可以安裝 npm install tsc-alias
這個(gè)包,用于替換路徑別名,用法也很簡(jiǎn)單,跟在 tsc 后面就可以了:
"scripts": { "build": "tsc && tsc-alias" }
現(xiàn)在執(zhí)行 npm run build
之后就可以看到路徑別名已經(jīng)被替換成相對(duì)路徑了:
當(dāng)然,這個(gè)只是比較簡(jiǎn)單的解決方式之一,你也可以根據(jù)項(xiàng)目的實(shí)際情況來(lái),比如使用 webpack 或者 vite,又或者你可以看下文末參考小節(jié)里的第一個(gè)鏈接來(lái)了解更多的解決方法。
tsc 為什么不會(huì)轉(zhuǎn)換路徑別名?
文章寫到這里,我們已經(jīng)解決了 ts 項(xiàng)目中的路徑別名問(wèn)題,但是相信很多人依舊有一個(gè)困惑:tsc 為什么不在編譯的時(shí)候直接把路徑別名替換掉?
在網(wǎng)上搜索相關(guān)的問(wèn)題,能找到的大多都是如何解決這個(gè)問(wèn)題,而對(duì)這個(gè)問(wèn)題起因的解釋卻很少,我在搜索解決方法的時(shí)候也只看到有人說(shuō),這件事啊,說(shuō)來(lái)話長(zhǎng)了。
這不由得勾起了我的興趣,在詳細(xì)搜索之后,我找到了這篇討論,卻沒(méi)想到居然是一篇橫跨三年的故事。下面我們就大致了解一下這個(gè)問(wèn)題的前世今生,如果你感興趣的話也可以直接去看一下:Module path maps are not resolved in emitted code · Issue #10866
故事要從 2016 年 9 月說(shuō)起,當(dāng)時(shí)的 typescript 還在 2.1 開(kāi)發(fā)版。有人在試用了 tsconfig 中的 paths 配置時(shí)也遭遇了相同的困惑,他便提交了一個(gè) issue 來(lái)詢問(wèn)這個(gè)問(wèn)題(事實(shí)上,最早的 issue 記錄可以追溯到 2016 年 7 月份,不過(guò)問(wèn)題相同,這里不再贅述 )。
沒(méi)過(guò)兩天,有兩位 ts 貢獻(xiàn)者先后回復(fù)到:你有沒(méi)有同時(shí)在用一些 webpack 之類的打包工具?另一位則簡(jiǎn)單介紹了這么做是 有意為之的,paths 配置項(xiàng)的本意是想為一些第三方包提供特殊的引入渠道,使得 ts 可以獲得更多類型信息,所以這不是個(gè)問(wèn)題。
但是很顯然,圍觀者的 ?? 表明大家都不是很能接受這個(gè)觀點(diǎn)。
在解釋之后,貢獻(xiàn)者給本 issue 添加了 Working as Intended
(按預(yù)期工作)標(biāo)簽并把問(wèn)題標(biāo)記為完成,同時(shí) issue 作者也表明可以理解,并善意的給后來(lái)者介紹了自己已經(jīng)通過(guò) module-alias 解決了問(wèn)題。
但是圍觀者顯然沒(méi)有被說(shuō)服,大家開(kāi)始陸續(xù)在這個(gè) issue 下評(píng)論,一些人在介紹自己使用的解決辦法,一些人在挺 ts,但是更多的人在吐槽這個(gè)設(shè)計(jì)太糟糕了,自己實(shí)在想不明白,希望有人能解釋一下。
這個(gè)討論足足持續(xù)了兩年多,期間很多人 @ ts 貢獻(xiàn)者并表示已經(jīng)過(guò)去兩年時(shí)間了,希望可以重新考慮下這個(gè)令人困惑的設(shè)計(jì)。甚至有人在 reddit 上發(fā)了帖子:Path maps cannot be resolved by tsc / Works as intended - what a joke。
時(shí)間來(lái)到 2019 年 2 月份,issue 最開(kāi)始回復(fù)的一位貢獻(xiàn)者站出來(lái)發(fā)出了一段長(zhǎng)文進(jìn)行了解釋。大致意思就是,ts 在編譯中修改路徑這些字面值是不切實(shí)際的。ts 的工作重點(diǎn)應(yīng)該是如何更好的和其他更擅長(zhǎng)此類工作的模塊相互協(xié)作,而不是為了支持更靈活的配置來(lái)自己實(shí)現(xiàn)這些功能,更別提可能會(huì)因此生成出“崩壞”的代碼。
同一天,ts 組成員 DanielRosenwasser 再次明確表示了:哪怕將來(lái)可能會(huì)優(yōu)化這里的開(kāi)發(fā)體驗(yàn),ts 也不會(huì)修改用戶寫下的路徑。隨后便鎖定了這個(gè)問(wèn)題。
至此,這個(gè)故事便畫上了句號(hào)。
現(xiàn)在了解了事情的前因后果,我們就可以來(lái)總結(jié)一下這個(gè)問(wèn)題的根本原因了。
tsc 不轉(zhuǎn)換路徑別名的根本原因
站在開(kāi)發(fā)者的角度看,我們希望可以用更少的工具完成更多的事情,這無(wú)可厚非。但是站在 ts 本身的角度看,typescript 只是開(kāi)發(fā)階段使用的一個(gè)工具。它的核心目的也是優(yōu)化開(kāi)發(fā)體驗(yàn),在開(kāi)發(fā)時(shí)以強(qiáng)類型對(duì)代碼進(jìn)行約束。換句話說(shuō)就是 ts 表示我是負(fù)責(zé)開(kāi)發(fā)的,打包工作不是我的強(qiáng)項(xiàng),我大概編譯一下,去掉我的類型,剩下的打包工作交給更專業(yè)的工具來(lái)做。
所以我們可以把 tsc 編譯后的代碼看作是一種“中間產(chǎn)物”,typescript 期望用戶可以使用更貼近自己需求的工具將這些代碼二次編譯為實(shí)際的生產(chǎn)代碼。
現(xiàn)在我們回頭看一下 ts hankbook 中關(guān)于路徑別名的介紹:
可以看到,paths 配置項(xiàng)的本意是為了給一些模塊提供特殊的引入渠道。注意,此處引入的包只是你開(kāi)發(fā)時(shí)需要的,而不一定是生產(chǎn)時(shí)使用的。
例如,我們可以在開(kāi)發(fā)時(shí)(使用 ts 開(kāi)發(fā)時(shí))引入一些開(kāi)發(fā)模式的包,而在生產(chǎn)時(shí)則使用壓縮過(guò)的 .min.js 包,更甚者我們可能會(huì)選擇在生產(chǎn)時(shí)使用 cdn 來(lái)鏈接這些包而不是直接引入。
如果 tsc 自己會(huì)轉(zhuǎn)換這些路徑別名,那么后續(xù)的打包工具就無(wú)從得知這個(gè)包是需要特殊對(duì)待的。在這些打包工具眼里這些引入的第三方代碼和你自己寫的代碼是地位相同的。
更危險(xiǎn)的情況下,如果 paths 引入的這個(gè)包是 只能 用在開(kāi)發(fā)環(huán)境下的,那么直接轉(zhuǎn)換路徑別名就相當(dāng)于把開(kāi)發(fā)包編譯到代碼包然后發(fā)布到生產(chǎn)環(huán)境里,從而導(dǎo)致程序崩潰的風(fēng)險(xiǎn)。這也是為什么 kitsonk 最后說(shuō):
Just because TypeScript can be configured to resolve modules in flexible ways doesn't not mean that TypeScript emits "broken code".
Daniel Rosenwasser 最后也提到,paths 配置項(xiàng)運(yùn)行用戶自己選擇引入的是何種格式的包,例如 AMD 或是 systemjs。在開(kāi)發(fā)階段這無(wú)關(guān)緊要,而在打包階段,用哪種格式的包應(yīng)該交給更專業(yè)的打包工具來(lái)選擇。
換一個(gè)角度看這個(gè)問(wèn)題,使用者希望 ts 必須一絲不茍的把自己的 ts 代碼翻譯成 js 代碼,之前執(zhí)行什么樣,轉(zhuǎn)換成 js 代碼后執(zhí)行就得是什么樣。而自己替換路徑別名這件事就開(kāi)了個(gè)口子,編譯器居然把一個(gè)根本不可能變化的字面值給改了?
現(xiàn)在好了,如果你是 tsc,現(xiàn)在甲方又想讓你保證代碼的準(zhǔn)確性,又因?yàn)閼邢胱屇阕约盒薷拇a。你怎么干?你能怎么干?
當(dāng)然是不干。畢竟霸哥也說(shuō)過(guò):
我寧愿什么都不做,也不愿犯錯(cuò)。
總結(jié)
本文介紹了怎么解決 ts 項(xiàng)目中的路徑別名問(wèn)題以及為什么會(huì)有這個(gè)問(wèn)題。本來(lái)打算簡(jiǎn)單寫一下的,沒(méi)想到還是洋洋灑灑幾千字出去了。
其實(shí)原因上面說(shuō)了這么多,總結(jié)起來(lái)也就一句話:路徑別名替換這件事我不干也有更專業(yè)的人來(lái)干,我要干了可能會(huì)出問(wèn)題,出了問(wèn)題你們還得罵我。
參考
- 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)無(wú)效的問(wèn)題
到此這篇關(guān)于typescript路徑別名問(wèn)題的文章就介紹到這了,更多相關(guān)typescript路徑別名問(wèn)題內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
JavaScript進(jìn)階(四)原型與原型鏈用法實(shí)例分析
這篇文章主要介紹了JavaScript原型與原型鏈,結(jié)合實(shí)例形式分析了JavaScript原型與原型鏈基本概念、原理、用法及操作注意事項(xiàng),需要的朋友可以參考下2020-05-05js+html5 canvas實(shí)現(xiàn)ps鋼筆摳圖
這篇文章主要介紹了js+html5 canvas實(shí)現(xiàn)ps鋼筆摳圖,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2019-04-04微信小程序當(dāng)前時(shí)間時(shí)段選擇器插件使用方法詳解
這篇文章主要為大家詳細(xì)介紹了微信小程序當(dāng)前時(shí)間時(shí)段選擇器插件使用方法,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2018-12-12JavaScript實(shí)現(xiàn)隨機(jī)點(diǎn)名小程序
這篇文章主要介紹了JavaScript實(shí)現(xiàn)隨機(jī)點(diǎn)名小程序,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2020-10-10nest.js 使用express需要提供多個(gè)靜態(tài)目錄的操作方法
這篇文章主要介紹了nest.js 使用express需要提供多個(gè)靜態(tài)目錄的操作,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2019-10-10javascript實(shí)現(xiàn)的listview效果
javascript實(shí)現(xiàn)的listview效果...2007-04-04基于原生js實(shí)現(xiàn)判斷元素是否有指定class名
這篇文章主要介紹了基于原生js實(shí)現(xiàn)判斷元素是否有指定class名,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-07-07