最近幾年,若是你是一名前端開發者,若是你沒有使用甚至據說過 babel,可能會被當作穿越者吧?javascript
說到 babel,一連串名詞會蹦出來:前端
這些都是 babel 嗎?他們分別是作什麼的?有區別嗎?java
簡單來講把 JavaScript 中 es2015/2016/2017/2046 的新語法轉化爲 es5,讓低端運行環境(如瀏覽器和 node )可以認識並執行。本文以 babel 6.x 爲基準進行討論。最近 babel 出了 7.x,放在最後聊。node
嚴格來講,babel 也能夠轉化爲更低的規範。但以目前狀況來講,es5 規範已經足以覆蓋絕大部分瀏覽器,所以常規來講轉到 es5 是一個安全且流行的作法。react
若是你對 es5/es2015 等等也不瞭解的話,那你可能真的須要先補補課了。webpack
總共存在三種方式:git
其中後面兩種比較常見。第二種多見於 package.json 中的 scripts
段落中的某條命令;第三種就直接集成到構建工具中。es6
這三種方式只有入口不一樣而已,調用的 babel 內核,處理方式都是同樣的,因此咱們先不糾結入口的問題。github
babel 總共分爲三個階段:解析,轉換,生成。web
babel 自己不具備任何轉化功能,它把轉化的功能都分解到一個個 plugin 裏面。所以當咱們不配置任何插件時,通過 babel 的代碼和輸入是相同的。
插件總共分爲兩種:
舉個簡單的例子,當咱們定義或者調用方法時,最後一個參數以後是不容許增長逗號的,如 callFoo(param1, param2,)
就是非法的。若是源碼是這種寫法,通過 babel 以後就會提示語法錯誤。
但最近的 JS 提案中已經容許了這種新的寫法(讓代碼 diff 更加清晰)。爲了不 babel 報錯,就須要增長語法插件 babel-plugin-syntax-trailing-function-commas
比起語法插件,轉譯插件其實更好理解,好比箭頭函數 (a) => a
就會轉化爲 function (a) {return a}
。完成這個工做的插件叫作 babel-plugin-transform-es2015-arrow-functions
。
同一類語法可能同時存在語法插件版本和轉譯插件版本。若是咱們使用了轉譯插件,就不用再使用語法插件了。
既然插件是 babel 的根本,那如何使用呢?總共分爲 2 個步驟:
babel
裏面,格式相同)npm install babel-plugin-xxx
進行安裝具體書寫格式就不詳述了。
好比 es2015 是一套規範,包含大概十幾二十個轉譯插件。若是每次要開發者一個個添加並安裝,配置文件很長不說,npm install
的時間也會很長,更不談咱們可能還要同時使用其餘規範呢。
爲了解決這個問題,babel 還提供了一組插件的集合。由於經常使用,因此沒必要重複定義 & 安裝。(單點和套餐的差異,套餐省下了巨多的時間和配置的精力)
preset 分爲如下幾種:
官方內容,目前包括 env, react, flow, minify 等。這裏最重要的是 env,後面會詳細介紹。
stage-x,這裏麪包含的都是當年最新規範的草案,每一年更新。
這裏面還細分爲
例如 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 的出現,已經廢棄。
很簡單的幾條原則:
preset 的逆向順序主要是爲了保證向後兼容,由於大多數用戶的編寫順序是 ['es2015', 'stage-0']
。這樣必須先執行 stage-0
才能確保 babel 不報錯。所以咱們編排 preset 的時候,也要注意順序,其實只要按照規範的時間順序列出便可。
簡略狀況下,插件和 preset 只要列出字符串格式的名字便可。但若是某個 preset 或者插件須要一些配置項(或者說參數),就須要把本身先變成數組。第一個元素依然是字符串,表示本身的名字;第二個元素是一個對象,即配置對象。
最須要配置的當屬 env,以下:
"presets": [
// 帶了配置項,本身變成數組
[
// 第一個元素依然是名字
"env",
// 第二個元素是對象,列出配置項
{
"module": false
}
],
// 不帶配置項,直接列出名字
"stage-2"
]
複製代碼
由於 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,下面來簡單介紹一下。
顧名思義,cli 就是命令行工具。安裝了 babel-cli
就可以在命令行中使用 babel
命令來編譯文件。
在開發 npm package 時常常會使用以下模式:
babel-cli
安裝爲 devDependencies
scripts
(好比 prepublish
),使用 babel
命令編譯文件npm publish
這樣既可使用較新規範的 JS 語法編寫源碼,同時又能支持舊版環境。由於項目可能不太大,用不到構建工具 (webpack 或者 rollup),因而在發佈以前用 babel-cli
進行處理。
babel-node
是 babel-cli
的一部分,它不須要單獨安裝。
它的做用是在 node 環境中,直接運行 es2015 的代碼,而不須要額外進行轉碼。例如咱們有一個 js 文件以 es2015 的語法進行編寫(如使用了箭頭函數)。咱們能夠直接使用 babel-node es2015.js
進行執行,而不用再進行轉碼了。
能夠說:babel-node
= babel-polyfill
+ babel-register
。那這兩位又是誰呢?
babel-register 模塊改寫 require
命令,爲它加上一個鉤子。此後,每當使用 require
加載 .js
、.jsx
、.es
和 .es6
後綴名的文件,就會先用 babel 進行轉碼。
使用時,必須首先加載 require('babel-register')
。
須要注意的是,babel-register 只會對 require
命令加載的文件轉碼,而 不會對當前文件轉碼。
另外,因爲它是實時轉碼,因此 只適合在開發環境使用。
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。
咱們時常在項目中看到 .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 的三種使用方法,而且已經介紹過了 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-cli | 容許命令行使用 babel 命令轉譯文件 | |
babel-node | 容許命令行使用 babel-node 直接轉譯+執行 node 文件 | 隨 babel-cli 一同安裝 babel-node = babel-polyfill + babel-register |
babel-register | 改寫 require 命令,爲其加載的文件進行轉碼,不對當前文件轉碼 |
只適用於開發環境 |
babel-polyfill | 爲全部 API 增長兼容方法 | 須要在全部代碼以前 require ,且體積比較大 |
babel-plugin-transform-runtime & babel-runtime | 把幫助類方法從每次使用前定義改成統一 require ,精簡代碼 |
babel-runtime 須要安裝爲依賴,而不是開發依賴 |
babel-loader | 使用 webpack 時做爲一個 loader 在代碼混淆以前進行代碼轉換 |
最近 babel 發佈了 7.0。由於上面部分都是針對 6.x 編寫的,因此咱們關注一下 7.0 帶來的變化(核心機制方面沒有變化,插件,preset,解析轉譯生成這些都沒有變化)
我只挑選一些和開發者關係比較大的列在這裏,省略的多數是針對某一個 plugin 的改動。完整的列表能夠參考官網。
淘汰 es201x 的目的是把選擇環境的工做交給 env 自動進行,而不須要開發者投入精力。凡是使用 es201x 的開發者,都應當使用 env 進行替換。但這裏的淘汰 (原文 deprecated) 並非刪除,只是不推薦使用了,很差說 babel 8 就真的刪了。
與之相比,stage-x 就沒那麼好運了,它們直接被刪了。這是由於 babel 團隊認爲爲這些 「不穩定的草案」 花費精力去更新 preset 至關浪費。stage-x 雖然刪除了,但它包含的插件並無刪除(只是被改名了,能夠看下面一節),咱們依然能夠顯式地聲明這些插件來得到等價的效果。完整列表
爲了減小開發者替換配置文件的機械工做,babel 開發了一款 babel-upgrade
的工具,它會檢測 babel 配置中的 stage-x 而且替換成對應的 plugins。除此以外它還有其餘功能,咱們一下子再詳細看。(總之目的就是讓你更加平滑地遷移到 babel 7)
這是 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
。(這個插件我本身沒有單獨用過,慚愧)
babel 7.0 開始再也不支持 nodejs 0.10, 0.12, 4, 5 這四個版本,至關於要求 nodejs >= 6 (當前 nodejs LTS 是 8,要求也不算太過度吧)。
這裏的再也不支持,指的是在這些低版本 node 環境中不能使用 babel 轉譯代碼,但 babel 轉譯後的代碼依然能在這些環境上運行,這點不要混淆。
在 babel 6 時,ignore
選項若是包含 *.foo.js
,實際上的含義 (轉化爲 glob) 是 ./**/*.foo.js
,也就是當前目錄 包括子目錄 的全部 foo.js
結尾的文件。這可能和開發者常規的認識有悖。
因而在 babel 7,相同的表達式 *.foo.js
只做用於當前目錄,不做用於子目錄。若是依然想做用於子目錄的,就要按照 glob 的完整規範書寫爲 ./**/*.foo.js
才能夠。only
也是相同。
這個規則變化只做用於通配符,不做用於路徑。因此 node_modules
依然包含全部它的子目錄,而不僅僅只有一層。(不然全世界開發者都要爆炸)
和 babel 6 不一樣,若是要使用 @babel/node
,就必須單獨安裝,並添加到依賴中。
在提到刪除 stage-x 時候提過這個工具,它的目的是幫助用戶自動化地從 babel 6 升級到 7。
這款升級工具的功能包括:(這裏並不列出完整列表,只列出比較重要和經常使用的內容)
babel-*
替換爲 @babel/*
@babel/*
依賴的版本更新爲最新版 (例如 ^7.0.0
)scripts
中有使用 babel-node
,自動添加 @babel/node
爲開發依賴babel
配置項,檢查其中的 plugins
和 presets
,把短名 (env
) 替換爲完整的名字 (@babel/preset-env
)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
的通配符轉化等等。