你可能並沒有理解的 babel 配置的原理

babel 是一個 JS、TS 的編譯器,它能把新語法寫的代碼轉換成目標環境支持的語法的代碼,並且對目標環境不支持的 api 自動 polyfill。

babel 基本每個項目都用,大家可能對 @babel/preset-env 和 @babel/plugin-transform-runtime 都很熟悉了,但是你真的理解它們麼?

相信很多同學只是知道它能幹什麼,但不知道它是怎麼實現的,這篇文章我們就來深入下它們的實現原理吧。

首先,我們先來試一下 preset-env 和 plugin-transform-runtime 的功能:

功能測試

@babel/preset-env 的作用是根據 targets 的配置引入對應插件來實現編譯和 polyfill。

比如這段代碼:

class Dong {
}

在低版本瀏覽器不支持,會做語法轉換。

我們把 targets 指定成比較低版本的瀏覽器,比如 chrome 30,並且打開 debug 選項,它的作用是會打印用到的 plugin。

{
    presets: [
        ['@babel/preset-env'{
            targets: 'chrome 30',
            debug: true,
            useBuiltIns: 'usage',
            corejs: 3
        }]
    ]
}

執行 babel 就會發現它用到了這些插件:

這就是 @babel/preset-env 的意義,自動根據 targets 來引入需要的插件,不然要是手動寫這麼一堆插件不得麻煩死。

開啓 polyfill 功能要指定它的引入方式,也就是 useBuiltIns。設置爲 usage 是在每個模塊引入用到的,設置爲 entry 是統一在入口處引入 targets 需要的。

polyfill 的實現就是 core-js,需要再指定下 corejs 版本,一般是指定 3,這個會 polyfill 實例方法,而 corejs2 不會。

上面一段代碼會轉換成這樣:

注入了 3 個 helper,也就是 _createClass 這種以下劃線開頭的輔助方法。

因爲 helper 方法裏用到了 Object.defineProperty 的 api,這裏也會從 core-js 裏引入。

我們再測試一下這樣一段代碼:

async function func() {
}

會被轉換成這樣:

除了注入 core-js、helper 代碼外,還注入了 regenerator 代碼,這個是 async await 的實現。

綜上,babel runtime 包含的代碼就 core-js、helper、regenerator 這三種。

@babel/preset-env 的處理方式是 helper 代碼直接注入、regenerator、core-js 代碼全局引入。

這樣就會導致多個模塊重複注入同樣的代碼,會污染全局環境。

解決這個問題就要使用 @babel/plugin-transform-runtime 插件了。

我們在配置文件裏引入這個插件:

{
    presets: [
        ['@babel/preset-env'{
            targets: 'chrome 30',
            debug: true,
            useBuiltIns: 'usage',
            corejs: 3
        }]
    ],
    plugins: [
        ['@babel/plugin-transform-runtime'{
            corejs: 3
        }]
    ]
}

注意,這個插件也是處理 polyfill ,也就同樣需要指定 corejs 的版本。

然後測試下引入之後有什麼變化:

先測試 class 那個案例:

之前是這樣的:

現在變成了這樣:

變成了從 @babel/runtime-corejs3 引入的形式,這樣就不會多個模塊重複注入同樣的實現代碼了,而且 core-js 的 api 也不是全局引入了,變成了模塊化引入。

這樣就解決了 corejs 的重複注入和全局引入 polyfill 的兩個問題。

再測試 async function 那個案例:

之前是這樣的:

同樣有全局引入和重複注入的問題。

引入 transform-runtime 插件之後是這樣的:

也是同樣的方式解決了那兩個問題。

再來測試一個 api 的,用這樣一段代碼:

new WeakMap();

當只配置 preset-env 時:

{
    presets: [
        ['@babel/preset-env'{
            targets: 'chrome 30',
            debug: true,
            useBuiltIns: 'usage',
            corejs: 3
        }]
    ]
}

結果是這樣的:

再加上 @babel/plugin-transform-runtime 後:

{
    presets: [
        ['@babel/preset-env'{
            targets: 'chrome 30',
            debug: true,
            useBuiltIns: 'usage',
            corejs: 3
        }]
    ],
    plugins: [
        ['@babel/plugin-transform-runtime',
            {
                corejs: 3
            }
        ]
    ]
}

結果是這樣的:

這樣我們就清楚了 @babel/plugin-transform-runtime 的功能,把注入的代碼和 core-js 全局引入的代碼轉換成從 @babel/runtime-corejs3 中引入的形式。

@babel/runtime-corejs3 就包含了 helpers、core-js、regenerator 這 3 部分。

功能我們都清楚了,那它們是怎麼實現的呢?

實現原理

preset-env 的原理之前講過,就是根據 targets 的配置查詢內部的 @babe/compat-data 的數據庫,過濾出目標環境不支持的語法和 api,引入對應的轉換插件。

targets 使用 browserslist 來解析成具體的瀏覽器和版本:

然後根據 @babel/compact-data 的數據來過濾出這些瀏覽器支持的語法和 api:

然後去掉這些已經支持的語法和 api 對應的插件,剩下的就是需要用的轉換插件:

這就是 preset-env 的根據 targtes 來按需轉換語法和 polyfill 的原理。

那 @babel/plugin-transform-runtime 呢?它是怎麼實現的?

這個插件的原理是因爲 babel 插件和 preset 生效的順序是這樣的(下面是官網文檔的截圖):

先插件後 preset,插件從左往右,preset 從右往左。

這就導致了 @babel/plugin-transform-runtime 是在 @babel/preset-env 之前調用的,提前做了 api 的轉換,那到了 @babel/preset-env 就沒什麼可轉了,也就實現了 polyfill 的抽取。

它的源碼是這樣的:

會根據配置來引入 corejs、regenerator 的轉換插件,實現 polyfill 注入的功能。

並且還設置了一個 helperGenerator 的函數到全局上下文 file,這樣後面 @babel/preset-env 就可以用它來生成 helper 代碼。那自然也就是抽離的了。

這就是 @babel/plugin-transform-runtime 的原理:

因爲插件在 preset 之前調用,所以可以提前把 polyfill 轉換了,而且注入了 helpGenerator 來修改 @babel/preset-env 生成 helper 代碼的行爲。

原理我們理清了,但是大家有沒有發現其中的問題:

現有方案的問題

我們通過 @babel/plugin-transform-runtime  提前把 polyfill 轉換了,但是這個插件裏沒有 targets 的設置呀,不是按需轉換的,那就會多做一些沒必要的轉換。

這個其實是已知問題,可以在 babel 的項目裏找到這個 issue:

當然官方也提出瞭解決的方案,只不過這個得等 babel 新版本更新再用了,等 babel8 吧。

總結

babel7 以後,我們只需要使用 @babel/preset-env,指定目標環境的 targets,babel 就會根據內部的兼容性數據庫查詢出該環境不支持的語法和 api,進行對應插件的引入,從而實現按需的語法轉換和 polyfill 引入。

但是 @babel/preset-env 轉換用到的一些輔助代碼(helper)是直接注入到模塊裏的,沒有做抽離,多個模塊可能會重複注入。並且用到的 polyfill 代碼也是全局引入的,可能污染全局環境。爲了解決這兩個問題我們會使用 @babel/plugin-transform-runtime 插件來把注入的代碼抽離,把全局的引入改爲從 @babel/runtime-corejs3 引入的方式。

runtime 包包含 core-js、regenerator、helper 三部分。

@babel/plugin-transform-runtime 能生效的原理是因爲插件先於 preset 被調用,提前把那些 api 做了轉換,並且設置了 preset-env 生成 helper 的方式。

但是這個轉換和 preset-env 是獨立的,它沒有 targets 的配置,這就導致了不能按需 polyfill,會進行一些不必要的轉換。這個是已知的 issue,等 babel 版本更新吧。

看到這裏,你對 babel 的配置和這些配置的原理是否有更深的理解了呢。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/2lH3AtlwhsyJc92E9_8YdA