你可能并沒(méi)有理解的 Babel 配置的原理
babel 是一個(gè) JS、TS 的編譯器,它能把新語(yǔ)法寫(xiě)的代碼轉(zhuǎn)換成目標(biāo)環(huán)境支持的語(yǔ)法的代碼,并且對(duì)目標(biāo)環(huán)境不支持的 api 自動(dòng) polyfill。
babel 基本每個(gè)項(xiàng)目都用,大家可能對(duì) @babel/preset-env 和 @babel/plugin-transform-runtime 都很熟悉了,但是你真的理解它們么?
相信很多同學(xué)只是知道它能干什么,但不知道它是怎么實(shí)現(xiàn)的,這篇文章我們就來(lái)深入下它們的實(shí)現(xiàn)原理吧。
首先,我們先來(lái)試一下 preset-env 和 plugin-transform-runtime 的功能:
功能測(cè)試
@babel/preset-env 的作用是根據(jù) targets 的配置引入對(duì)應(yīng)插件來(lái)實(shí)現(xiàn)編譯和 polyfill。
比如這段代碼:
class Dong {
}
在低版本瀏覽器不支持,會(huì)做語(yǔ)法轉(zhuǎn)換。
我們把 targets 指定成比較低版本的瀏覽器,比如 chrome 30,并且打開(kāi) debug 選項(xiàng),它的作用是會(huì)打印用到的 plugin。
{
presets: [
['@babel/preset-env', {
targets: 'chrome 30',
debug: true,
useBuiltIns: 'usage',
corejs: 3
}]
]
}
執(zhí)行 babel 就會(huì)發(fā)現(xiàn)它用到了這些插件:
這就是 @babel/preset-env 的意義,自動(dòng)根據(jù) targets 來(lái)引入需要的插件,不然要是手動(dòng)寫(xiě)這么一堆插件不得麻煩死。
開(kāi)啟 polyfill 功能要指定它的引入方式,也就是 useBuiltIns。設(shè)置為 usage 是在每個(gè)模塊引入用到的,設(shè)置為 entry 是統(tǒng)一在入口處引入 targets 需要的。
polyfill 的實(shí)現(xiàn)就是 core-js,需要再指定下 corejs 版本,一般是指定 3,這個(gè)會(huì) polyfill 實(shí)例方法,而 corejs2 不會(huì)。
上面一段代碼會(huì)轉(zhuǎn)換成這樣:
注入了 3 個(gè) helper,也就是 _createClass 這種以下劃線(xiàn)開(kāi)頭的輔助方法。
因?yàn)?helper 方法里用到了 Object.defineProperty 的 api,這里也會(huì)從 core-js 里引入。
我們?cè)贉y(cè)試一下這樣一段代碼:
async function func() {
}
會(huì)被轉(zhuǎn)換成這樣:
除了注入 core-js、helper 代碼外,還注入了 regenerator 代碼,這個(gè)是 async await 的實(shí)現(xiàn)。
綜上,babel runtime 包含的代碼就 core-js、helper、regenerator 這三種。
@babel/preset-env 的處理方式是 helper 代碼直接注入、regenerator、core-js 代碼全局引入。
這樣就會(huì)導(dǎo)致多個(gè)模塊重復(fù)注入同樣的代碼,會(huì)污染全局環(huán)境。
解決這個(gè)問(wèn)題就要使用 @babel/plugin-transform-runtime 插件了。
我們?cè)谂渲梦募镆脒@個(gè)插件:
{
presets: [
['@babel/preset-env', {
targets: 'chrome 30',
debug: true,
useBuiltIns: 'usage',
corejs: 3
}]
],
plugins: [
['@babel/plugin-transform-runtime', {
corejs: 3
}]
]
}
注意,這個(gè)插件也是處理 polyfill ,也就同樣需要指定 corejs 的版本。
然后測(cè)試下引入之后有什么變化:
先測(cè)試 class 那個(gè)案例:
之前是這樣的:
現(xiàn)在變成了這樣:
變成了從 @babel/runtime-corejs3 引入的形式,這樣就不會(huì)多個(gè)模塊重復(fù)注入同樣的實(shí)現(xiàn)代碼了,而且 core-js 的 api 也不是全局引入了,變成了模塊化引入。
這樣就解決了 corejs 的重復(fù)注入和全局引入 polyfill 的兩個(gè)問(wèn)題。
再測(cè)試 async function 那個(gè)案例:
之前是這樣的:
同樣有全局引入和重復(fù)注入的問(wèn)題。
引入 transform-runtime 插件之后是這樣的:
也是同樣的方式解決了那兩個(gè)問(wèn)題。
再來(lái)測(cè)試一個(gè) api 的,用這樣一段代碼:
new WeakMap();
當(dāng)只配置 preset-env 時(shí):
{
presets: [
['@babel/preset-env', {
targets: 'chrome 30',
debug: true,
useBuiltIns: 'usage',
corejs: 3
}]
]
}
結(jié)果是這樣的:
再加上 @babel/plugin-transform-runtime 后:
{
presets: [
['@babel/preset-env', {
targets: 'chrome 30',
debug: true,
useBuiltIns: 'usage',
corejs: 3
}]
],
plugins: [
['@babel/plugin-transform-runtime',
{
corejs: 3
}
]
]
}
結(jié)果是這樣的:
這樣我們就清楚了 @babel/plugin-transform-runtime 的功能,把注入的代碼和 core-js 全局引入的代碼轉(zhuǎn)換成從 @babel/runtime-corejs3 中引入的形式。
@babel/runtime-corejs3 就包含了 helpers、core-js、regenerator 這 3 部分。
功能我們都清楚了,那它們是怎么實(shí)現(xiàn)的呢?
實(shí)現(xiàn)原理
preset-env 的原理之前講過(guò),就是根據(jù) targets 的配置查詢(xún)內(nèi)部的 @babe/compat-data 的數(shù)據(jù)庫(kù),過(guò)濾出目標(biāo)環(huán)境不支持的語(yǔ)法和 api,引入對(duì)應(yīng)的轉(zhuǎn)換插件。
targets 使用 browserslist 來(lái)解析成具體的瀏覽器和版本:
然后根據(jù) @babel/compact-data 的數(shù)據(jù)來(lái)過(guò)濾出這些瀏覽器支持的語(yǔ)法和 api:
然后去掉這些已經(jīng)支持的語(yǔ)法和 api 對(duì)應(yīng)的插件,剩下的就是需要用的轉(zhuǎn)換插件:
這就是 preset-env 的根據(jù) targtes 來(lái)按需轉(zhuǎn)換語(yǔ)法和 polyfill 的原理。
那 @babel/plugin-transform-runtime 呢?它是怎么實(shí)現(xiàn)的?
這個(gè)插件的原理是因?yàn)?babel 插件和 preset 生效的順序是這樣的(下面是官網(wǎng)文檔的截圖):
先插件后 preset,插件從左往右,preset 從右往左。
這就導(dǎo)致了 @babel/plugin-transform-runtime 是在 @babel/preset-env 之前調(diào)用的,提前做了 api 的轉(zhuǎn)換,那到了 @babel/preset-env 就沒(méi)什么可轉(zhuǎn)了,也就實(shí)現(xiàn)了 polyfill 的抽取。
它的源碼是這樣的:
會(huì)根據(jù)配置來(lái)引入 corejs、regenerator 的轉(zhuǎn)換插件,實(shí)現(xiàn) polyfill 注入的功能。
并且還設(shè)置了一個(gè) helperGenerator 的函數(shù)到全局上下文 file,這樣后面 @babel/preset-env 就可以用它來(lái)生成 helper 代碼。那自然也就是抽離的了。
這就是 @babel/plugin-transform-runtime 的原理:
因?yàn)椴寮?preset 之前調(diào)用,所以可以提前把 polyfill 轉(zhuǎn)換了,而且注入了 helpGenerator 來(lái)修改 @babel/preset-env 生成 helper 代碼的行為。
原理我們理清了,但是大家有沒(méi)有發(fā)現(xiàn)其中的問(wèn)題:
現(xiàn)有方案的問(wèn)題
我們通過(guò) @babel/plugin-transform-runtime 提前把 polyfill 轉(zhuǎn)換了,但是這個(gè)插件里沒(méi)有 targets 的設(shè)置呀,不是按需轉(zhuǎn)換的,那就會(huì)多做一些沒(méi)必要的轉(zhuǎn)換。
這個(gè)其實(shí)是已知問(wèn)題,可以在 babel 的項(xiàng)目里找到這個(gè) issue:
當(dāng)然官方也提出了解決的方案,只不過(guò)這個(gè)得等 babel 新版本更新再用了,等 babel8 吧。
總結(jié)
babel7 以后,我們只需要使用 @babel/preset-env,指定目標(biāo)環(huán)境的 targets,babel 就會(huì)根據(jù)內(nèi)部的兼容性數(shù)據(jù)庫(kù)查詢(xún)出該環(huán)境不支持的語(yǔ)法和 api,進(jìn)行對(duì)應(yīng)插件的引入,從而實(shí)現(xiàn)按需的語(yǔ)法轉(zhuǎn)換和 polyfill 引入。
但是 @babel/preset-env 轉(zhuǎn)換用到的一些輔助代碼(helper)是直接注入到模塊里的,沒(méi)有做抽離,多個(gè)模塊可能會(huì)重復(fù)注入。并且用到的 polyfill 代碼也是全局引入的,可能污染全局環(huán)境。為了解決這兩個(gè)問(wèn)題我們會(huì)使用 @babel/plugin-transform-runtime 插件來(lái)把注入的代碼抽離,把全局的引入改為從 @babel/runtime-corejs3 引入的方式。
runtime 包包含 core-js、regenerator、helper 三部分。
@babel/plugin-transform-runtime 能生效的原理是因?yàn)椴寮扔?preset 被調(diào)用,提前把那些 api 做了轉(zhuǎn)換,并且設(shè)置了 preset-env 生成 helper 的方式。
但是這個(gè)轉(zhuǎn)換和 preset-env 是獨(dú)立的,它沒(méi)有 targets 的配置,這就導(dǎo)致了不能按需 polyfill,會(huì)進(jìn)行一些不必要的轉(zhuǎn)換。這個(gè)是已知的 issue,等 babel 版本更新吧。
看到這里,你對(duì) babel 的配置和這些配置的原理是否有更深的理解了呢。