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