最近幾年,如果你是一名前端開發者,如果你沒有使用甚至聽說過 babel,可能會被當做穿越者吧?

說到 babel,一連串名詞會蹦出來:

babel-cli

babel-core

babel-runtime

babel-node

babel-polyfill

。。。

這些都是 babel 嗎?他們分別是做什麼的?有區別嗎?

babel 到底做了什麼?怎麼做的?

簡單來說把 JavaScript 中 es2015/2016/2017/2046 的新語法轉化為 es5,讓低端執行環境(如瀏覽器和 node )能夠認識並執行。本文以 babel 6。x 為基準進行討論。最近 babel 出了 7。x,放在最後聊。

嚴格來說,babel 也可以轉化為更低的規範。但以目前情況來說,es5 規範已經足以覆蓋絕大部分瀏覽器,因此常規來說轉到 es5 是一個安全且流行的做法。

如果你對 es5/es2015 等等也不瞭解的話,那你可能真的需要先補補課了。

使用方法

總共存在三種方式:

使用單體檔案 (standalone script)

命令列 (cli)

構建工具的外掛 (webpack 的 babel-loader, rollup 的 rollup-plugin-babel)。

其中後面兩種比較常見。第二種多見於 package。json 中的

scripts

段落中的某條命令;第三種就直接整合到構建工具中。

這三種方式只有入口不同而已,呼叫的 babel 核心,處理方式都是一樣的,所以我們先不糾結入口的問題。

執行方式和外掛

babel 總共分為三個階段:解析,轉換,生成。

babel 本身不具有任何轉化功能,它把轉化的功能都分解到一個個 plugin 裡面。因此當我們不配置任何外掛時,經過 babel 的程式碼和輸入是相同的。

外掛總共分為兩種:

當我們新增

語法外掛

之後,在解析這一步就使得 babel 能夠解析更多的語法。(順帶一提,babel 內部使用的解析類庫叫做 babylon,並非 babel 自行開發)

舉個簡單的例子,當我們定義或者呼叫方法時,最後一個引數之後是不允許增加逗號的,如

callFoo(param1, param2,)

就是非法的。如果原始碼是這種寫法,經過 babel 之後就會提示語法錯誤。

但最近的 JS 提案中已經允許了這種新的寫法(讓程式碼 diff 更加清晰)。為了避免 babel 報錯,就需要增加語法外掛

babel-plugin-syntax-trailing-function-commas

當我們新增

轉譯外掛

之後,在轉換這一步把原始碼轉換並輸出。這也是我們使用 babel 最本質的需求。

比起語法外掛,轉譯外掛其實更好理解,比如箭頭函式

(a) => a

就會轉化為

function (a) {return a}

。完成這個工作的外掛叫做

babel-plugin-transform-es2015-arrow-functions

同一類語法可能同時存在語法外掛版本和轉譯外掛版本。

如果我們使用了轉譯外掛,就不用再使用語法外掛了。

配置檔案

既然外掛是 babel 的根本,那如何使用呢?總共分為 2 個步驟:

將外掛的名字增加到配置檔案中 (根目錄下建立 。babelrc 或者 package。json 的

babel

裡面,格式相同)

使用

npm install babel-plugin-xxx

進行安裝

具體書寫格式就不詳述了。

preset

比如 es2015 是一套規範,包含大概十幾二十個轉譯外掛。如果每次要開發者一個個新增並安裝,配置檔案很長不說,

npm install

的時間也會很長,更不談我們可能還要同時使用其他規範呢。

為了解決這個問題,babel 還提供了一組外掛的集合。因為常用,所以不必重複定義 & 安裝。(單點和套餐的差別,套餐省下了巨多的時間和配置的精力)

preset 分為以下幾種:

官方內容,目前包括 env, react, flow, minify 等。這裡最重要的是 env,後面會詳細介紹。

stage-x,這裡麵包含的都是當年最新規範的草案,每年更新。

這裡面還細分為

Stage 0 - 稻草人: 只是一個想法,經過 TC39 成員提出即可。

Stage 1 - 提案: 初步嘗試。

Stage 2 - 初稿: 完成初步規範。

Stage 3 - 候選: 完成規範和瀏覽器初步實現。

Stage 4 - 完成: 將被新增到下一年度釋出。

例如

syntax-dynamic-import

就是 stage-2 的內容,

transform-object-rest-spread

就是 stage-3 的內容。

此外,低一級的 stage 會包含所有高階 stage 的內容,例如 stage-1 會包含 stage-2, stage-3 的所有內容。

stage-4 在下一年更新會直接放到 env 中,所以沒有單獨的 stage-4 可供使用。

es201x, latest

這些是已經納入到標準規範的語法。例如 es2015 包含

arrow-functions

,es2017 包含

syntax-trailing-function-commas

。但因為 env 的出現,使得 es2016 和 es2017 都已經廢棄。所以我們經常可以看到 es2015 被單獨列出來,但極少看到其他兩個。

latest 是 env 的雛形,它是一個每年更新的 preset,目的是包含所有 es201x。但也是因為更加靈活的 env 的出現,已經廢棄。

執行順序

很簡單的幾條原則:

Plugin 會執行在 Preset 之前。

Plugin 會從前到後順序執行。

Preset 的順序則

剛好相反

(從後向前)。

preset 的逆向順序主要是為了保證向後相容,因為大多數使用者的編寫順序是

[‘es2015’, ‘stage-0’]

。這樣必須先執行

stage-0

才能確保 babel 不報錯。因此我們編排 preset 的時候,也要注意順序,

其實只要按照規範的時間順序列出即可。

外掛和 preset 的配置項

簡略情況下,外掛和 preset 只要列出字串格式的名字即可。但如果某個 preset 或者外掛需要一些配置項(或者說引數),就需要把自己先變成陣列。第一個元素依然是字串,表示自己的名字;第二個元素是一個物件,即配置物件。

最需要配置的當屬 env,如下:

“presets”

// 帶了配置項,自己變成陣列

// 第一個元素依然是名字

“env”

// 第二個元素是物件,列出配置項

{

“module”

false

}

],

// 不帶配置項,直接列出名字

“stage-2”

env (重點)

因為 env 最為常用也最重要,所以我們有必要重點關注。

env 的核心目的是透過配置得知目標環境的特點,然後只做必要的轉換。例如目標瀏覽器支援 es2015,那麼 es2015 這個 preset 其實是不需要的,於是程式碼就可以小一點(一般轉化後的程式碼總是更長),構建時間也可以縮短一些。

如果不寫任何配置項,env 等價於 latest,也等價於 es2015 + es2016 + es2017 三個相加(不包含 stage-x 中的外掛)。env 包含的外掛列表維護在這裡

下面列出幾種比較常用的配置方法:

{

“presets”

“env”

{

“targets”

{

“browsers”

“last 2 versions”

“safari >= 7”

}

}]

}

如上配置將考慮所有瀏覽器的最新2個版本(safari大於等於7。0的版本)的特性,將必要的程式碼進行轉換。而這些版本已有的功能就不進行轉化了。這裡的語法可以參考 browserslist

{

“presets”

“env”

{

“targets”

{

“node”

“6。10”

}

}]

}

如上配置將目標設定為 nodejs,並且支援 6。10 及以上的版本。也可以使用

node: ‘current’

來支援最新穩定版本。例如箭頭函式在 nodejs 6 及以上將不被轉化,但如果是 nodejs 0。12 就會被轉化了。

另外一個有用的配置項是

modules

。它的取值可以是

amd

umd

systemjs

commonjs

false

。這可以讓 babel 以特定的模組化格式來輸出程式碼。如果選擇

false

就不進行模組化處理。

其他配套工具

以上討論了 babel 的核心處理機制和配置方法等,不論任何入口呼叫 babel 都走這一套。但文章開頭提的那一堆

babel-*

還是讓人一頭霧水。實際上這些

babel-*

大多是不同的入口(方式)來使用 babel,下面來簡單介紹一下。

babel-cli

顧名思義,cli 就是命令列工具。安裝了

babel-cli

就能夠在命令列中使用

babel

命令來編譯檔案。

在開發 npm package 時經常會使用如下模式:

babel-cli

安裝為

devDependencies

在 package。json 中新增

scripts

(比如

prepublish

),使用

babel

命令編譯檔案

npm publish

這樣既可以使用較新規範的 JS 語法編寫原始碼,同時又能支援舊版環境。因為專案可能不太大,用不到構建工具 (webpack 或者 rollup),於是在釋出之前用

babel-cli

進行處理。

babel-node

babel-node

babel-cli

的一部分,它不需要單獨安裝。

它的作用是在 node 環境中,直接執行 es2015 的程式碼,而不需要額外進行轉碼。例如我們有一個 js 檔案以 es2015 的語法進行編寫(如使用了箭頭函式)。我們可以直接使用

babel-node es2015。js

進行執行,而不用再進行轉碼了。

可以說:

babel-node

=

babel-polyfill

+

babel-register

。那這兩位又是誰呢?

babel-register

babel-register 模組改寫

require

命令,為它加上一個鉤子。此後,每當使用

require

載入

。js

。jsx

。es

。es6

字尾名的檔案,就會先用 babel 進行轉碼。

使用時,必須首先載入

require(‘babel-register’)

需要注意的是,babel-register 只會對

require

命令載入的檔案轉碼,而

不會對當前檔案轉碼

另外,由於它是實時轉碼,所以

只適合在開發環境使用

babel-polyfill

babel 預設只轉換 js 語法,而不轉換新的 API,比如 Iterator、Generator、Set、Maps、Proxy、Reflect、Symbol、Promise 等全域性物件,以及一些定義在全域性物件上的方法(比如

Object。assign

)都不會轉碼。

舉例來說,es2015 在 Array 物件上新增了

Array。from

方法。babel 就不會轉碼這個方法。如果想讓這個方法執行,必須使用

babel-polyfill

。(內部集成了

core-js

regenerator

使用時,在所有程式碼執行之前增加

require(‘babel-polyfill’)

。或者更常規的操作是在

webpack。config。js

中將

babel-polyfill

作為第一個 entry。因此必須把

babel-polyfill

作為

dependencies

而不是

devDependencies

babel-polyfill

主要有兩個缺點:

使用

babel-polyfill

會導致打出來的包非常大,因為

babel-polyfill

是一個整體,把所有方法都加到原型鏈上。比如我們只使用了

Array。from

,但它把

Object。defineProperty

也給加上了,這就是一種浪費了。這個問題可以透過單獨使用

core-js

的某個類庫來解決,

core-js

都是分開的。

babel-polyfill

會汙染全域性變數,給很多類的原型鏈上都作了修改,如果我們開發的也是一個類庫供其他開發者使用,這種情況就會變得非常不可控。

因此在實際使用中,如果我們無法忍受這兩個缺點(尤其是第二個),通常我們會傾向於使用

babel-plugin-transform-runtime

但如果程式碼中包含高版本 js 中型別的例項方法 (例如

[1,2,3]。includes(1)

),這還是要使用 polyfill。

babel-runtime 和 babel-plugin-transform-runtime (重點)

我們時常在專案中看到 。babelrc 中使用

babel-plugin-transform-runtime

,而

package。json

中的

dependencies

(注意不是

devDependencies

) 又包含了

babel-runtime

,那這兩個是不是成套使用的呢?他們又起什麼作用呢?

先說

babel-plugin-transform-runtime

babel 會轉換 js 語法,之前已經提過了。以

async/await

舉例,如果不使用這個 plugin (即預設情況),轉換後的程式碼大概是:

// babel 新增一個方法,把 async 轉化為 generator

function

_asyncToGenerator

fn

{

return

function

()

{。。。。}}

// 很長很長一段

// 具體使用處

var

_ref

=

_asyncToGenerator

function

*

arg1

arg2

{

yield

0

something

)(

arg1

arg2

);

});

不用過於糾結具體的語法,只需看到,這個

_asyncToGenerator

在當前檔案被定義,然後被使用了,以替換原始碼的

await

。但每個被轉化的檔案都會插入一段

_asyncToGenerator

這就導致重複和浪費了。

在使用了

babel-plugin-transform-runtime

了之後,轉化後的程式碼會變成

// 從直接定義改為引用,這樣就不會重複定義了。

var

_asyncToGenerator2

=

require

‘babel-runtime/helpers/asyncToGenerator’

);

var

_asyncToGenerator3

=

_interopRequireDefault

_asyncToGenerator2

);

// 具體使用處是一樣的

var

_ref

=

_asyncToGenerator3

function

*

arg1

arg2

{

yield

0

something

)(

arg1

arg2

);

});

從定義方法改成引用,那重複定義就變成了重複引用,就不存在程式碼重複的問題了。

但在這裡,我們也發現

babel-runtime

出場了,它就是這些方法的集合處,也因此,

在使用 babel-plugin-transform-runtime 的時候必須把 babel-runtime 當做依賴。

再說

babel-runtime

,它內部集成了

core-js

: 轉換一些內建類 (

Promise

Symbols

等等) 和靜態方法 (

Array。from

等)。絕大部分轉換是這裡做的。自動引入。

regenerator

: 作為

core-js

的拾遺補漏,主要是

generator/yield

async/await

兩組的支援。當代碼中有使用

generators/async

時自動引入。

helpers, 如上面的

asyncToGenerator

就是其中之一,其他還有如

jsx

classCallCheck

等等,可以檢視 babel-helpers。在程式碼中有內建的 helpers 使用時(如上面的第一段程式碼)移除定義,並插入引用(於是就變成了第二段程式碼)。

babel-plugin-transform-runtime

不支援

例項方法 (例如

[1,2,3]。includes(1)

此外補充一點,把 helpers 抽離並統一起來,避免重複程式碼的工作還有一個 plugin 也能做,叫做

babel-plugin-external-helpers

。但因為我們使用的

transform-runtime

已經包含了這個功能,因此不必重複使用。而且 babel 的作者們也已經開始討論這兩個外掛過於類似,正在討論在 babel 7 中把

external-helpers

刪除,討論在 issue#5699 中。

babel-loader

前面提過 babel 的三種使用方法,並且已經介紹過了

babel-cli

。但一些大型的專案都會有構建工具 (如 webpack 或 rollup) 來進行程式碼構建和壓縮 (uglify)。理論上來說,我們也可以對壓縮後的程式碼進行 babel 處理,但那會非常慢。因此如果在 uglify 之前就加入 babel 處理,豈不完美?

所以就有了 babel 插入到構建工具內部這樣的需求。以(我還算熟悉的) webpack 為例,webpack 有 loader 的概念,因此就出現了

babel-loader

babel-cli

一樣,

babel-loader

也會讀取 。babelrc 或者 package。json 中的

babel

段作為自己的配置,之後的核心處理也是相同。唯一比

babel-cli

複雜的是,它需要和 webpack 互動,因此需要在 webpack 這邊進行配置。比較常見的如下:

module

{

rules

{

test

/\。js$/

exclude

/(node_modules|bower_components)/

loader

‘babel-loader’

}

}

如果想在這裡傳入 babel 的配置項,也可以把改成:

// loader: ‘babel-loader’ 改成如下:

use

{

loader

‘babel-loader’

options

{

// 配置項在這裡

}

}

這裡的配置項優先順序是最高的。但我認為放到單獨的配置檔案中更加清晰合理,可讀性強一些。

小結一下

一口(很長的)氣了解 babel

Babel 7。x

最近 babel 釋出了 7。0。因為上面部分都是針對 6。x 編寫的,所以我們關注一下 7。0 帶來的變化(核心機制方面沒有變化,外掛,preset,解析轉譯生成這些都沒有變化)

我只挑選一些和開發者關係比較大的列在這裡,省略的多數是針對某一個 plugin 的改動。完整的列表可以參考官網。

preset 的變更:淘汰 es201x,刪除 stage-x,強推 env (重點)

淘汰 es201x 的目的是把選擇環境的工作交給 env 自動進行,而不需要開發者投入精力。

凡是使用 es201x 的開發者,都應當使用 env 進行替換

。但這裡的淘汰 (原文 deprecated) 並不是刪除,只是不推薦使用了,不好說 babel 8 就真的刪了。

與之相比,stage-x 就沒那麼好運了,它們直接被刪了。這是因為 babel 團隊認為為這些 “不穩定的草案” 花費精力去更新 preset 相當浪費。stage-x 雖然刪除了,但它包含的外掛並沒有刪除(只是被更名了,可以看下面一節),我們依然可以顯式地宣告這些外掛來獲得等價的效果。完整列表

為了減少開發者替換配置檔案的機械工作,babel 開發了一款

babel-upgrade

的工具,它會檢測 babel 配置中的 stage-x 並且替換成對應的 plugins。除此之外它還有其他功能,我們一會兒再詳細看。(總之目的就是讓你更加平滑地遷移到 babel 7)

npm package 名稱的變化 (重點)

這是 babel 7 的一個重大變化,把所有

babel-*

重新命名為

@babel/*

,例如:

babel-cli

變成了

@babel/cli

babel-preset-env

變成了

@babel/preset-env

。進一步,還可以省略

preset

而簡寫為

@babel/env

babel-plugin-transform-arrow-functions

變成了

@babel/plugin-transform-arrow-functions

。和

preset

一樣,

plugin

也可以省略,於是簡寫為

@babel/transform-arrow-functions

這個變化不單單應用於 package。json 的依賴中,包括 。babelrc 的配置 (

plugins

presets

) 也要這麼寫,為了保持一致。例如

{

“presets”: [

- “env”

+ “@babel/preset-env”

}

順帶提一句,上面提過的 babel 解析語法的核心

babylon

現在重新命名為

@babel/parser

,看起來是被收編了。

上文提過的 stage-x 被刪除了,它包含的外掛雖然保留,但也被重新命名了。babel 團隊希望更明顯地區分已經位於規範中的外掛 (如 es2015 的

babel-plugin-transform-arrow-functions

) 和僅僅位於草案中的外掛 (如 stage-0 的

@babel/plugin-proposal-function-bind

)。方式就是在名字中增加

proposal

,所有包含在 stage-x 的轉譯外掛都使用了這個字首,語法外掛不在其列。

最後,如果外掛名稱中包含了規範名稱 (

-es2015-

-es3-

之類的),一律刪除。例如

babel-plugin-transform-es2015-classes

變成了

@babel/plugin-transform-classes

。(這個外掛我自己沒有單獨用過,慚愧)

不再支援低版本 node

babel 7。0 開始不再支援 nodejs 0。10, 0。12, 4, 5 這四個版本,相當於要求 nodejs >= 6 (當前 nodejs LTS 是 8,要求也不算太過分吧)。

這裡的不再支援,指的是在這些低版本 node 環境中不能使用 babel 轉譯程式碼,但 babel 轉譯後的程式碼依然能在這些環境上執行,這點不要混淆。

only 和 ignore 匹配規則的變化

在 babel 6 時,

ignore

選項如果包含

*。foo。js

,實際上的含義 (轉化為 glob) 是

。/**/*。foo。js

,也就是當前目錄

包括子目錄

的所有

foo。js

結尾的檔案。這可能和開發者常規的認識有悖。

於是在 babel 7,相同的表示式

*。foo。js

只作用於當前目錄,不作用於子目錄。如果依然想作用於子目錄的,就要按照 glob 的完整規範書寫為

。/**/*。foo。js

才可以。

only

也是相同。

這個規則變化只作用於萬用字元,不作用於路徑。所以

node_modules

依然包含所有它的子目錄,而不單單隻有一層。(否則全世界開發者都要爆炸)

@babel/node 從 @babel/cli 中獨立了

和 babel 6 不同,如果要使用

@babel/node

,就必須單獨安裝,並新增到依賴中。

babel-upgrade

在提到刪除 stage-x 時候提過這個工具,它的目的是幫助使用者自動化地從 babel 6 升級到 7。

這款升級工具的功能包括:(這裡並不列出完整列表,只列出比較重要和常用的內容)

package。json

把依賴(和開發依賴)中所有的

babel-*

替換為

@babel/*

把這些

@babel/*

依賴的版本更新為最新版 (例如

^7。0。0

如果

scripts

中有使用

babel-node

,自動新增

@babel/node

為開發依賴

如果有

babel

配置項,檢查其中的

plugins

presets

,把短名 (

env

) 替換為完整的名字 (

@babel/preset-env

。babelrc

檢查其中的

plugins

presets

,把短名 (

env

) 替換為完整的名字 (

@babel/preset-env

檢查是否包含

preset-stage-x

,如有替換為對應的外掛並新增到

plugins

使用方式如下:

# 不安裝到本地而是直接執行命令,npm 的新功能

npx babel-upgrade ——write

# 或者常規方式

npm i babel-upgrade -g

babel-upgrade ——write

babel-upgrade

工具本身也還在開發中,還列出了許多 TODO 沒有完成,因此之後的功能可能會更加豐富,例如上面提過的

ignore

的萬用字元轉化等等。