TypeScript代碼的編譯過程一直以來會給很多小伙伴造成困擾,typescript官方提供tsc對ts代碼進行編譯,babel也表示能夠編譯ts代碼,它們二者的區(qū)別是什么?我們應(yīng)該選擇哪種方案?為什么IDE打開ts項目的時候,就能有這些ts代碼的類型定義?為什么明明IDE對代碼標紅報錯,但代碼有能夠編譯出來?
帶著這些問題,我們由淺入深介紹TypeScript代碼編譯的兩種方案以及我們?nèi)粘J褂肐DE進行ts文件類型檢查的關(guān)系,讓你今后面對基于ts的工程能夠做到游刃有余。
寫在前面
其實這篇文章并非是全新的文章,早在22年的8月份,我就寫了一篇名為《TypeScript與Babel、webpack的關(guān)系以及IDE對TS的類型檢查》的文章,里面的內(nèi)容就包含了本文的內(nèi)容,但迫于當時編寫的匆忙,整個文章的結(jié)構(gòu)安排的不好,脈絡(luò)不清晰,東一塊西一塊想到哪里寫到哪里,同時還想把webpack相關(guān)的也介紹了,所以最終內(nèi)容比較多比較亂。有強迫癥的我一直以來對當時的文章都不是很滿意。
恰好剛好最近又在寫有關(guān)TSX(基于TypeScript代碼的JSX代碼)的類型檢查相關(guān)的介紹,故重新將當時的文章翻了出來,重新編排整理了內(nèi)容,增加了更多的示意圖,移除了有關(guān)webpack的部分,著重介紹現(xiàn)階段TypeScript代碼的編譯方案,讓文章內(nèi)容更加聚焦。而在三部曲的第二部分,則會著重介紹本文移除了的對于webpack工程如何編譯TypeScript項目的內(nèi)容(考慮到該部分內(nèi)容需要有本文的基礎(chǔ),故放在了第二部分)。在最后一部分將會介紹TSX的類型檢查。
TypeScript基本原則
原則1:主流的瀏覽器的主流版本只認識js代碼
原則2:ts的代碼一定會經(jīng)過編譯為js代碼,才能運行在主流瀏覽器上
TypeScript編譯方式
首先,想要編譯ts代碼,至少具備以下3個要素:
- ts源代碼
- ts編譯器
- ts編譯配置
上述過程為:ts編譯器讀取ts源代碼,并通過指定的編譯配置,將ts源代碼編譯為指定形式的js代碼。
目前主流的ts編譯方案有2種,分別是:
- tsc編譯
- babel編譯
接下來將詳細介紹上述兩種方案以及它們之間的差異。
tsc編譯
官方編譯方案,按照TypeScript官方的指南,你需要使用tsc(TypeScript Compiler)完成,該tsc來源于你本地或是項目安裝的typescript包中。
按照上面的ts代碼編譯3要素,我們可以完成一一對應(yīng):
- ts源代碼
- ts編譯器:tsc
- ts編譯配置:tsconfig.json
讓我們通過一個simple-tsc-demo,實踐這一過程。
首先,創(chuàng)建一個名為simple-tsc-demo的空文件夾,并進行yarn init
(npm init
亦可)。然后,我們按照上述的三要素模型,準備:
(1)ts源代碼:編寫項目根目錄/src/index.ts
interface User {
id: string;
name: string;
}
export const userToString = (u: User) => `${u.id}/${u.name}`
(2)編譯器tsc:安裝typescript獲得
yarn add typescript
(3)編譯配置tsconfig.json:項目根目錄/tsconfig.json
{
"compilerOptions": {
"module": "commonjs",
"rootDir": "./src",
"outDir": "./dist"
}
}
簡單介紹上述tsconfig.json配置:
- module:指定ts代碼編譯生成何種模塊方案的js代碼,這里暫時寫的commonjs,后續(xù)會介紹其它值的差異;
- rootDir:指定ts代碼存放的根目錄,這里就是當前目錄(項目根目錄)下的src文件夾,能夠匹配到我們編寫的
項目根目錄/src/index.ts
; - outDir:指定ts代碼經(jīng)過編譯后,生成的js代碼的存放目錄。
當然,為了方便執(zhí)行命令,我們在package.json中添加名為build
的腳本:
{
...
+ "scripts": {
+ "build": "tsc"
+ },
...
}
完成搭建以后,項目整體如下:
運行build腳本,能夠看到在項目根目錄產(chǎn)生dist/index.js
:
對于index.js的內(nèi)容,熟悉js模塊化規(guī)范的小伙伴應(yīng)該很容易看出這是commonjs的規(guī)范:給exports對象上添加屬性字段,exports對象會作為模塊導(dǎo)出,被其他模塊使用。
之所以產(chǎn)生的js代碼是符合commonjs模塊規(guī)范的代碼,源于我們在tsconfig.json中配置的module值為commonjs
。倘若我們將module字段改為es6
:
{
"compilerOptions": {
- "module": "commonjs",
+ "module": "es6",
"rootDir": "./src",
"outDir": "./dist"
}
}
再一次編譯以后,會看到編譯后的js代碼則是符合es6模塊規(guī)范的代碼:
對于tsc編譯方案,按照TypeScript編譯三要素模型簡單總結(jié)一下:我們準備了ts源碼、tsc編譯器以及tsconfig.json配置。通過tsc編譯器讀取tsconfig.json編譯配置,將ts源碼編譯為了js代碼。此外,在tsconfig.json中,我們配置了生成的js代碼的兩種模塊規(guī)范:"module": "commonjs"
與"module": "es6"
,并驗證了其結(jié)果符合對應(yīng)的模塊規(guī)范。
對于編譯器這部分來說,除了上面我們嘗試過的tsc編譯器,是否還存在其他的編譯器呢?答案是肯定的:babel。
babel編譯
本文并不是一篇專門講babel的文章,但是為了讓相關(guān)知識能夠比較好的銜接,還是需要介紹這塊內(nèi)容的。當然如果讀者有時間,我推薦這篇深入了解babel的文章:一口(很長的)氣了解 babel - 知乎 (zhihu.com)。
babel 總共分為三個階段:解析,轉(zhuǎn)換,生成。
babel 本身不具有任何轉(zhuǎn)化功能,它把轉(zhuǎn)化的功能都分解到一個個 plugin 里面。因此當我們不配置任何插件時,經(jīng)過 babel 的代碼和輸入是相同的。
插件總共分為兩種:
- 當我們添加 語法插件 之后,在解析這一步就使得 babel 能夠解析更多的語法。(順帶一提,babel 內(nèi)部使用的解析類庫叫做 babylon,并非 babel 自行開發(fā))
舉個簡單的例子,當我們定義或者調(diào)用方法時,最后一個參數(shù)之后是不允許增加逗號的,如
callFoo(param1, param2,)
就是非法的。如果源碼是這種寫法,經(jīng)過 babel 之后就會提示語法錯誤。但最近的 JS 提案中已經(jīng)允許了這種新的寫法(讓代碼 diff 更加清晰)。為了避免 babel 報錯,就需要增加語法插件babel-plugin-syntax-trailing-function-commas
- 當我們添加 轉(zhuǎn)譯插件 之后,在轉(zhuǎn)換這一步把源碼轉(zhuǎn)換并輸出。這也是我們使用 babel 最本質(zhì)的需求。
比起語法插件,轉(zhuǎn)譯插件其實更好理解,比如箭頭函數(shù)
(a) => a
就會轉(zhuǎn)化為function (a) {return a}
。完成這個工作的插件叫做babel-plugin-transform-es2015-arrow-functions
。同一類語法可能同時存在語法插件版本和轉(zhuǎn)譯插件版本。如果我們使用了轉(zhuǎn)譯插件,就不用再使用語法插件了。
總結(jié)來說,babel轉(zhuǎn)換代碼就像如下流程:
源代碼 -(babel)-> 目標代碼
如果沒有使用任何插件,源代碼和目標代碼就沒有任何差異。當我們引入各種插件的時候,就像如下流程一樣:
源代碼
|
進入babel
|
babel插件1處理代碼,例如移除某些符號
|
babel插件2處理代碼,例如將形如() => {}的箭頭函數(shù),轉(zhuǎn)換成function xxx() {}
|
目標代碼
babel提倡一個插件專注做一個事情,比如某個插件只進行箭頭函數(shù)轉(zhuǎn)換工作,某個插件只處理將const轉(zhuǎn)var代碼,這樣設(shè)計的好處是可以靈活的組合各種插件完成代碼轉(zhuǎn)換。
但又因為babel的插件處理的力度很細,JS代碼的語法規(guī)范有很多,為了處理這些語法,可能需要配置一大堆的插件。為了解決這個問題,babel設(shè)計preset(預(yù)置集)概念,preset組合了一堆插件。于是,我們只需要引入一個插件組合包preset,就能處理代碼的各種語法。
PS:官方收編的插件包通常以 “@babel/plugin-” 開頭的,而預(yù)置集包通常以 “@babel/preset-” 開頭。
回到TypeScript編譯,對于babel編譯TS的體系,我們同樣按照TypeScript編譯三要素模型,來一一對應(yīng):
- ts源碼
- ts編譯器:babel+相關(guān)preset、plugin
- ts編譯配置:.babelrc
同樣的,讓我們通過一個simple-babel-demo,實踐這一過程。
首先,創(chuàng)建一個名為simple-babel-demo的空文件夾,并進行yarn init
(npm init
亦可)。然后,我們按照上述的三要素模型,準備:
(1)源代碼:編寫項目根目錄/src/index.ts
interface User {
id: string;
name: string;
}
export const userToString = (u: User) => `${u.id}/${u.name}`
(2)ts編譯器babel+相關(guān)preset、plugin:項目安裝如下依賴包
yarn add -D @babel/cli @babel/core
yarn add -D @babel/preset-env @babel/preset-typescript
yarn add -D @babel/plugin-proposal-object-rest-spread
讀者看到需要安裝這么多的依賴包不要感到恐懼,讓我們一個一個分析:
-
@babel/core
:babel的核心模塊,控制了整體代碼編譯的運轉(zhuǎn)以及代碼語法、語義分析的功能; -
@babel/cli
:支持我們可以在控制臺使用babel命令; -
@babel/preset-
開頭的就是預(yù)置組件包合集,其中@babel/preset-env
表示使用了可以根據(jù)實際的瀏覽器運行環(huán)境,會選擇相關(guān)的轉(zhuǎn)義插件包,通過配置得知目標環(huán)境的特點只做必要的轉(zhuǎn)換。如果不寫任何配置項,env 等價于 latest,也等價于 es2015 + es2016 + es2017 三個相加(不包含 stage-x 中的插件);@babel/preset-typescript
會處理所有ts的代碼的語法和語義規(guī)則,并轉(zhuǎn)換為js代碼。 -
plugin開頭的就是插件,這里我們引入:
@babel/plugin-proposal-object-rest-spread
(對象展開),它會處理我們在代碼中使用的...
運算符轉(zhuǎn)換為普通的js調(diào)用。
介紹完以后,是不是有了一些清晰的認識了呢。讓我們繼續(xù)三要素的最后一個:編譯配置。
(3)編譯配置.babelrc:項目根目錄/.babelrc文件
{
"presets": [
"@babel/preset-env",
"@babel/preset-typescript"
],
"plugins": [
"@babel/plugin-proposal-object-rest-spread"
]
}
上面的配置并不復(fù)雜,對應(yīng)了我們安裝依賴包中關(guān)于preset與plugin的部分。這部分配置,也是在告訴babel,處理代碼的時候,需要加載哪些preset、plugin好讓它們處理代碼。
最后,我們在package.json添加編譯腳本:
{
...
+ "scripts": {
+ "build": "babel src --config-file ./.babelrc -x .ts -d dist"
+ },
...
}
編譯指令指定了babel要讀取的源代碼所在目錄(src
)、babel配置文件地址(--config-file ./.babelrc
)、babel需要處理的文件擴展(-x .ts
)、編譯代碼生成目錄(-d dist
)。
完成項目搭建以后,整體如下:
運行build腳本,能夠看到在項目根目錄產(chǎn)生dist/index.js
:
這段代碼,與上面tsc基于commonjs編譯的js代碼差別不大。也就是說,babel基于@babel/preset-env
+@babel/preset-typescript
就能將TS代碼編譯為commonjs代碼。那么我們?nèi)绾问褂胋abel將ts代碼編譯器es6的代碼呢?從babel配置下手,實際上,我們只需要將babelrc的@babel/preset-env
移除即可:
{
"presets": [
- "@babel/preset-env",
"@babel/preset-typescript"
],
"plugins": [
"@babel/plugin-proposal-object-rest-spread"
]
}
再次編譯后,可以看到生成的index.js符合es6規(guī)范:
對于babel編譯,同樣簡單總結(jié)一下,對應(yīng)TypeScript編譯三要素模型,我們準備了ts源碼、babel與相關(guān)preset和plugin作為編譯器,以及babelrc作為編譯配置。babel處理代碼的流程啟動以后,根據(jù)編譯配置知道需要加載哪些plugin、preset,將代碼以及相關(guān)信息交給plugin、preset處理,最終編譯為js代碼。此外,在babelrc中,我們通過是否配置@babel/preset-env
控制生成滿足commonjs或es6模塊規(guī)范的js代碼。
編譯總結(jié)
不難看出,ts無論有多么龐大的語法體系,多么強大的類型檢查,最終的產(chǎn)物都是js。
此外還要注意的一點是,ts中的模塊化不能和js中的模塊化混為一談。js中的模塊化方案很多(es6、commonjs、umd等等),所以ts本身在編譯過程中,需要指定一種js的模塊化表達,才能編譯為對應(yīng)的代碼。在ts中的import/export
,不能認為和es6的import/export
是一樣的,他們是完全不同的兩個體系!只是語法上相似而已。
tsc編譯與babel編譯的差異
前面,我們介紹了tsc編譯與babel編譯TS代碼,那他們二者有什么差異呢?讓我們先來看這樣一個場景:下面是一段ts源代碼:
interface User {
id: string;
name: string;
}
export const userToString = (u: User) => `${u.id}/${u.name}`
我們故意將u.name
錯寫為u.myName
:
- export const userToString = (u: User) => `${u.id}/${u.name}`
+ export const userToString = (u: User) => `${u.id}/${u.myName}`
預(yù)期上講,類型檢查肯定不通過,因為User
接口根本沒有name
字段。讓我們分別在tsc編譯和babel編譯中看一下編譯的結(jié)果是否滿足我們的預(yù)期。
tsc編譯錯誤代碼
可以從結(jié)果很清楚的看到,使用tsc編譯錯誤代碼的時候,tsc類型檢查幫助我們找到了代碼的錯誤點,符合我們的預(yù)期。
babel編譯錯誤代碼
從結(jié)果來看,babel編譯居然可以直接成功!查看生成的index.js代碼:
export const userToString = u => `${u.id}/${u.myName}`;
從js代碼角度來看,這段代碼沒有任何的問題,此時的u
參數(shù)變量在js層面,并沒有明確的類型定義,js作為動態(tài)語言,運行的時候,myName
也可能就存在,這誰也無法確定。
為什么babel編譯會這樣處理代碼?這不得不提到babel中的@babel/preset-typescript
是如何編譯TS代碼的:
警告!有一個震驚的消息,你可能想坐下來好好聽下。
Babel 如何處理 TypeScript 代碼?它刪除它。
是的,它刪除了所有 TypeScript,將其轉(zhuǎn)換為“常規(guī)的” JavaScript,并繼續(xù)以它自己的方式愉快處理。
這聽起來很荒謬,但這種方法有兩個很大的優(yōu)勢。
第一個優(yōu)勢:???閃電般快速??。
大多數(shù) Typescript 開發(fā)人員在開發(fā)/監(jiān)視模式下經(jīng)歷過編譯時間長的問題。你正在編寫代碼,保存一個文件,然后...它來了...再然后...最后,你看到了你的變更。哎呀,錯了一個字,修復(fù),保存,然后...啊。它只是慢得令人煩惱并打消你的勢頭。
很難去指責(zé) TypeScript 編譯器,它在做很多工作。它在掃描那些包括
node_modules
在內(nèi)的類型定義文件(*.d.ts
),并確保你的代碼正確使用。這就是為什么許多人將 Typescript 類型檢查分到一個單獨的進程。然而,Babel + TypeScript 組合仍然提供更快的編譯,這要歸功于 Babel 的高級緩存和單文件發(fā)射架構(gòu)。
具體的內(nèi)容小伙伴可以查看: TypeScript 和 Babel:美麗的結(jié)合 - 知乎 (zhihu.com)。
也就是說,babel處理TypeScript代碼的時候,并不進行任何的類型檢查!那小伙伴可能會說,那如果我使用babel編譯方案,怎么進行類型檢查以確保ts代碼的正確性呢?答案則是:引入tsc,但僅僅進行類型檢查。
回到我們之前的simple-babel-example。在之前的基礎(chǔ)上,我們依舊安裝typescript從而獲得tsc:
{
...
"devDependencies": {
"@babel/cli": "^7.21.0",
"@babel/core": "^7.21.4",
"@babel/plugin-proposal-object-rest-spread": "^7.20.7",
"@babel/preset-env": "^7.21.4",
"@babel/preset-typescript": "^7.21.4",
+ "typescript": "^5.0.4"
}
}
然后,在項目中添加tsconfig.json文件,配置如下
{
"compilerOptions": {
"noEmit": true,
"rootDir": "src"
}
}
比起tsc編譯方案里面的配置有所不同,在babel編譯方案中的類型檢查的tsconfig.json需要我們配置noEmit
為true
,表明tsc讀取到了ts源代碼以后,不會生成任何的文件,僅僅會進行類型檢查。
于是,在babel編譯方案中,整個體系如下:
主流IDE對TS項目如何進行類型檢查
不知道有沒有細心的讀者在使用IDEA的時候,會發(fā)現(xiàn)如果是IDE當前打開的TS文件,IDEA右下角會展示一個typescript:
VSCode同樣也會有:
在同一臺電腦上,甚至發(fā)現(xiàn)IDEA和VSCode的typescript版本都還不一樣(5.0.3和4.9.5)。這是怎么一回事呢?實際上,IDE檢測到你所在的項目是一個ts項目的時候(或當前正在編輯ts文件),就會自動的啟動一個ts的檢測服務(wù),專門用于當前ts代碼的類型檢測。這個ts類型檢測服務(wù),同樣使用tsc來完成,但這個tsc來源于兩個途徑:
- 每個IDE默認情況下自帶的typescript中的tsc
- 當前項目安裝的typescript的tsc
例如,上圖本人機器上的IDEA,因為檢測到了項目安裝了"typescript": "^5.0.3"
,所以自動切換為了項目安裝的TypeScript;而VSCode似乎沒有檢測到,所以使用VSCode自帶的。
當然,你也可以在IDE中手動切換:
最后,我們簡單梳理下IDE是如何在對應(yīng)的代碼位置展示代碼的類型錯誤,流程如下:
但是,同樣是IDE中的ts類型檢查也要有一定的依據(jù)。譬如,外部庫的類型定義的文件從哪里查找,是否允許較新的語法等,這些配置依然是由tsconfig.json來提供的,但若未提供,則IDE會使用一份默認的配置。如果要進行類型檢測的自定義配置,則需要提供tsconfig.json。
編譯方案與IDE類型檢查整合
綜合前面的tsc編譯與babel編譯的過程,再整理上述的IDE對TS項目的類型檢查,我們可以分別總結(jié)出tsc編譯與babel編譯兩種場景的代碼編譯流程和IDE類型檢查流程。
首先是tsc編譯方案:
在這套方案中,ts項目的代碼本身的編譯,會走項目安裝的typescript,并加載項目本身的tsconfig.json配置。同時,IDE也會利用項目本身的typescript以及讀取相同配置的tsconfig.json來完成項目代碼的類型檢查。
于是,無論是代碼編譯還是IDE呈現(xiàn)的類型檢查,都是走的一套邏輯,當IDE提示了某些ts代碼的編譯問題,那么ts代碼編譯一定會出現(xiàn)相同的問題。不會存在這樣的情況:代碼有編譯問題,但是IDE不會紅色顯示類型檢查問題。
再來看babel編譯方案:
很顯然,babel編譯方案,代碼編譯與IDE的類型檢查是兩條路線。也就是說,有可能你的IDE提示了錯誤,但是babel編譯是沒有問題。這也是很多小伙伴拿到基于babel編譯的TS項目容易出現(xiàn)IDE有代碼異常問題的UI顯示,但是編譯代碼有沒有問題的原因所在。文章來源:http://www.zghlxwxcb.cn/news/detail-405371.html
寫在最后
本文著重介紹了TypeScript代碼的兩種編譯方案,以及IDE是如何進行TypeScript的類型檢查的。作為三部曲的第一部,內(nèi)容比較多,比較細,感謝讀者的耐心閱讀。接下來的剩余兩部分,將分別介紹webpack如何編譯打包基于TypeScript的項目以及TSX是如何進行類型檢查。文章來源地址http://www.zghlxwxcb.cn/news/detail-405371.html
到了這里,關(guān)于TypeScript必知三部曲(一)TypeScript編譯方案以及IDE對TS的類型檢查的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!