概要
Babel 是一個工具鏈,主要用於將采用 ECMAScript 2015+ 語法編寫的代碼轉換為向后兼容的 JavaScript 語法,以便能夠運行在當前和舊版本的瀏覽器或其他環境中。
Babel可以幫我們做一下事情:
- 可以把項目中的 es6、es7 等代碼轉成目標環境支持的代碼
- 可以自動 polyfill 目標環境不支持的 api(通過引入第三方 polyfill 模塊,例如 core-js)
- 用 babel 來編譯 typescript,不用 tsc 了
使用方式
有三種使用方式,如下:
- 使用單體文件 (standalone script)
- 命令行 (cli)
- 構建工具的插件 (webpack 的 babel-loader, rollup 的 rollup-plugin-babel)。
其中后面兩種比較常見。第二種多見於 package.json 中的 scripts 段落中的某條命令;第三種就直接集成到構建工具中。
這三種方式只有入口不同而已,調用的 babel 內核,處理方式都是一樣的。
babel插件--實際的打工人
babel 處理過程分為三個階段:解析,轉換,生成。
babel 本身不具有任何轉化功能,它把轉化的功能都分解到一個個 plugin 里面。因此當我們不配置任何插件時,經過 babel 的代碼和輸入是相同的。
插件總共分為兩種:
1. 語法插件
當我們添加 語法插件 之后,在解析這一步就使得 babel 能夠解析更多的語法。(順帶一提,babel 內部使用的解析類庫叫做 babylon,並非 babel 自行開發)
舉個簡單的例子,當我們定義或者調用方法時,最后一個參數之后是不允許增加逗號的,如 callFoo(param1, param2,)
就是非法的。如果源碼是這種寫法,經過 babel 之后就會提示語法錯誤。
但最近的 JS 提案中已經允許了這種新的寫法(讓代碼 diff 更加清晰)。為了避免 babel 報錯,就需要增加語法插件 babel-plugin-syntax-trailing-function-commas
2. 轉譯插件
當我們添加 轉譯插件 之后,在轉換這一步把源碼轉換並輸出。這也是我們使用 babel 最本質的需求。
比起語法插件,轉譯插件其實更好理解,比如箭頭函數 (a) => a
就會轉化為 function (a) {return a}
。完成這個工作的插件叫做 babel-plugin-transform-es2015-arrow-functions
。
同一類語法可能同時存在語法插件版本和轉譯插件版本。如果我們使用了轉譯插件,就不用再使用語法插件了。
產品使用說明書--babel配置文件
既然插件是 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 - 完成: 將被添加到下一年度發布。
stage-3包括以下插件:
transform-async-to-generator 支持async/await
transform-exponentiation-operator 支持冪運算符語法糖
stage-2包括stage-3的所有插件,額外還包括以下插件:
syntax-trailing-function-commas 支持尾逗號函數
transform-object-reset-spread 支持對象的解構賦值
stage-1包括stage2所有插件,額外還包括以下插件:
transform-class-constructor-call 支持class的構造函數
transform-class-properties 支持class的static屬性
transform-decorators 支持es7的裝飾者模式即@符號引入的方法 (還在討論中的特性?)
transform-export-extensions 支持export方法
stage-0包括stage1所有插件,額外還包括以下插件:
transform-do-expressions 支持在jsx中書寫if/else
transform-function-bind 支持::操作符來切換上下文,類似於es5的bind
例如 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 包含的插件列表, 有興趣的可以到github搜索一下。
下面列出幾種比較常用的配置方法:
{
"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-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.x
因為上面部分都是針對 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
。
這里的不再支持,指的是在這些低版本 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