babel 最開始叫 6to5,顧名思義,功能是 es6 轉 es5。咱們知道,es 版本一年一個,有了 es7(es2016)、es8(es2017)等等。顯然,6to5 的名字已經不合適了,因此 6to5 更名爲了 babel。javascript
babel 來自巴別塔的典故:java
當時人類聯合起來興建但願能通往天堂的高塔,爲了阻止人類的計劃,上帝讓人類說不一樣的語言,令人類相互之間不能溝通,計劃所以失敗,人類自此各散東西。此事件,爲世上出現不一樣語言和種族提供解釋。這座塔就是巴別塔。node
這個巴別塔的典故很符合 babel 的轉譯器的定位。react
babel 從最初到如今一直的目的都很明確,就是把源碼中的新語法和 api 轉成目標瀏覽器支持的。它採用了微內核的架構,整個流程比較精簡,全部的轉換功能都是經過插件來完成的。git
babel 的編譯流程就是 parse、transform、generate 3步, parse 是把源碼轉成 AST,transform 是對 AST 的轉換,generate 是把 AST 轉成目標代碼,而且生成 sourcemap。es6
在 transform 階段,會應用各類內置的插件來完成 AST 的轉換。內置插件作的轉換包括兩部分,一是把不支持的語法轉成目標環境支持的語法來實現相同功能,二是不支持的 api 自動引入對應的 polyfill。github
babel 的編譯流程和目的從沒有變過,可是完成這個目的的方式卻變化很大,咱們來回顧一下 babel 6
,babel 7
都是怎麼設計的,babel 8
又會怎麼作,或許能幫你真正理解 babel。數據庫
es 的標準一年一個版本,也就意味着 babel 插件要實時的去跟進,一年實現一系列插件。json
新的語法和 api 進入 es 標準也是有個過程的,這個過程分爲這幾個階段:api
有這麼多特性要 babel 去轉換,每一個特性用一個 babel 插件來作。可是特性多啊,也就是說插件多,總不能讓用戶本身去配一個個插件吧,因此 babel 6 引入了 preset 的概念,就是 plugin 的集合。
若是咱們想用 es6 語法就用 babel-preset-es2015,es7 就在引入 babel-preset-es2016 等等。若是是想用還沒加入標準的特性,則分別用 babel-preset-stage0、babel-preset-stage1 等來引入。這樣經過選擇不一樣的 preset,加上手動引入一些插件,就是全部 babel 會作的轉換。
能夠把這個過程理解爲集合求並集的過程。
並集的結果就是全部支持的特性。
babel 6 就是經過這樣的方式來支持各類目標環境不支持的特性轉換的配置。
細想一下,這樣的方式有沒有問題?
這樣雖然能達到目的,可是是有問題的,主要有兩點:
es 的標準每一年都在變,如今的 stage-0 可能很快就 stage-2 了,那 preset 怎麼維護,要不要跟着變,用戶怎麼知道這個 stage-x 都支持什麼特性?
只能轉成 es5,那目標環境支持一些 es6 特性了,那這些轉換和 polyfill 豈不是無用功? 並且還增長了產物的體積。
polyfill 手動引入,比較麻煩,有沒有更好的方式
這兩個問題是 babel 6 的時候一直存在的。因此這種方案算是及格,可是仍是有問題的,咱們給 70 分不過度吧。 (能完成功能就能夠給 60 分,多加 10 分是給 babel 6 引入的 preset,確實簡化了不少配置)
那怎麼解決 babel 6 的問題呢?babel 7 給出了答案。
babel 7 改動挺大的,好比全部的包都遷移到了 @babel 的 scope 下,也就是 @babel/xxx,這些咱們無論,只看 babel 7 是怎麼解決 babel 6 的問題的,
babel 7 廢棄了 preset-20xx 和 preset-stage-x 的 preset 包,而換成了 preset-env,preset-env 默認會支持全部 es 標準的特性,若是沒進入標準的,再也不封裝成 preset,須要手動指定 plugin-proposal-xxx。
它的集合是這樣的:
是否是比起 babel 6 更簡單了。
(preset-react 等不是 es 標準語法,也沒有啥變化,就不包括在裏面了)。
可是 preset 和 plugin proposal 的改變只是解決了以前的 preset 常常變的問題。那麼多轉換了一些環境支持的特性,這個問題是怎麼解決的呢?
答案是 compat-table,它給出了每一個特性在不一樣瀏覽器或者 node 環境中的最低支持版本,babel 基於這個本身維護了一份數據庫,在 @babel/compat-data 下。
其中有每一個特性在不一樣環境的什麼版本支持的數據:
有了這些數據,那麼只要用戶指定他的目標環境是啥就能夠了,這時候能夠用 browserslist 的 query 來寫,好比 last 1 version, > 1% 這種字符串,babel 會使用 brwoserslist 來把它們轉成目標環境具體版本的數據。
有了不一樣特性支持的環境的最低版本的數據,有了具體的版本,那麼過濾出來的就是目標環境不支持的特性,而後引入它們對應的插件便可。這就是 preset-env 作的事情。
配置方式好比:
{
"presets": [["@babel/preset-env", { "targets": "> 0.25%, not dead" }]]
}
複製代碼
這樣就經過 preset-env 解決了轉換了目標環境已經支持的特性的問題。其實 polyfill 也能夠經過 targets 來過濾。
再也不手動引入 polyfill,那麼怎麼引入? 固然是用 preset-env 自動引入了。可是也不是默認就會啓用這個功能,須要配置。
{
"presets": [["@babel/preset-env", {
"targets": "> 0.25%, not dead",
"useBuiltIns": "usage",// or "entry" or "false"
"corejs": 3
}]]
}
複製代碼
配置下 corejs 和 useBuiltIns。
corejs 就是 babel 7 所用的 polyfill,須要指定下版本,corejs 3 才支持實例方法(好比 Array.prototype.fill )的 polyfill。
useBuiltIns 就是使用 polyfill (corejs)的方式,是在入口處所有引入(entry),仍是每一個文件引入用到的(usage),或者不引入(false)。
配置了這兩個 option 就能夠自動引入 polyfill 了。
polyfill 默認是全局引入的,有的時候不想污染全局變量就要用 @babel/plugin-transform-runtime 轉換下。(這個插件 babel 6 就有了)。
這樣就再也不污染全局環境了,而是使用一個惟一的標識符來引入。
看起來,babel 7 好像已經很完美了,能夠打 90 多分了?
不是的,babel 7 有 babel 7 的問題。
babel 7 的問題
@babel/plugin-transform-runtime 是不支持配置 targets 的,由於不知道目標環境支持啥,它只能所有作轉換。你可能說不是有 preset-env 麼?
babel 中插件的應用順序是:先 plugin 再 preset,plugin 從左到右,preset 從右到左,這樣 plugin-transform-runtime 是在 preset-env 前面的。
等 @babel/plugin-transform-runtime 轉完了以後,再交給 preset-env 這時候已經作了無用的轉換了。
咱們來試驗一下:
咱們先看一下 Array.prototype.fill 的環境支持狀況:
能夠看到在 Chrome 45 及以上支持這個特性,而在 Chrome 44 就不支持了。
咱們先單獨試一下 preset-env:
當指定 targets 爲 Chrome 44 時,應該自動引入polyfill:
當指定 targets 爲 Chrome 45 時,不須要引入polyfill:
結果都符合預期,44 引入,45 不引入。
咱們再來試試 @babel/plugin-transform-runtime:
是否是發現問題了,Chrome 45 不是支持 Array.prototype.fill 方法麼,爲啥仍是引入了 polyfill。
因而我就去問了下做者,提了個 feature request,做者說能夠用最新的 @babel/polyfills 解決了這個問題.
我去看了下,這個包還在試驗階段,確實解決了這個問題。
這個包估計在 babel 8 會內置到 babel。
那麼給 babel 7 打個分吧,原本 preset-env 的引入使咱們能更精準的轉換代碼和引入 polyfill,想給 90 分,可是 plugin-transform-runtime 的問題讓我給它減了 10 分,綜合給 80 分吧。
babel 8 還沒出來,可是咱們知道 babel 再怎麼更新也是圍繞主線來的,也就是對目標環境不支持的特性自動進行精準的轉換和 polyfill。每一個版本都是解決了上個版本的問題的,babel 8 的 @babel/polyfills 包就解決了 babel 7 的 @babel/plugin-transform-runtime 的遺留問題,能夠經過 targets 來按需精準引入 polyfill 了。
它支持配置一個 polyfill provider,也就是說你能夠指定 corejs二、corejs三、es-shims 等 polyfill,還能夠自定義 polyfil,也就是你可使用本身的 polyfill。
而後有了 polyfill 源以後,使用 polyfill 的方式也把以前 transform-runtime 作的事情內置了,也就是從以前的 useBuiltIns: entry、 useBuiltIns: usage 的兩種,變成了 3 種:
其實這三種方式 babel 7 也支持,可是如今再也不須要插件了,並且還支持了 polyfill provider 的配置,因此到了 babel 8 的階段, @babel/preset-env 纔是功能完備的。
那麼插件若是想用 targets 該怎麼用呢?
由於我最近在寫 《babel 插件通關祕籍》 的小冊,因此比較關注對插件的影響,我就問了一下 babel 維護者是否是須要在 @babel/core 調用插件的時候注入到 api 中,讓插件能夠拿到 targets。
上午問的,下午我就驚喜的發現 babel 文檔補充了 @babel/helper-compilation-targets 的文檔。helper 是用於插件之間複用代碼的方式,也就是給插件開發用的庫。
我看了下,這個庫提供了 3 個 api:
分別對應咱們前面聊到的須要 先經過 query 肯定目標環境,而後對目標環境作過濾,以後判斷某個插件是否須要的 3個階段。
插件裏面經過 api.targets() 拿到環境的配置,而後經過 isRequired 來肯定某個插件有沒有必要用。
這樣,不論是內置 plugin 和 preset 的實現方式也好,仍是插件所能用的 api 也好,都完美支持了 targets,到了這個階段 targets 纔算真正融入進了 babel 中。
這個階段的 babel,我以爲已經能夠給出 90 分的分數了:
支持按照配置的目標環境按需進行 polyfill 和 transform,支持 polyfill 的切換和自定義,配置方式也足夠簡單,插件中也能夠用 targets,並且提供了方便的 helper 包。
babel 8 還在路上,可是咱們已經可以隱約看到他會是什麼樣子了,其實 babel 從最開始到如今,核心的思路始終沒有變過,就像最開始的名字 6to5 同樣,就是爲了 把目標環境中不支持的語法和 api 進行轉換或 polyfill,儘可能的準確、配置儘可能的簡單、插件更容易書寫能作到更多事情。
因此針對這個目標,babel 一路發展而來, 設計出了 preset(babel 6)、preset-env (babel 7)、polyfill provider(babel 8),plugin-transform-runtime (babel 6)等。
插件可以用的 api、helper 等也愈來愈豐富。
babel 一直在發展,可是目標和本質從未變過。咱們去學習一個東西,也要去抓住它的本質來學,因此我寫了《babel 插件通關祕籍》 的小冊(即將上線),但願能幫你「通關」 babel!