編譯TS 代碼用TSC 還是Babel?
編譯 TypeScript 代碼用什么編譯器?
那還用說,肯定是 ts 自帶的 compiler 呀。
但其實 babel 也能編譯 ts 代碼,那用 babel 和 tsc 編譯 ts 代碼有什么區(qū)別呢?
我們分別來看一下:
tsc 的編譯流程
typescript compiler 的編譯流程是這樣的:
源碼要先用 Scanner 進行詞法分析,拆分成一個個不能細分的單詞,叫做 token。
然后用 Parser 進行語法分析,組裝成抽象語法樹(Abstract Syntax Tree)AST。
之后做語義分析,包括用 Binder 進行作用域分析,和有 Checker 做類型檢查。如果有類型的錯誤,就是在 Checker 這個階段報的。
如果有 Transformer 插件(tsc 支持 custom transform),會在 Checker 之后調用,可以對 AST 做各種增刪改。
類型檢查通過后就會用 Emmiter 把 AST 打印成目標代碼,生成類型聲明文件 d.ts,還有 sourcemap。
sourcemap 的作用是映射源碼和目標代碼的代碼位置,這樣調試的時候打斷點可以定位到相應的源碼,線上報錯的時候也能根據(jù) sourcemap 定位到源碼報錯的位置。
tsc 生成的 AST 可以用 astexplorer.net 可視化的查看:
生成的目標代碼和 d.ts 和報錯信息也可以用 ts playground 來直接查看:
大概了解了 tsc 的編譯流程,我們再來看下 babel 的:
babel 的編譯流程
babel 的編譯流程是這樣的:
源碼經(jīng)過 Parser 做詞法分析和語法分析,生成 token 和 AST。
AST 會做語義分析生成作用域信息,然后會調用 Transformer 進行 AST 的轉換。
最后會用 Generator 把 AST 打印成目標代碼并生成 sourcemap。
babel 的 AST 和 token 也可以用 astexplorer.net 可視化的查看:
如果想看到 tokens,需要點開設置,開啟 tokens:
而且 babel 也有 playground(babel 的叫 repl) 可以直接看編譯之后生成的代碼:
其實對比下 tsc 的編譯流程,區(qū)別并不大:
Parser 對應 tsc 的 Scanner 和 Parser,都是做詞法分析和語法分析,只不過 babel 沒有細分。
Transform 階段做語義分析和代碼轉換,對應 tsc 的 Binder 和 Transformer。只不過 babel 不會做類型檢查,沒有 Checker。
Generator 做目標代碼和 sourcemap 的生成,對應 tsc 的 Emitter。只不過因為沒有類型信息,不會生成 d.ts。
對比兩者的編譯流程,會發(fā)現(xiàn) babel 除了不會做類型檢查和生成類型聲明文件外,tsc 能做的事情,babel 都能做。
看起來好像是這樣的,但是 babel 和 tsc 實現(xiàn)這些功能是有區(qū)別的:
babel 和 tsc 的區(qū)別
拋開類型檢查和生成 d.ts 這倆 babel 不支持的功能不談,我們看下其他功能的對比:
分別對比下語法支持和代碼生成兩方面:
語法支持
tsc 默認支持最新的 es 規(guī)范的語法和一些還在草案階段的語法(比如 decorators),想支持新語法就要升級 tsc 的版本。
babel 是通過 @babel/preset-env 按照目標環(huán)境 targets 的配置自動引入需要用到的插件來支持標準語法,對于還在草案階段的語法需要單獨引入 @babel/proposal-xx 的插件來支持。
所以如果你只用標準語法,那用 tsc 或者 babel 都行,但是如果你想用一些草案階段的語法,tsc 可能很多都不支持,而 babel 卻可以引入 @babel/poposal-xx 的插件來支持。
從支持的語法特性上來說,babel 更多一些。
代碼生成
tsc 生成的代碼沒有做 polyfill 的處理,想做兼容處理就需要在入口引入下 core-js(polyfill 的實現(xiàn))。
import "core-js";
Promise.resolve;
babel 的 @babel/preset-env 可以根據(jù) targets 的配置來自動引入需要的插件,引入需要用到的 core-js 模塊,
引入方式可以通過 useBuiltIns 來配置:
entry 是在入口引入根據(jù) targets 過濾出的所有需要用的 core-js。
usage 則是每個模塊按照使用到了哪些來按需引入。
module.exports = {
presets: [
[
'@babel/preset-typescript',
'@babel/preset-env',
{
targets: '目標環(huán)境',
useBuiltIns: 'entry' // ‘usage’
}
]
]
}
此外,babel 會注入一些 helper 代碼,可以通過 @babel/plugin-transform-runtime 插件抽離出來,從 @babel/runtime 包引入。
使用 transform-runtime 之前:
使用 transform-runtime 之后:
(transform runtime 顧名思義就是 transform to runtime,轉換成從 runtime 包引入 helper 代碼的方式)
所以一般babel 都會這么配:
module.exports = {
presets: [
[
'@babel/preset-typescript',
'@babel/preset-env',
{
targets: '目標環(huán)境',
useBuiltIns: 'usage' // ‘entry’
}
]
],
plugins: [ '@babel/plugin-transform-runtime']
}
當然,這里不是講 babel 怎么配置,我們繞回主題,babel 和 tsc 生成代碼的區(qū)別:
tsc 生成的代碼沒有做 polyfill 的處理,需要全量引入 core-js,而 babel 則可以用 @babel/preset-env 根據(jù) targets 的配置來按需引入 core-js 的部分模塊,所以生成的代碼體積更小。
看起來用 babel 編譯 ts 代碼全是優(yōu)點?
也不全是,babel 有一些 ts 語法并不支持:
babel 不支持的 ts 語法
babel 是每個文件單獨編譯的,而 tsc 不是,tsc 是整個項目一起編譯,會處理類型聲明文件,會做跨文件的類型聲明合并,比如 namespace 和 interface 就可以跨文件合并。
所以 babel 編譯 ts 代碼有一些特性是沒法支持的:
const enum 不支持
enum 編譯之后是這樣的:
而 const enum 編譯之后是直接替換用到 enum 的地方為對應的值,是這樣的:
const enum 是在編譯期間把 enum 的引用替換成具體的值,需要解析類型信息,而 babel 并不會解析,所以它會把 const enum 轉成 enum 來處理:
namespace 部分支持:不支持 namespace 的合并,不支持導出非 const 的值
比如這樣一段 ts 代碼:
namespace Guang {
export const name = 'guang';
}
namespace Guang {
export const name2 = name;
}
console.log(Guang.name2);
按理說 Guang.name2 是 'dong',因為 ts 會自動合并同名 namespace。
ts 編譯之后的代碼是這樣的:
都掛到了 Guang 這個對象上,所以 name2 就能取到 name 的值。
而 babel 對每個 namespace 都是單獨處理,所以是這樣的:
因為不會做 namespace 的合并,所以 name 為 undefined。
還有 namespace 不支持導出非 const 的值。
ts 的 namespace 是可以導出非 const 的值的,后面可以修改:
但是 babel 并不支持:
原因也是因為不會做 namespace 的解析,而 namespace 是全局的,如果在另一個文件改了 namespace 導出的值,babel 并不能處理。所以不支持對 namespace 導出的值做修改。
除此以外,還有一些語法也不支持:
部分語法不支持
像 export = import = 這種過時的模塊語法并不支持:
開啟了 jsx 編譯之后,不能用尖括號的方式做類型斷言:
我們知道,ts 是可以做類型斷言來修改某個類型到某個類型的,用 as xx 或者尖括號的方式。
但是如果開啟了 jsx 編譯之后,尖括號的形式會和 jsx 的語法沖突,所以就不支持做類型斷言了:
tsc 都不支持,babel 當然也是一樣:
babel 不支持 ts 這些特性,那是否可以用 babel 編譯 ts 呢?
babel 還是 tsc?
babel 不支持 const enum(會作為 enum 處理),不支持 namespace 的跨文件合并,導出非 const 的值,不支持過時的 export = import = 的模塊語法。
這些其實影響并不大,只要代碼里沒用到這些語法,完全可以用 babel 來編譯 ts。
babel 編譯 ts 代碼的優(yōu)點是可以通過插件支持更多的語言特性,而且生成的代碼是按照 targets 的配置按需引入 core-js 的,而 tsc 沒做這方面的處理,只能全量引入。
而且 tsc 因為要做類型檢查所以是比較慢的,而 babel 不做類型檢查,編譯會快很多。
那用 babel 編譯,就不做類型檢查了么?
可以用 tsc --noEmit 來做類型檢查,加上 noEmit選項就不會生成代碼了。
如果你要生成 d.ts,也要單獨跑下 tsc 編譯。
總結
babel 和 tsc 的編譯流程大同小異,都有把源碼轉換成 AST 的 Parser,都會做語義分析(作用域分析)和 AST 的 transform,最后都會用 Generator(或者 Emitter)把 AST 打印成目標代碼并生成 sourcemap。
但是 babel 不做類型檢查,也不會生成 d.ts 文件。
tsc 支持最新的 es 標準特性和部分草案的特性(比如 decorator),而 babel 通過 @babel/preset-env 支持所有標準特性,也可以通過 @babel/proposal-xx 來支持各種非標準特性,支持的語言特性上 babel 更強一些。
tsc 沒有做 polyfill 的處理,需要全量引入 core-js,而 babel 的 @babel/preset-env 會根據(jù) targets 的配置按需引入 core-js,引入方式受 useBuiltIns 影響 (entry 是在入口引入 targets 需要的,usage 是每個模塊引入用到的)。
但是 babel 因為是每個文件單獨編譯的(tsc 是整個項目一起編譯),而且也不解析類型,所以 const enum,namespace 合并,namespace 導出非 const 值并不支持。而且過時的 export = 的模塊語法也不支持。
但這些影響不大,完全可以用 babel 編譯 ts 代碼來生成體積更小的代碼,不做類型檢查編譯速度也更快。
如果想做類型檢查可以單獨執(zhí)行 tsc --noEmit。
當然,文中只是討論了 tsc 和 babel 編譯 ts 代碼的區(qū)別,并沒有說最好用什么,具體用什么編譯 ts,大家可以根據(jù)場景自己選擇。