2021-4-23 前端達(dá)人
最近幾年,如果你是一名前端開發(fā)者,如果你沒有使用甚至聽說過 babel,可能會被當(dāng)做穿越者吧?
說到 babel,一連串名詞會蹦出來:
這些都是 babel 嗎?他們分別是做什么的?有區(qū)別嗎?
簡單來說把 JavaScript 中 es2015/2016/2017/2046 的新語法轉(zhuǎn)化為 es5,讓低端運(yùn)行環(huán)境(如瀏覽器和 node )能夠認(rèn)識并執(zhí)行。本文以 babel 6.x 為基準(zhǔn)進(jìn)行討論。最近 babel 出了 7.x,放在最后聊。
嚴(yán)格來說,babel 也可以轉(zhuǎn)化為更低的規(guī)范。但以目前情況來說,es5 規(guī)范已經(jīng)足以覆蓋絕大部分瀏覽器,因此常規(guī)來說轉(zhuǎn)到 es5 是一個安全且流行的做法。
如果你對 es5/es2015 等等也不了解的話,那你可能真的需要先補(bǔ)補(bǔ)課了。
總共存在三種方式:
其中后面兩種比較常見。第二種多見于 package.json 中的 scripts
段落中的某條命令;第三種就直接集成到構(gòu)建工具中。
這三種方式只有入口不同而已,調(diào)用的 babel 內(nèi)核,處理方式都是一樣的,所以我們先不糾結(jié)入口的問題。
babel 總共分為三個階段:解析,轉(zhuǎn)換,生成。
babel 本身不具有任何轉(zhuǎn)化功能,它把轉(zhuǎn)化的功能都分解到一個個 plugin 里面。因此當(dāng)我們不配置任何插件時,經(jīng)過 babel 的代碼和輸入是相同的。
插件總共分為兩種:
舉個簡單的例子,當(dāng)我們定義或者調(diào)用方法時,最后一個參數(shù)之后是不允許增加逗號的,如 callFoo(param1, param2,)
就是非法的。如果源碼是這種寫法,經(jīng)過 babel 之后就會提示語法錯誤。
但最近的 JS 提案中已經(jīng)允許了這種新的寫法(讓代碼 diff 更加清晰)。為了避免 babel 報(bào)錯,就需要增加語法插件 babel-plugin-syntax-trailing-function-commas
比起語法插件,轉(zhuǎn)譯插件其實(shí)更好理解,比如箭頭函數(shù) (a) => a
就會轉(zhuǎn)化為 function (a) {return a}
。完成這個工作的插件叫做 babel-plugin-transform-es2015-arrow-functions
。
同一類語法可能同時存在語法插件版本和轉(zhuǎn)譯插件版本。如果我們使用了轉(zhuǎn)譯插件,就不用再使用語法插件了。
既然插件是 babel 的根本,那如何使用呢?總共分為 2 個步驟:
babel
里面,格式相同)
npm install babel-plugin-xxx
進(jìn)行安裝
具體書寫格式就不詳述了。
比如 es2015 是一套規(guī)范,包含大概十幾二十個轉(zhuǎn)譯插件。如果每次要開發(fā)者一個個添加并安裝,配置文件很長不說,npm install
的時間也會很長,更不談我們可能還要同時使用其他規(guī)范呢。
為了解決這個問題,babel 還提供了一組插件的集合。因?yàn)槌S?,所以不必重?fù)定義 & 安裝。(單點(diǎn)和套餐的差別,套餐省下了巨多的時間和配置的精力)
preset 分為以下幾種:
例如 syntax-dynamic-import
就是 stage-2 的內(nèi)容,transform-object-rest-spread
就是 stage-3 的內(nèi)容。
此外,低一級的 stage 會包含所有高級 stage 的內(nèi)容,例如 stage-1 會包含 stage-2, stage-3 的所有內(nèi)容。
stage-4 在下一年更新會直接放到 env 中,所以沒有單獨(dú)的 stage-4 可供使用。
arrow-functions
,es2017 包含 syntax-trailing-function-commas
。但因?yàn)?env 的出現(xiàn),使得 es2016 和 es2017 都已經(jīng)廢棄。所以我們經(jīng)??梢钥吹?es2015 被單獨(dú)列出來,但極少看到其他兩個。很簡單的幾條原則:
preset 的逆向順序主要是為了保證向后兼容,因?yàn)榇蠖鄶?shù)用戶的編寫順序是 ['es2015', 'stage-0']
。這樣必須先執(zhí)行 stage-0
才能確保 babel 不報(bào)錯。因此我們編排 preset 的時候,也要注意順序,其實(shí)只要按照規(guī)范的時間順序列出即可。
簡略情況下,插件和 preset 只要列出字符串格式的名字即可。但如果某個 preset 或者插件需要一些配置項(xiàng)(或者說參數(shù)),就需要把自己先變成數(shù)組。第一個元素依然是字符串,表示自己的名字;第二個元素是一個對象,即配置對象。
最需要配置的當(dāng)屬 env,如下:
"presets": [ // 帶了配置項(xiàng),自己變成數(shù)組 [ // 第一個元素依然是名字 "env", // 第二個元素是對象,列出配置項(xiàng) { "module": false } ], // 不帶配置項(xiàng),直接列出名字 "stage-2" ]
因?yàn)?env 最為常用也最重要,所以我們有必要重點(diǎn)關(guān)注。
env 的核心目的是通過配置得知目標(biāo)環(huán)境的特點(diǎn),然后只做必要的轉(zhuǎn)換。例如目標(biāo)瀏覽器支持 es2015,那么 es2015 這個 preset 其實(shí)是不需要的,于是代碼就可以小一點(diǎn)(一般轉(zhuǎn)化后的代碼總是更長),構(gòu)建時間也可以縮短一些。
如果不寫任何配置項(xiàng),env 等價于 latest,也等價于 es2015 + es2016 + es2017 三個相加(不包含 stage-x 中的插件)。env 包含的插件列表維護(hù)在這里
下面列出幾種比較常用的配置方法:
{ "presets": [ ["env", { "targets": { "browsers": ["last 2 versions", "safari >= 7"] } }] ] }
如上配置將考慮所有瀏覽器的最新2個版本(safari大于等于7.0的版本)的特性,將必要的代碼進(jìn)行轉(zhuǎn)換。而這些版本已有的功能就不進(jìn)行轉(zhuǎn)化了。這里的語法可以參考 browserslist
{ "presets": [ ["env", { "targets": { "node": "6.10" } }] ] }
如上配置將目標(biāo)設(shè)置為 nodejs,并且支持 6.10 及以上的版本。也可以使用 node: 'current'
來支持最新穩(wěn)定版本。例如箭頭函數(shù)在 nodejs 6 及以上將不被轉(zhuǎn)化,但如果是 nodejs 0.12 就會被轉(zhuǎn)化了。
另外一個有用的配置項(xiàng)是 modules
。它的取值可以是 amd
, umd
, systemjs
, commonjs
和 false
。這可以讓 babel 以特定的模塊化格式來輸出代碼。如果選擇 false
就不進(jìn)行模塊化處理。
以上討論了 babel 的核心處理機(jī)制和配置方法等,不論任何入口調(diào)用 babel 都走這一套。但文章開頭提的那一堆 babel-*
還是讓人一頭霧水。實(shí)際上這些 babel-*
大多是不同的入口(方式)來使用 babel,下面來簡單介紹一下。
顧名思義,cli 就是命令行工具。安裝了 babel-cli
就能夠在命令行中使用 babel
命令來編譯文件。
在開發(fā) npm package 時經(jīng)常會使用如下模式:
babel-cli
安裝為 devDependencies
scripts
(比如 prepublish
),使用 babel
命令編譯文件
npm publish
這樣既可以使用較新規(guī)范的 JS 語法編寫源碼,同時又能支持舊版環(huán)境。因?yàn)轫?xiàng)目可能不太大,用不到構(gòu)建工具 (webpack 或者 rollup),于是在發(fā)布之前用 babel-cli
進(jìn)行處理。
babel-node
是 babel-cli
的一部分,它不需要單獨(dú)安裝。
它的作用是在 node 環(huán)境中,直接運(yùn)行 es2015 的代碼,而不需要額外進(jìn)行轉(zhuǎn)碼。例如我們有一個 js 文件以 es2015 的語法進(jìn)行編寫(如使用了箭頭函數(shù))。我們可以直接使用 babel-node es2015.js
進(jìn)行執(zhí)行,而不用再進(jìn)行轉(zhuǎn)碼了。
可以說:babel-node
= babel-polyfill
+ babel-register
。那這兩位又是誰呢?
babel-register 模塊改寫 require
命令,為它加上一個鉤子。此后,每當(dāng)使用 require
加載 .js
、.jsx
、.es
和 .es6
后綴名的文件,就會先用 babel 進(jìn)行轉(zhuǎn)碼。
使用時,必須首先加載 require('babel-register')
。
需要注意的是,babel-register 只會對 require
命令加載的文件轉(zhuǎn)碼,而 不會對當(dāng)前文件轉(zhuǎn)碼。
另外,由于它是實(shí)時轉(zhuǎn)碼,所以 只適合在開發(fā)環(huán)境使用。
babel 默認(rèn)只轉(zhuǎn)換 js 語法,而不轉(zhuǎn)換新的 API,比如 Iterator、Generator、Set、Maps、Proxy、Reflect、Symbol、Promise 等全局對象,以及一些定義在全局對象上的方法(比如 Object.assign
)都不會轉(zhuǎn)碼。
舉例來說,es2015 在 Array 對象上新增了 Array.from
方法。babel 就不會轉(zhuǎn)碼這個方法。如果想讓這個方法運(yùn)行,必須使用 babel-polyfill
。(內(nèi)部集成了 core-js
和 regenerator
)
使用時,在所有代碼運(yùn)行之前增加 require('babel-polyfill')
?;蛘吒R?guī)的操作是在 webpack.config.js
中將 babel-polyfill
作為第一個 entry。因此必須把 babel-polyfill
作為 dependencies
而不是 devDependencies
babel-polyfill
主要有兩個缺點(diǎn):
babel-polyfill
會導(dǎo)致打出來的包非常大,因?yàn)?nbsp;babel-polyfill
是一個整體,把所有方法都加到原型鏈上。比如我們只使用了 Array.from
,但它把 Object.defineProperty
也給加上了,這就是一種浪費(fèi)了。這個問題可以通過單獨(dú)使用 core-js
的某個類庫來解決,core-js
都是分開的。babel-polyfill
會污染全局變量,給很多類的原型鏈上都作了修改,如果我們開發(fā)的也是一個類庫供其他開發(fā)者使用,這種情況就會變得非常不可控。
因此在實(shí)際使用中,如果我們無法忍受這兩個缺點(diǎn)(尤其是第二個),通常我們會傾向于使用 babel-plugin-transform-runtime
。
但如果代碼中包含高版本 js 中類型的實(shí)例方法 (例如 [1,2,3].includes(1)
),這還是要使用 polyfill。
我們時常在項(xiàng)目中看到 .babelrc 中使用 babel-plugin-transform-runtime
,而 package.json
中的 dependencies
(注意不是 devDependencies
) 又包含了 babel-runtime
,那這兩個是不是成套使用的呢?他們又起什么作用呢?
先說 babel-plugin-transform-runtime
。
babel 會轉(zhuǎn)換 js 語法,之前已經(jīng)提過了。以 async/await
舉例,如果不使用這個 plugin (即默認(rèn)情況),轉(zhuǎn)換后的代碼大概是:
// babel 添加一個方法,把 async 轉(zhuǎn)化為 generator function _asyncToGenerator(fn) { return function () {....}} // 很長很長一段 // 具體使用處 var _ref = _asyncToGenerator(function* (arg1, arg2) { yield (0, something)(arg1, arg2); });
不用過于糾結(jié)具體的語法,只需看到,這個 _asyncToGenerator
在當(dāng)前文件被定義,然后被使用了,以替換源代碼的 await
。但每個被轉(zhuǎn)化的文件都會插入一段 _asyncToGenerator
這就導(dǎo)致重復(fù)和浪費(fèi)了。
在使用了 babel-plugin-transform-runtime
了之后,轉(zhuǎn)化后的代碼會變成
// 從直接定義改為引用,這樣就不會重復(fù)定義了。 var _asyncToGenerator2 = require('babel-runtime/helpers/asyncToGenerator'); var _asyncToGenerator3 = _interopRequireDefault(_asyncToGenerator2); // 具體使用處是一樣的 var _ref = _asyncToGenerator3(function* (arg1, arg2) { yield (0, something)(arg1, arg2); });
從定義方法改成引用,那重復(fù)定義就變成了重復(fù)引用,就不存在代碼重復(fù)的問題了。
但在這里,我們也發(fā)現(xiàn) babel-runtime
出場了,它就是這些方法的集合處,也因此,在使用 babel-plugin-transform-runtime
的時候必須把 babel-runtime
當(dāng)做依賴。
再說 babel-runtime
,它內(nèi)部集成了
core-js
: 轉(zhuǎn)換一些內(nèi)置類 (Promise
, Symbols
等等) 和靜態(tài)方法 (Array.from
等)。絕大部分轉(zhuǎn)換是這里做的。自動引入。regenerator
: 作為 core-js
的拾遺補(bǔ)漏,主要是 generator/yield
和 async/await
兩組的支持。當(dāng)代碼中有使用 generators/async
時自動引入。asyncToGenerator
就是其中之一,其他還有如 jsx
, classCallCheck
等等,可以查看 babel-helpers。在代碼中有內(nèi)置的 helpers 使用時(如上面的第一段代碼)移除定義,并插入引用(于是就變成了第二段代碼)。
babel-plugin-transform-runtime
不支持 實(shí)例方法 (例如 [1,2,3].includes(1)
)
此外補(bǔ)充一點(diǎn),把 helpers 抽離并統(tǒng)一起來,避免重復(fù)代碼的工作還有一個 plugin 也能做,叫做 babel-plugin-external-helpers
。但因?yàn)槲覀兪褂玫?nbsp;transform-runtime
已經(jīng)包含了這個功能,因此不必重復(fù)使用。而且 babel 的作者們也已經(jīng)開始討論這兩個插件過于類似,正在討論在 babel 7 中把 external-helpers
刪除,討論在 issue#5699 中。
前面提過 babel 的三種使用方法,并且已經(jīng)介紹過了 babel-cli
。但一些大型的項(xiàng)目都會有構(gòu)建工具 (如 webpack 或 rollup) 來進(jìn)行代碼構(gòu)建和壓縮 (uglify)。理論上來說,我們也可以對壓縮后的代碼進(jìn)行 babel 處理,但那會非常慢。因此如果在 uglify 之前就加入 babel 處理,豈不完美?
所以就有了 babel 插入到構(gòu)建工具內(nèi)部這樣的需求。以(我還算熟悉的) webpack 為例,webpack 有 loader 的概念,因此就出現(xiàn)了 babel-loader
。
和 babel-cli
一樣,babel-loader
也會讀取 .babelrc 或者 package.json 中的 babel
段作為自己的配置,之后的內(nèi)核處理也是相同。唯一比 babel-cli
復(fù)雜的是,它需要和 webpack 交互,因此需要在 webpack 這邊進(jìn)行配置。比較常見的如下:
module: { rules: [ { test: /\.js$/, exclude: /(node_modules|bower_components)/, loader: 'babel-loader' } ] }
如果想在這里傳入 babel 的配置項(xiàng),也可以把改成:
// loader: 'babel-loader' 改成如下: use: { loader: 'babel-loader', options: { // 配置項(xiàng)在這里 } }
這里的配置項(xiàng)優(yōu)先級是最高的。但我認(rèn)為放到單獨(dú)的配置文件中更加清晰合理,可讀性強(qiáng)一些。
最近 babel 發(fā)布了 7.0。因?yàn)樯厦娌糠侄际轻槍?6.x 編寫的,所以我們關(guān)注一下 7.0 帶來的變化(核心機(jī)制方面沒有變化,插件,preset,解析轉(zhuǎn)譯生成這些都沒有變化)
我只挑選一些和開發(fā)者關(guān)系比較大的列在這里,省略的多數(shù)是針對某一個 plugin 的改動。完整的列表可以參考官網(wǎng)。
淘汰 es201x 的目的是把選擇環(huán)境的工作交給 env 自動進(jìn)行,而不需要開發(fā)者投入精力。凡是使用 es201x 的開發(fā)者,都應(yīng)當(dāng)使用 env 進(jìn)行替換。但這里的淘汰 (原文 deprecated) 并不是刪除,只是不推薦使用了,不好說 babel 8 就真的刪了。
與之相比,stage-x 就沒那么好運(yùn)了,它們直接被刪了。這是因?yàn)?babel 團(tuán)隊(duì)認(rèn)為為這些 “不穩(wěn)定的草案” 花費(fèi)精力去更新 preset 相當(dāng)浪費(fèi)。stage-x 雖然刪除了,但它包含的插件并沒有刪除(只是被更名了,可以看下面一節(jié)),我們依然可以顯式地聲明這些插件來獲得等價的效果。完整列表
為了減少開發(fā)者替換配置文件的機(jī)械工作,babel 開發(fā)了一款 babel-upgrade
的工具,它會檢測 babel 配置中的 stage-x 并且替換成對應(yīng)的 plugins。除此之外它還有其他功能,我們一會兒再詳細(xì)看。(總之目的就是讓你更加平滑地遷移到 babel 7)
這是 babel 7 的一個重大變化,把所有 babel-*
重命名為 @babel/*
,例如:
babel-cli
變成了 @babel/cli
。
babel-preset-env
變成了 @babel/preset-env
。進(jìn)一步,還可以省略 preset
而簡寫為 @babel/env
。
babel-plugin-transform-arrow-functions
變成了 @babel/plugin-transform-arrow-functions
。和 preset
一樣,plugin
也可以省略,于是簡寫為 @babel/transform-arrow-functions
。
這個變化不單單應(yīng)用于 package.json 的依賴中,包括 .babelrc 的配置 (plugins
, presets
) 也要這么寫,為了保持一致。例如
{
"presets": [ - "env" + "@babel/preset-env" ]
}
順帶提一句,上面提過的 babel 解析語法的內(nèi)核 babylon
現(xiàn)在重命名為 @babel/parser
,看起來是被收編了。
上文提過的 stage-x 被刪除了,它包含的插件雖然保留,但也被重命名了。babel 團(tuán)隊(duì)希望更明顯地區(qū)分已經(jīng)位于規(guī)范中的插件 (如 es2015 的 babel-plugin-transform-arrow-functions
) 和僅僅位于草案中的插件 (如 stage-0 的 @babel/plugin-proposal-function-bind
)。方式就是在名字中增加 proposal
,所有包含在 stage-x 的轉(zhuǎn)譯插件都使用了這個前綴,語法插件不在其列。
最后,如果插件名稱中包含了規(guī)范名稱 (-es2015-
, -es3-
之類的),一律刪除。例如 babel-plugin-transform-es2015-classes
變成了 @babel/plugin-transform-classes
。(這個插件我自己沒有單獨(dú)用過,慚愧)
babel 7.0 開始不再支持 nodejs 0.10, 0.12, 4, 5 這四個版本,相當(dāng)于要求 nodejs >= 6 (當(dāng)前 nodejs LTS 是 8,要求也不算太過分吧)。
這里的不再支持,指的是在這些低版本 node 環(huán)境中不能使用 babel 轉(zhuǎn)譯代碼,但 babel 轉(zhuǎn)譯后的代碼依然能在這些環(huán)境上運(yùn)行,這點(diǎn)不要混淆。
在 babel 6 時,ignore
選項(xiàng)如果包含 *.foo.js
,實(shí)際上的含義 (轉(zhuǎn)化為 glob) 是 ./**/*.foo.js
,也就是當(dāng)前目錄 包括子目錄 的所有 foo.js
結(jié)尾的文件。這可能和開發(fā)者常規(guī)的認(rèn)識有悖。
于是在 babel 7,相同的表達(dá)式 *.foo.js
只作用于當(dāng)前目錄,不作用于子目錄。如果依然想作用于子目錄的,就要按照 glob 的完整規(guī)范書寫為 ./**/*.foo.js
才可以。only
也是相同。
這個規(guī)則變化只作用于通配符,不作用于路徑。所以 node_modules
依然包含所有它的子目錄,而不單單只有一層。(否則全世界開發(fā)者都要爆炸)
和 babel 6 不同,如果要使用 @babel/node
,就必須單獨(dú)安裝,并添加到依賴中。
在提到刪除 stage-x 時候提過這個工具,它的目的是幫助用戶自動化地從 babel 6 升級到 7。
這款升級工具的功能包括:(這里并不列出完整列表,只列出比較重要和常用的內(nèi)容)
babel-*
替換為 @babel/*
@babel/*
依賴的版本更新為最新版 (例如 ^7.0.0
)
scripts
中有使用 babel-node
,自動添加 @babel/node
為開發(fā)依賴
babel
配置項(xiàng),檢查其中的 plugins
和 presets
,把短名 (env
) 替換為完整的名字 (@babel/preset-env
)plugins
和 presets
,把短名 (env
) 替換為完整的名字 (@babel/preset-env
)
preset-stage-x
,如有替換為對應(yīng)的插件并添加到 plugins
使用方式如下:
# 不安裝到本地而是直接運(yùn)行命令,npm 的新功能 npx babel-upgrade --write # 或者常規(guī)方式 npm i babel-upgrade -g
babel-upgrade --write
babel-upgrade
工具本身也還在開發(fā)中,還列出了許多 TODO 沒有完成,因此之后的功能可能會更加豐富,例如上面提過的 ignore
的通配符轉(zhuǎn)化等等。
轉(zhuǎn)自:知乎。作者:前端解憂雜貨鋪
藍(lán)藍(lán)設(shè)計(jì)( www.miumiuwan.com )是一家專注而深入的界面設(shè)計(jì)公司,為期望卓越的國內(nèi)外企業(yè)提供卓越的UI界面設(shè)計(jì)、BS界面設(shè)計(jì) 、 cs界面設(shè)計(jì) 、 ipad界面設(shè)計(jì) 、 包裝設(shè)計(jì) 、 圖標(biāo)定制 、 用戶體驗(yàn) 、交互設(shè)計(jì)、 網(wǎng)站建設(shè) 、平面設(shè)計(jì)服務(wù)
藍(lán)藍(lán)設(shè)計(jì)的小編 http://www.miumiuwan.com