只要接觸過ts的前端同學都能回答出ts是js超集,它具備靜態型別分析,能夠根據型別在靜態程式碼的解析過程中對ts程式碼進行型別檢查,從而在保證型別的一致性。那,現在讓你對你的webpack專案(其實任意型別的專案都同理)加入ts,你知道怎麼做嗎?帶著這個問題,我們由淺入深,逐步介紹TypeScript、Babel以及我們日常使用IDE進行ts檔案型別檢查的關係,讓你今後面對基於ts的工程能夠做到遊刃有餘。
原則1:主流的瀏覽器的主流版本只認識js程式碼
原則2:ts的程式碼一定會經過編譯為js程式碼,才能執行在主流瀏覽器上
要編譯ts程式碼,至少具備以下幾個要素:
目前主流的ts編譯方案有2種,分別是官方tsc編譯、babel+ts外掛編譯。
對於ts官方模式來說,ts編譯器就是tsc(安裝typescript就可以獲得),而編譯器所需的設定就是tsconfig.json組態檔形式或其他形式。ts原始碼經過tsc的編譯(Compile),就可以生成js程式碼,在tsc編譯的過程中,需要編譯設定來確定一些編譯過程中要處理的內容。
我們首先準備一個ts-demo,該demo中有如下的結構:
ts-demo
|- packages.json
|- tsconfig.json
|- src
|- index.ts
安裝typescript
yarn add -D typescript
package.json
{
"name": "ts-demo",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"build-ts": "tsc"
},
"author": "",
"license": "MIT",
"devDependencies": {
"typescript": "^4.7.4"
}
}
tsconfig.js(對於這個簡單的tsconfig,我不再贅述其設定的含義。)
{
"compilerOptions": {
"module": "commonjs",
"rootDir": "./src",
"outDir": "./dist"
}
}
index.ts
interface User {
name: string;
age: number;
}
const userToString = (user: User) => `${user.name}@${user.age}`;
export {userToString, User};
此時,我們只需要執行yarn build-ts
就可以將我們的index.ts編譯為index.js:
commonjs模組化方式產物:
"use strict";
exports.__esModule = true;
exports.userToString = void 0;
var userToString = function (user) { return "".concat(user.name, "@").concat(user.age); };
exports.userToString = userToString;
可以看到,原本index.ts編譯為index.js的產物,使用了commonjs模組化方案(tsconfig裡面設定模組化方案是"commonjs",編譯後的程式碼可以看到"exports"的身影);倘若我們將模組化方案改為ESM(ES模組化)的es:"module": "es6"
,編譯後的產物依然是index.js,只不過內容採用了es6中的模組方案。
es6模組化方式產物:
var userToString = function (user) {
return "".concat(user.name, "@").concat(user.age);
};
export {userToString};
說了這麼多,只是想要告訴各位同學,ts無論有多麼龐大的語法體系,多麼強大的型別檢查,最終的產物都是js。
此外,ts中的模組化,不能和js中的模組化混為一談。js中的模組化方案很多(es6、commonjs、umd等等),所以ts本身在編譯過程中,需要指定一種js的模組化表達,才能編譯為對應的程式碼。也就是說,在ts中的import/export
,不能認為和es6的import/export
是一樣的,他們是完全不同的兩個體系!只是語法上類似而已。
如前文所述
ts原始碼經過tsc的編譯(Compile),就可以生成js程式碼,在tsc編譯的過程中,需要編譯設定來確定一些編譯過程中要處理的內容。
那麼是不是說,編譯器這塊是不是有其他的代替呢?ts原始碼經過某種其他的編譯器編譯後,生成目標js程式碼。答案是肯定的:babel。
我們準備一個ts-babel-demo:
ts-babel-demo
|- packages.json
|- .babelrc
|- src
|- index.ts
依賴新增:
yarn add -D @babel/core @babel/cli
yarn add -D @babel/preset-env @babel/preset-typescript
yarn add -D @babel/plugin-proposal-class-properties @babel/plugin-proposal-object-rest-spread
package.json:
{
"name": "ts-babel-demo",
"version": "1.0.0",
"main": "index.js",
"license": "MIT",
"private": true,
"scripts": {
"build": "babel src -d dist -x '.ts, .tsx'"
},
"devDependencies": {
"@babel/cli": "^7.18.10",
"@babel/core": "^7.18.10",
"@babel/plugin-proposal-class-properties": "^7.18.6",
"@babel/plugin-proposal-object-rest-spread": "^7.18.9",
"@babel/preset-env": "^7.18.10",
"@babel/preset-typescript": "^7.18.6"
}
}
.babelrc
{
"presets": [
"@babel/preset-env",
"@babel/preset-typescript"
],
"plugins": [
"@babel/plugin-proposal-object-rest-spread",
"@babel/plugin-proposal-class-properties"
]
}
index.ts和ts-demo保持一致。
完成基礎的專案搭建以後,我們執行yarn build
:
~/Projects/web-projects/ts-babel-demo > yarn build
yarn run v1.22.17
$ babel src -d dist -x '.ts, .tsx'
Successfully compiled 1 file with Babel (599ms).
Done in 4.05s.
可以看到專案dist目錄下出現了編譯好的js程式碼:
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.userToString = void 0;
var userToString = function userToString(user) {
return "".concat(user.name, "@").concat(user.age);
};
exports.userToString = userToString;
可以看到和使用tsc編譯為commonjs效果是一樣。
回顧這個專案,其實按照我們之前的思路來梳理:
瞭解babel機制
如果對於babel不太熟悉,可能對上述的一堆依賴感到恐懼:
yarn add -D @babel/core @babel/cli
yarn add -D @babel/preset-env @babel/preset-typescript
yarn add -D @babel/plugin-proposal-class-properties @babel/plugin-proposal-object-rest-spread
這裡如果讀者有時間,我推薦這篇深入瞭解babel的文章:一口(很長的)氣了解 babel - 知乎 (zhihu.com)。當然,如果這口氣憋不住(哈哈),我做一個簡單摘抄:
babel 總共分為三個階段:解析,轉換,生成。
babel 本身不具有任何轉化功能,它把轉化的功能都分解到一個個 plugin 裡面。因此當我們不設定任何外掛時,經過 babel 的程式碼和輸入是相同的。
外掛總共分為兩種:
- 當我們新增 語法外掛 之後,在解析這一步就使得 babel 能夠解析更多的語法。(順帶一提,babel 內部使用的解析類庫叫做 babylon,並非 babel 自行開發)
舉個簡單的例子,當我們定義或者呼叫方法時,最後一個引數之後是不允許增加逗號的,如
callFoo(param1, param2,)
就是非法的。如果原始碼是這種寫法,經過 babel 之後就會提示語法錯誤。但最近的 JS 提案中已經允許了這種新的寫法(讓程式碼 diff 更加清晰)。為了避免 babel 報錯,就需要增加語法外掛
babel-plugin-syntax-trailing-function-commas
- 當我們新增 轉譯外掛 之後,在轉換這一步把原始碼轉換並輸出。這也是我們使用 babel 最本質的需求。
比起語法外掛,轉譯外掛其實更好理解,比如箭頭函數
(a) => a
就會轉化為function (a) {return a}
。完成這個工作的外掛叫做babel-plugin-transform-es2015-arrow-functions
。同一類語法可能同時存在語法外掛版本和轉譯外掛版本。如果我們使用了轉譯外掛,就不用再使用語法外掛了。
簡單來講,使用babel就像如下流程:
原始碼 =babel=> 目的碼
如果沒有使用任何外掛,原始碼和目的碼就沒有任何差異。當我們引入各種外掛的時候,就像如下流程一樣:
原始碼
|
進入babel
|
babel外掛1處理程式碼:移除某些符號
|
babel外掛2處理程式碼:將形如() => {}的箭頭函數,轉換成function xxx() {}
|
目的碼
因為babel的外掛處理的力度很細,我們程式碼的語法、語意內容規範有很多,如果我們要處理這些語法,可能需要設定一大堆的外掛,所以babel提出,將一堆外掛組合成一個preset(預置外掛包),這樣,我們只需要引入一個外掛組合包,就能處理程式碼的各種語法、語意。
所以,回到我們上述的那些@babel開頭的npm包,再回首可能不會那麼迷茫:
@babel/core
@babel/preset-env
@babel/preset-typescript
@babel/preset-react
@babel/plugin-proposal-class-properties
@babel/plugin-proposal-object-rest-spread
@babel/core
毋庸置疑,babel的核心模組,實現了上述的流程運轉以及程式碼語法、語意分析的功能;
@babel/cli
則是我們可以在命令列使用babel命令;
plugin開頭的就是外掛,這裡我們引入了兩個:@babel/plugin-proposal-class-properties
(允許類具有屬性)和@babel/plugin-proposal-object-rest-spread
(物件展開);
preset開頭的就是預置元件包合集,其中@babel/preset-env
表示使用了可以根據實際的瀏覽器執行環境,會選擇相關的跳脫外掛包,通過設定得知目標環境的特點只做必要的轉換。如果不寫任何設定項,env 等價於 latest,也等價於 es2015 + es2016 + es2017 三個相加(不包含 stage-x 中的外掛);@babel/preset-typescript
會處理所有ts的程式碼的語法和語意規則,並轉換為js程式碼。
關於babel編譯ts,並不是所有的語法都支援,這裡有一篇文章專門介紹了其中注意點:《TypeScript 和 Babel:美麗的結合》。
前面的內容,我們已經介紹了將ts編譯為js的兩種方式(tsc、babel),但僅僅是簡單將一個index.ts編譯為index.js。實際上,對於專案級別的ts專案,還有很多需要了解的。接下來基於一個webpack專案來逐步介紹如何基於前文的兩種方式來使用ts。
對於webpack來說,至少需要讀者瞭解到webpack的基本機制:概念 | webpack 中文檔案 (docschina.org)。
簡單來講,webpack執行從指定的entry檔案開始,從頂層開始分析依賴的內容,依賴的內容可以是任何的內容(只要是import的或require了的),而loader可以專門來處理各種型別的檔案。
webpack 只能理解 JavaScript 和 JSON 檔案,這是 webpack 開箱可用的自帶能力。loader 讓 webpack 能夠去處理其他型別的檔案,並將它們轉換為有效 模組,以供應用程式使用,以及被新增到依賴圖中
所以,當一個webpack專案是基於TS進行的時候,我們一定會有一個loader來處理ts(甚至是tsx)。當然,我們還是通過demo搭建來演示講解。
mkdir webpack-ts-loader-demo && cd webpack-ts-loader-demo
yarn init
yarn add -D webpack webpack-cli
yarn add -D ts-loader
package.json
{
"name": "webpack-ts-loader-demo",
"version": "1.0.0",
"main": "index.js",
"license": "MIT",
"scripts": {
"build": "webpack --config webpack.config.js"
},
"devDependencies": {
"ts-loader": "^9.3.1",
"webpack": "^5.74.0",
"webpack-cli": "^4.10.0"
}
}
webpack.config.js
const {resolve} = require('path');
module.exports = {
entry: './src/index.ts',
output: {
path: resolve(__dirname, './dist'),
filename: "index.js"
},
module: {
rules: [
{
test: /\.ts/,
loader: "ts-loader"
}
]
}
};
src/index.ts
interface User {
name: string;
age: number;
}
const userToString = (user: User) => `${user.name}@${user.age}`;
export {userToString, User};
表面上,只需要上述三個檔案,就可以編譯ts檔案,但是嘗試執行yarn build
會報錯:
Module build failed (from ./node_modules/ts-loader/index.js):
Error: Could not load TypeScript. Try installing with `yarn add typescript` or `npm install typescript`. If TypeScript is installed globally, try using `yarn link typescript` or `npm link typescript`.
通過報錯很容易理解,我們沒有安裝typescript。為什麼?因為ts-loader本身處理ts檔案的時候,本質上還是呼叫的tsc,而tsc是typescript模組提供的。因此,我們只需要yarn add -D typescript
即可(其實只需要開發依賴即可),但是緊接著又會有另外一個報錯:
ERROR in ./src/index.t
Module build failed (from ./node_modules/ts-loader/index.js):
Error: error while parsing tsconfig.json
報錯提醒我們,解析tsconfig的出錯,不難理解,我們還沒有設定tsconfig.json,因為tsc需要!所以,在我們專案中,加上tsconfig.json即可:
tsconfig.json
{
"compilerOptions": {
"module": "commonjs",
"rootDir": "./src",
"outDir": "./dist"
}
}
設定完成以後,我們再次編譯,發現可以編譯成功,並且在dist目錄下會有對應的js程式碼。
然而,事情到這裡就結束了嗎?一箇中大型的專案,必然有模組的引入,假如現在我們新增了個utils.ts
export const hello = () => {
return 'hello';
}
修改index.ts的程式碼,引入該hello方法,並使用:
import {hello} from "./utils"; // 引入utils
interface User {
name: string;
age: number;
}
const userToString = (user: User) => `${user.name}@${user.age}${hello()}`;
export {userToString, User};
再次執行yarn build
,讀者會發現還是會報錯,但這一次的錯誤略有點出乎意料:
Module not found: Error: Can't resolve './utils' in '/Users/w4ngzhen/Projects/web-projects/webpack-ts-loader-demo/src'
resolve './utils' in '/Users/w4ngzhen/Projects/web-projects/webpack-ts-loader-demo/src'
核心報錯在於,webpack似乎無法找到utils這個模組。為什麼呢?因為webpack預設是處理js程式碼的,如果你的程式碼中編寫了import xxx from 'xxx'
,在沒有明確指明這個模組的字尾的時候,webpack只會認為這個模組是以下幾種:
所以,你會看到具體一點的報錯:
resolve './utils' in '/Users/w4ngzhen/Projects/web-projects/webpack-ts-loader-demo/src'
using description file: /Users/w4ngzhen/Projects/web-projects/webpack-ts-loader-demo/package.json (relative path: ./src)
Field 'browser' doesn't contain a valid alias configuration
using description file: /Users/w4ngzhen/Projects/web-projects/webpack-ts-loader-demo/package.json (relative path: ./src/utils)
no extension
Field 'browser' doesn't contain a valid alias configuration
/Users/w4ngzhen/Projects/web-projects/webpack-ts-loader-demo/src/utils doesn't exist
.js
Field 'browser' doesn't contain a valid alias configuration
/Users/w4ngzhen/Projects/web-projects/webpack-ts-loader-demo/src/utils.js doesn't exist
.json
Field 'browser' doesn't contain a valid alias configuration
/Users/w4ngzhen/Projects/web-projects/webpack-ts-loader-demo/src/utils.json doesn't exist
.wasm
Field 'browser' doesn't contain a valid alias configuration
/Users/w4ngzhen/Projects/web-projects/webpack-ts-loader-demo/src/utils.wasm doesn't exist
as directory
要想讓webpack知道我們引入的utils是ts程式碼,方式為在webpack設定中,指明webpack預設處理的檔案字尾:
const {resolve} = require('path');
module.exports = {
// ... ...
resolve: {
// webpack 預設只處理js、jsx等js程式碼
// 為了防止在import其他ts程式碼的時候,出現
// " Can't resolve 'xxx' "的錯誤,需要特別設定
extensions: ['.js', '.jsx', '.ts', '.tsx']
},
// ... ...
};
完成設定以後,我們就能夠正確編譯具備模組匯入的ts程式碼了。
綜合來看,在基於ts-loader的webpack專案的解析流程處理如下。
回顧一下webpack,它預設處理模組化js程式碼,比如index.js參照了utils.js(模組參照方式可以是commonjs,也可以是esModule形式),那麼webpack從入口的index.js出發,來處理依賴,並打包為一個js(暫不考慮js拆分)。
對於wepack+ts-loader的ts專案體系主要是通過ts-loader內部呼叫typescript提供的tsc,將ts程式碼編譯為js程式碼(編譯後的js程式碼依然是js模組化的形式),所以這個過程是需要tsconfig參與;等到tsc將整個所有的ts程式碼均編譯為js程式碼以後,再整體交給webpack進行依賴分析並打包(也就進入webpack的預設處理流程)。
細心的讀者會發現這個過程有一個問題:由於先經過tsc編譯後的js,又再被webpack預設的js處理機制進行分析並編譯打包,這個過程一方面經過了兩次編譯(ts->標準模組化js->webpack模組體系js),那麼如果ts專案特別大,模組特別多的時候,這個兩次編譯的過程會特別漫長!
前面我們簡單介紹瞭如何使用babel對一份ts進行編譯,那麼在webpack中,如何使用babel呢?有的同學可能會想到這樣操作步驟:我先用babel對ts進行編譯為js,然後再利用webpack對js進行打包,這樣的做法是可以的,但細想不就和上面的ts-loader一樣的情況了嗎?
只要開發過基於webpack的現代化前端專案的同學,或多或少都看到過babel-loader的身影,他是個什麼東西呢?先說結論吧,babel-loader是webpack和babel(由@babel/core和一堆預置集preset、外掛plugins組合)的橋樑。
根據這個圖,同學可能覺得這不是和ts-loader的架構很像嗎?webpack啟動,遇到入口ts,匹配到babel-loader,babel-loader交給babel處理,處理完畢,回到webpack打包。但是使用babel進行ts處理,比起ts-loader更加高效。而關於這塊的說明,我更加推薦讀者閱讀這篇文章 TypeScript 和 Babel:美麗的結合 - 知乎 (zhihu.com),簡單來講:
警告!有一個震驚的訊息,你可能想坐下來好好聽下。
Babel 如何處理 TypeScript 程式碼?它刪除它。
是的,它刪除了所有 TypeScript,將其轉換為「常規的」 JavaScript,並繼續以它自己的方式愉快處理。
這聽起來很荒謬,但這種方法有兩個很大的優勢。
第一個優勢:️⚡️閃電般快速⚡️。
大多數 Typescript 開發人員在開發/監視模式下經歷過編譯時間長的問題。你正在編寫程式碼,儲存一個檔案,然後...它來了...再然後...最後,你看到了你的變更。哎呀,錯了一個字,修復,儲存,然後...啊。它只是慢得令人煩惱並打消你的勢頭。
很難去指責 TypeScript 編譯器,它在做很多工作。它在掃描那些包括
node_modules
在內的型別定義檔案(*.d.ts
),並確保你的程式碼正確使用。這就是為什麼許多人將 Typescript 型別檢查分到一個單獨的程序。然而,Babel + TypeScript 組合仍然提供更快的編譯,這要歸功於 Babel 的高階快取和單檔案發射架構。
讓我們來搭建一個專案來複習這一過程吧:
mkdir webpack-babel-loader-demo && cd webpack-babel-loader-demo
yarn init
yarn add -D webpack webpack-cli
yarn add -D babel-loader
yarn add -D @babel/core
yarn add -D @babel/preset-env @babel/preset-typescript
yarn add -D @babel/plugin-proposal-class-properties @babel/plugin-proposal-object-rest-spread
package.json
{
"name": "webpack-babel-loader-demo",
"version": "1.0.0",
"main": "index.js",
"license": "MIT",
"scripts": {
"build": "webpack --config webpack.config.js"
},
"devDependencies": {
"@babel/core": "^7.18.13",
"@babel/plugin-proposal-class-properties": "^7.18.6",
"@babel/plugin-proposal-object-rest-spread": "^7.18.9",
"@babel/preset-env": "^7.18.10",
"@babel/preset-typescript": "^7.18.6",
"babel-loader": "^8.2.5",
"webpack": "^5.74.0",
"webpack-cli": "^4.10.0"
}
}
webpack.config.js
const {resolve} = require('path');
module.exports = {
entry: './src/index.ts',
output: {
path: resolve(__dirname, './dist'),
filename: "index.js"
},
resolve: {
// webpack 預設只處理js、jsx等js程式碼
// 為了防止在import其他ts程式碼的時候,出現
// " Can't resolve 'xxx' "的錯誤,需要特別設定
extensions: ['.js', '.jsx', '.ts', '.tsx']
},
module: {
rules: [
{
test: /\.ts/,
loader: "babel-loader"
}
]
}
};
src/index.ts
import {hello} from "./utils";
interface User {
name: string;
age: number;
}
const userToString = (user: User) => `${user.name}@${user.age}${hello()}`;
export {userToString, User};
src/utils.ts
export const hello = () => {
return 'hello';
}
完成上述package.json、webpack.config.js、src原始碼三個部分,我們可以開始執行yarn build
,但實際上會報錯:
ERROR in ./src/index.ts
Module build failed (from ./node_modules/babel-loader/lib/index.js):
SyntaxError: /Users/w4ngzhen/Projects/web-projects/webpack-babel-loader-demo/src/index.ts: Unexpected reserved word 'interface'. (1:0)
> 1 | interface User {
| ^
2 | name: string;
3 | age: number;
4 | }
at instantiate (/Users/w4ngzhen/Projects/web-projects/webpack-babel-loader-demo/node_modules/@babel/parser/lib/index.js:72:32)
出現了語法的錯誤,報錯的主要原因在於沒有把整個babel處理ts的鏈路打通。目前的鏈路是:webpack找到入口ts檔案,匹配上babel-loader,babel-loader交給@babel/core,@babel/core處理ts。由於我們沒有給@babel/core設定plugin、preset,所以導致了babel還是以預設的js角度來處理ts程式碼,所以有語法報錯。此時,我們需要新增.babelrc檔案來指明讓babel載入處理ts程式碼的外掛:
.babelrc
{
"presets": [
"@babel/preset-env",
"@babel/preset-typescript"
],
"plugins": [
"@babel/plugin-proposal-object-rest-spread",
"@babel/plugin-proposal-class-properties"
]
}
完成設定以後,我們再次執行yarn build
,編譯通過,但是在dist下的index.js卻是空白的!
如果按照上述的設定以後,我們能夠成功編譯但是卻發現,輸出的js程式碼是空白的!原因在於:我們編寫的js程式碼,是按照類庫的模式進行編寫(在indexjs中只有匯出一些函數卻沒有實際的使用),且webpack打包的時候,沒有指定js程式碼的編譯為什麼樣子的庫。
假如我們在index中編寫一段具有副作用的程式碼:
import {hello} from "./utils";
interface User {
name: string;
age: number;
}
const userToString = (user: User) => `${user.name}@${user.age}${hello()}`;
// 具備副作用:在id=app的元素上新增監聽
document
.querySelector('#app')
.addEventListener('click', () => {})
export {userToString, User};
此時我們使用生產模式(mode: 'production')來編譯,會發現dist/index.js的內容如下:
(() => {
"use strict";
document.querySelector("#app").addEventListener("click", (function () {
}));
})();
會發現只有副作用程式碼,但是userToString相關的程式碼完全被剔除了!這時候,可能有讀者會說,我匯出的程式碼有可能別人會使用,你憑什麼要幫我剔除?其實,因為webpack預設是生成專案使用的js,也就是做打包操作,他的目的是生成當前專案需要的js。在我們這個範例中,在沒有寫副作用之前,webpack認為打包是沒有意義的,因為只有匯出方法,卻沒有使用。那麼,如果讓webpack知道,我們需要做一個類庫呢?在webpack中設定library欄位即可:
const {resolve} = require('path');
module.exports = {
entry: './src/index.ts',
mode: 'production',
output: {
// ... ...
library: {
// 設定library欄位的相關設定,這裡我們設定為commonjs2
// 至於這塊設定的意義,讀者需要自行學習~
type: 'commonjs2',
},
},
// ... ...
};
現在我們先編寫一個簡單錯誤程式碼:
interface User {
name: string;
age: number;
}
// user.myName並沒有在User介面中提供
const userToString = (user: User) => `${user.myName}@${user.age}`;
export {userToString, User};
在這個範例中,我們試圖存取在User型別中不存在的myName欄位。
前面我們提到了ts-loader內部呼叫的是tsc作為編譯器,我們嘗試執行基於ts-loader的webpack設定進行打包該模組,會發現報錯:
... ...
TS2551: Property 'myName' does not exist on type 'User'. Did you mean 'name'?
ts-loader-default_e3b0c44298fc1c14
webpack 5.74.0 compiled with 1 error in 2665 ms
error Command failed with exit code 1.
info Visit https://yarnpkg.com/en/docs/cli/run for documentation about this command.
可以看得出來,tsc幫助我們提示了型別錯誤的地方,user這個型別並沒有對應的myName欄位。
我們切換一下到babel-loader對該ts檔案進行編譯,居然發現編譯可以直接成功!並且,我們檢查編譯好的js程式碼,會發現這部分:
// dist/index.js
(() => {
"use strict";
// ... ...
var r = function (e) {
// 注意這個地方:依然在使用myName
return "".concat(e.myName, "@").concat(e.age);
};
module.exports = o;
})();
編譯好的js程式碼就在直接使用myName欄位。為什麼型別檢查失效了?還記得我們前面提到的babel怎麼處理ts的?
Babel 如何處理 TypeScript 程式碼?它刪除它。
是的,它刪除了所有 TypeScript,將其轉換為「常規的」 JavaScript,並繼續以它自己的方式愉快處理。
是的,babel並沒有進行型別檢查,而是將各種型別移除掉以達到快速完成編譯的目的。那麼問題來了,我們如何讓babel進行型別判斷呢?實際上,我們沒有辦法讓babel進行型別判斷,必須要藉助另外的工具進行。那為什麼我們的IDE卻能夠現實ts程式碼的錯誤呢?因為IDE幫助我們進行了型別判斷。
不知道有沒有細心的讀者在使用IDEA的時候,發現一個ts專案的IDEA右下角展示了typescript:
VSCode也能看到類似:
在同一臺電腦上,甚至發現IDEA和VSCode的typescript版本都還不一樣(4.7.4和4.7.3)。這是怎麼一回事呢?實際上,IDE檢測到你所在的專案是一個ts專案的時候(或包含ts檔案),就會自動的啟動一個ts的檢測服務,專門用於所在專案的ts型別檢測。這個ts型別檢測服務,是通過每個IDE預設情況下自帶的typescript中的tsc進行型別檢測。
但是,我們可以全域性安裝(npm -g)或者是為每個專案單獨安裝typescript,然後就可以讓IDE選擇啟動獨立安裝的typescript。比如,我們在本專案中,安裝一個特定版本的ts(版本4.7.2):
yarn add -D [email protected]
在IDEA中,設定 - Languages & Frameworks - TypeScript中,就可以選擇IDEA啟動的4.7.2版本的TypeScript為我們專案提供型別檢查(注意看選項中有一個Bundled的TS,版本是4.7.4,就是預設的):
IDE之所以能夠在對應的程式碼位置展示程式碼的型別錯誤,流程如下:
但是,ts型別檢查也要有一定的依據。譬如,有些型別定義的檔案從哪裡查詢,是否允許較新的語法等,這些設定依然是由tsconfig.json來提供的,但若未提供,則IDE會使用一份預設的設定。如果要進行型別檢測的自定義設定,則需要提供tsconfig.json。
還記得我們前面的ts-loader嗎?在程式碼編譯期,ts-loader呼叫tsc,tsc讀取專案目錄下的tsconfig.json設定。而咱們編寫程式碼的時候,又讓IDE的ts讀取該tsconfig.json組態檔進行型別檢查。
對於ts-loader專案體系來說,ts程式碼編譯和ts的型別檢測如下:
然而,對於babel-loader專案體系就不像ts-loader那樣了:
在babel-loader體系中,程式碼的編譯只取決於babel部分的處理,根型別沒有根本的關係,而型別檢查使用到的tsconfig和tsc則只作用在型別檢查的部分,根ts程式碼編譯沒有任何關係。