【JavaScript】深入理解Babel原理及其使用

前言

半年前也寫過(guò)一篇babel的簡(jiǎn)單使用文章募疮,當(dāng)時(shí)看了下babel的文檔裤唠,但是很多地方還不理解祭椰,所以文章里沒(méi)有怎么說(shuō)道babel的一些關(guān)鍵概念花椭,只是機(jī)械的描述如何使用(配合webstorm)忽匈。
最近剛好遇到一個(gè)問(wèn)題,發(fā)現(xiàn)是因?yàn)閖s代碼中使用的es6的新api沒(méi)有被轉(zhuǎn)義矿辽,導(dǎo)致拋異常了丹允。查找了下資料,是沒(méi)有引入polyfill所致袋倔,之前一直對(duì)babel的這些概念沒(méi)有很好的理解雕蔽,把這個(gè)bug作為引子,認(rèn)真了解了下babel宾娜,寫出此文批狐。
本文的babel使用場(chǎng)景局限于babel配合webpack來(lái)轉(zhuǎn)譯輸出es5的js代碼,babel的命令行前塔、以代碼形式調(diào)用或node環(huán)境下這些統(tǒng)統(tǒng)都不會(huì)涉及贾陷。

Babel的包構(gòu)成

核心包

  • babel-core:babel轉(zhuǎn)譯器本身缘眶,提供了babel的轉(zhuǎn)譯API,如babel.transform等髓废,用于對(duì)代碼進(jìn)行轉(zhuǎn)譯。像webpack的babel-loader就是調(diào)用這些API來(lái)完成轉(zhuǎn)譯過(guò)程的该抒。
  • babylon:js的詞法解析器
  • babel-traverse:用于對(duì)AST(抽象語(yǔ)法樹(shù)慌洪,想了解的請(qǐng)自行查詢編譯原理)的遍歷,主要給plugin用
  • babel-generator:根據(jù)AST生成代碼

功能包

  • babel-types:用于檢驗(yàn)凑保、構(gòu)建和改變AST樹(shù)的節(jié)點(diǎn)
  • babel-template:輔助函數(shù)冈爹,用于從字符串形式的代碼來(lái)構(gòu)建AST樹(shù)節(jié)點(diǎn)
  • babel-helpers:一系列預(yù)制的babel-template函數(shù),用于提供給一些plugins使用
  • babel-code-frames:用于生成錯(cuò)誤信息欧引,打印出錯(cuò)誤點(diǎn)源代碼幀以及指出出錯(cuò)位置
  • babel-plugin-xxx:babel轉(zhuǎn)譯過(guò)程中使用到的插件频伤,其中babel-plugin-transform-xxx是transform步驟使用的
  • babel-preset-xxx:transform階段使用到的一系列的plugin
  • babel-polyfill:JS標(biāo)準(zhǔn)新增的原生對(duì)象和API的shim,實(shí)現(xiàn)上僅僅是core-js和regenerator-runtime兩個(gè)包的封裝
  • babel-runtime:功能類似babel-polyfill芝此,一般用于library或plugin中憋肖,因?yàn)樗粫?huì)污染全局作用域

工具包

babel-cli:babel的命令行工具,通過(guò)命令行對(duì)js代碼進(jìn)行轉(zhuǎn)譯
babel-register:通過(guò)綁定node.js的require來(lái)自動(dòng)轉(zhuǎn)譯require引用的js代碼文件

babel的配置

使用形式

如果是以命令行方式使用babel婚苹,那么babel的設(shè)置就以命令行參數(shù)的形式帶過(guò)去岸更;
還可以在package.json里在babel字段添加設(shè)置;
但是建議還是使用一個(gè)單獨(dú)的.babelrc文件膊升,把babel的設(shè)置都放置在這里怎炊,所有babel API的options(除了回調(diào)函數(shù)之外)都能夠支持,具體的options見(jiàn)babel的API options文檔

常用options字段說(shuō)明

  • env:指定在不同環(huán)境下使用的配置廓译。比如production和development兩個(gè)環(huán)境使用不同的配置评肆,就可以通過(guò)這個(gè)字段來(lái)配置。env字段的從process.env.BABEL_ENV獲取非区,如果BABEL_ENV不存在瓜挽,則從process.env.NODE_ENV獲取,如果NODE_ENV還是不存在院仿,則取默認(rèn)值"development"
  • plugins:要加載和使用的插件列表秸抚,插件名前的babel-plugin-可省略;plugin列表按從頭到尾的順序運(yùn)行
  • presets:要加載和使用的preset列表歹垫,preset名前的babel-preset-可省略剥汤;presets列表的preset按從尾到頭的逆序運(yùn)行(為了兼容用戶使用習(xí)慣)
  • 同時(shí)設(shè)置了presets和plugins,那么plugins的先運(yùn)行排惨;每個(gè)preset和plugin都可以再配置自己的option

配置文件的查找

babel會(huì)從當(dāng)前轉(zhuǎn)譯的文件所在目錄下查找配置文件吭敢,如果沒(méi)有找到,就順著文檔目錄樹(shù)一層層往上查找暮芭,一直到.babelrc文件存在或者帶babel字段的package.json文件存在為止鹿驼。

babel的工作原理

babel是一個(gè)轉(zhuǎn)譯器欲低,感覺(jué)相對(duì)于編譯器compiler,叫轉(zhuǎn)譯器transpiler更準(zhǔn)確畜晰,因?yàn)樗皇前淹N語(yǔ)言的高版本規(guī)則翻譯成低版本規(guī)則砾莱,而不像編譯器那樣,輸出的是另一種更低級(jí)的語(yǔ)言代碼凄鼻。
但是和編譯器類似腊瑟,babel的轉(zhuǎn)譯過(guò)程也分為三個(gè)階段:parsing、transforming块蚌、generating闰非,以ES6代碼轉(zhuǎn)譯為ES5代碼為例,babel轉(zhuǎn)譯的具體過(guò)程如下:

ES6代碼輸入 ==》 babylon進(jìn)行解析 ==》 得到AST
==》 plugin用babel-traverse對(duì)AST樹(shù)進(jìn)行遍歷轉(zhuǎn)譯 ==》 得到新的AST樹(shù)
==》 用babel-generator通過(guò)AST樹(shù)生成ES5代碼

此外峭范,還要注意很重要的一點(diǎn)就是财松,babel只是轉(zhuǎn)譯新標(biāo)準(zhǔn)引入的語(yǔ)法,比如ES6的箭頭函數(shù)轉(zhuǎn)譯成ES5的函數(shù)纱控;而新標(biāo)準(zhǔn)引入的新的原生對(duì)象辆毡,部分原生對(duì)象新增的原型方法,新增的API等(如Proxy其徙、Set等)胚迫,這些babel是不會(huì)轉(zhuǎn)譯的。需要用戶自行引入polyfill來(lái)解決

plugins

插件應(yīng)用于babel的轉(zhuǎn)譯過(guò)程唾那,尤其是第二個(gè)階段transforming访锻,如果這個(gè)階段不使用任何插件,那么babel會(huì)原樣輸出代碼闹获。
我們主要關(guān)注transforming階段使用的插件期犬,因?yàn)閠ransform插件會(huì)自動(dòng)使用對(duì)應(yīng)的詞法插件,所以parsing階段的插件不需要配置避诽。

presets

如果要自行配置轉(zhuǎn)譯過(guò)程中使用的各類插件龟虎,那太痛苦了,所以babel官方幫我們做了一些預(yù)設(shè)的插件集沙庐,稱之為preset鲤妥,這樣我們只需要使用對(duì)應(yīng)的preset就可以了。以JS標(biāo)準(zhǔn)為例拱雏,babel提供了如下的一些preset:

  • es2015
  • es2016
  • es2017
  • env
    es20xx的preset只轉(zhuǎn)譯該年份批準(zhǔn)的標(biāo)準(zhǔn)棉安,而env則代指最新的標(biāo)準(zhǔn),包括了latest和es20xx各年份
    另外铸抑,還有 stage-0到stage-4的標(biāo)準(zhǔn)成形之前的各個(gè)階段贡耽,這些都是實(shí)驗(yàn)版的preset,建議不要使用。

polyfill

polyfill是一個(gè)針對(duì)ES2015+環(huán)境的shim蒲赂,實(shí)現(xiàn)上來(lái)說(shuō)babel-polyfill包只是簡(jiǎn)單的把core-js和regenerator runtime包裝了下阱冶,這兩個(gè)包才是真正的實(shí)現(xiàn)代碼所在(后文會(huì)詳細(xì)介紹core-js)。
使用babel-polyfill會(huì)把ES2015+環(huán)境整體引入到你的代碼環(huán)境中滥嘴,讓你的代碼可以直接使用新標(biāo)準(zhǔn)所引入的新原生對(duì)象木蹬,新API等,一般來(lái)說(shuō)單獨(dú)的應(yīng)用和頁(yè)面都可以這樣使用若皱。

使用方法

  1. 先安裝包: npm install --save babel-polyfill
  2. 要確保在入口處導(dǎo)入polyfill届囚,因?yàn)閜olyfill代碼需要在所有其他代碼前先被調(diào)用
    代碼方式: import "babel-polyfill"
    webpack配置: module.exports = { entry: ["babel-polyfill", "./app/js"] };

如果只是需要引入部分新原生對(duì)象或API,那么可以按需引入是尖,而不必導(dǎo)入全部的環(huán)境,具體見(jiàn)下文的core-js

runtime

polyfill和runtime的區(qū)別

直接使用babel-polyfill對(duì)于應(yīng)用或頁(yè)面等環(huán)境在你控制之中的情況來(lái)說(shuō)泥耀,并沒(méi)有什么問(wèn)題饺汹。但是對(duì)于在library中使用polyfill,就變得不可行了痰催。因?yàn)閘ibrary是供外部使用的兜辞,但外部的環(huán)境并不在library的可控范圍,而polyfill是會(huì)污染原來(lái)的全局環(huán)境的(因?yàn)樾碌脑鷮?duì)象夸溶、API這些都直接由polyfill引入到全局環(huán)境)逸吵。這樣就很容易會(huì)發(fā)生沖突,所以這個(gè)時(shí)候缝裁,babel-runtime就可以派上用場(chǎng)了扫皱。

transform-runtime和babel-runtime

babel-plugin-transform-runtime插件依賴babel-runtime,babel-runtime是真正提供runtime環(huán)境的包捷绑;也就是說(shuō)transform-runtime插件是把js代碼中使用到的新原生對(duì)象和靜態(tài)方法轉(zhuǎn)換成對(duì)runtime實(shí)現(xiàn)包的引用韩脑,舉個(gè)例子如下:

// 輸入的ES6代碼
var sym = Symbol();
// 通過(guò)transform-runtime轉(zhuǎn)換后的ES5+runtime代碼 
var _symbol = require("babel-runtime/core-js/symbol");
var sym = (0, _symbol.default)();

從上面這個(gè)例子可見(jiàn),原本代碼中使用的ES6新原生對(duì)象Symbol被transform-runtimec插件轉(zhuǎn)換成了babel-runtime的實(shí)現(xiàn)粹污,既保持了Symbol的功能段多,同時(shí)又沒(méi)有像polyfill那樣污染全局環(huán)境(因?yàn)樽罱K生成的代碼中,并沒(méi)有對(duì)Symbol的引用)
另外壮吩,這里我們也可以隱約發(fā)現(xiàn)进苍,babel-runtime其實(shí)也不是真正的實(shí)現(xiàn)代碼所在,真正的代碼實(shí)現(xiàn)是在core-js中鸭叙,后面我們?cè)僬f(shuō)

transform-runtime插件的功能

  1. 把代碼中的使用到的ES6引入的新原生對(duì)象和靜態(tài)方法用babel-runtime/core-js導(dǎo)出的對(duì)象和方法替代
  2. 當(dāng)使用generators或async函數(shù)時(shí)觉啊,用babel-runtime/regenerator導(dǎo)出的函數(shù)取代(類似polyfill分成regenerator和core-js兩個(gè)部分)
  3. 把Babel生成的輔助函數(shù)改為用babel-runtime/helpers導(dǎo)出的函數(shù)來(lái)替代(babel默認(rèn)會(huì)在每個(gè)文件頂部放置所需要的輔助函數(shù),如果文件多的話递雀,這些輔助函數(shù)就在每個(gè)文件中都重復(fù)了柄延,通過(guò)引用babel-runtime/helpers就可以統(tǒng)一起來(lái),減少代碼體積)

上述三點(diǎn)就是transform-runtime插件所做的事情,由此也可見(jiàn)搜吧,babel-runtime就是一個(gè)提供了regenerator市俊、core-js和helpers的運(yùn)行時(shí)庫(kù)。
建議不要直接使用babel-runtime滤奈,因?yàn)閠ransform-runtime依賴babel-runtime摆昧,大部分情況下都可以用transform-runtime達(dá)成目的。
此外蜒程,transform-runtime在.babelrc里配置的時(shí)候绅你,還可以設(shè)置helpers、polyfill昭躺、regenerator這三個(gè)開(kāi)關(guān)忌锯,以自行決定runtime是否要引入對(duì)應(yīng)的功能。
最后補(bǔ)充一點(diǎn):由于runtime不會(huì)污染全局空間领炫,所以實(shí)例方法是無(wú)法工作的(因?yàn)檫@必須在原型鏈上添加這個(gè)方法偶垮,這是和polyfill最大的不同) ,比如:

var arr = ['a', 'b', 'c'];
arr.fill(7);  // 實(shí)例方法不行
Array.prototype.fill.apply(arr, 7);  // 用原型鏈來(lái)調(diào)用也是不行

通過(guò)core-js實(shí)現(xiàn)按需引入polyfill或runtime

core-js包才上述的polyfill帝洪、runtime的核心似舵,因?yàn)閜olyfill和runtime其實(shí)都只是對(duì)core-js和regenerator的再封裝,方便使用而已葱峡。
但是polyfill和runtime都是整體引入的砚哗,不能做細(xì)粒度的調(diào)整,如果我們的代碼只是用到了小部分ES6而導(dǎo)致需要使用polyfill和runtime的話砰奕,會(huì)造成代碼體積不必要的增大(runtime的影響較兄虢妗)。所以脆淹,按需引入的需求就自然而然產(chǎn)生了常空,這個(gè)時(shí)候就得依靠core-js來(lái)實(shí)現(xiàn)了。

core-js的組織結(jié)構(gòu)

首先盖溺,core-js有三種使用方式:

  • 默認(rèn)方式:require('core-js')
    這種方式包括全部特性漓糙,標(biāo)準(zhǔn)的和非標(biāo)準(zhǔn)的
  • 庫(kù)的形式: var core = require('core-js/library')
    這種方式也包括全部特性,只是它不會(huì)污染全局名字空間
  • 只是shim: require('core-js/shim')或var shim = require('core-js/library/shim')
    這種方式只包括標(biāo)準(zhǔn)特性(就是只有polyfill功能烘嘱,沒(méi)有擴(kuò)展的特性)

core-js的結(jié)構(gòu)是高度模塊化的昆禽,它把每個(gè)特性都組織到一個(gè)小模塊里,然后再把這些小模塊組合成一個(gè)大特性蝇庭,層層組織醉鳖。比如:
core-js/es6(core-js/library/es6)就包含了全部的ES6特性,而core-js/es6/array(core-js/library/es6/array)則只包含ES6的Array特性哮内,而core-js/fn/array/from(core-js/library/fn/array/from)則只有Array.from這個(gè)實(shí)現(xiàn)盗棵。
實(shí)現(xiàn)按需使用壮韭,就是自己選擇使用到的特性,然后導(dǎo)入即可纹因。具體的每個(gè)特性和對(duì)應(yīng)的路徑可以直接查看core-js的github

core-js的按需使用

1喷屋、類似polyfill,直接把特性添加到全局環(huán)境瞭恰,這種方式體驗(yàn)最完整

require('core-js/fn/set');
require('core-js/fn/array/from');
require('core-js/fn/array/find-index');

Array.from(new Set([1, 2, 3, 2, 1])); // => [1, 2, 3]
[1, 2, NaN, 3, 4].findIndex(isNaN);   // => 2

2屯曹、類似runtime一樣,以庫(kù)的形式來(lái)使用特性惊畏,這種方式不會(huì)污染全局名字空間恶耽,但是不能使用實(shí)例方法

var Set       = require('core-js/library/fn/set');
var from      = require('core-js/library/fn/array/from');
var findIndex = require('core-js/library/fn/array/find-index');

from(new Set([1, 2, 3, 2, 1]));      // => [1, 2, 3]
findIndex([1, 2, NaN, 3, 4], isNaN); // => 2

3、因?yàn)榈诙N庫(kù)的形式不能使用prototype方法颜启,所以第三種方式使用了一個(gè)小技巧偷俭,通過(guò)::這個(gè)符號(hào)而不是.來(lái)調(diào)用實(shí)例方式,從而達(dá)到曲線救國(guó)的目的缰盏。這種方式的使用社搅,路徑中都會(huì)帶有/virtual/

import {fill, findIndex} from 'core-js/library/fn/array/virtual';

Array(10)::fill(0).map((a, b) => b * b)::findIndex(it => it && !(it % 8)); // => 4

// 對(duì)比下polyfill的實(shí)現(xiàn) 
// Array(10).fill(0).map((a, b) => b * b).findIndex(it => it && !(it % 8));

總結(jié)

Babel使用的難點(diǎn)主要在于理解polyfill、runtime和core-js乳规,通過(guò)本文,把這三者的概念和關(guān)系理清楚了合呐,對(duì)babel的使用就不存在問(wèn)題暮的!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市淌实,隨后出現(xiàn)的幾起案子冻辩,更是在濱河造成了極大的恐慌,老刑警劉巖拆祈,帶你破解...
    沈念sama閱讀 206,839評(píng)論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件恨闪,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡放坏,警方通過(guò)查閱死者的電腦和手機(jī)咙咽,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,543評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)淤年,“玉大人钧敞,你說(shuō)我怎么就攤上這事◆锪福” “怎么了溉苛?”我有些...
    開(kāi)封第一講書人閱讀 153,116評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)弄诲。 經(jīng)常有香客問(wèn)我愚战,道長(zhǎng),這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書人閱讀 55,371評(píng)論 1 279
  • 正文 為了忘掉前任寂玲,我火速辦了婚禮塔插,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘敢茁。我一直安慰自己佑淀,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,384評(píng)論 5 374
  • 文/花漫 我一把揭開(kāi)白布彰檬。 她就那樣靜靜地躺著伸刃,像睡著了一般。 火紅的嫁衣襯著肌膚如雪逢倍。 梳的紋絲不亂的頭發(fā)上捧颅,一...
    開(kāi)封第一講書人閱讀 49,111評(píng)論 1 285
  • 那天,我揣著相機(jī)與錄音较雕,去河邊找鬼碉哑。 笑死,一個(gè)胖子當(dāng)著我的面吹牛亮蒋,可吹牛的內(nèi)容都是我干的扣典。 我是一名探鬼主播,決...
    沈念sama閱讀 38,416評(píng)論 3 400
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼慎玖,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼贮尖!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起趁怔,我...
    開(kāi)封第一講書人閱讀 37,053評(píng)論 0 259
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤湿硝,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后润努,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體关斜,經(jīng)...
    沈念sama閱讀 43,558評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,007評(píng)論 2 325
  • 正文 我和宋清朗相戀三年铺浇,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了痢畜。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,117評(píng)論 1 334
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡鳍侣,死狀恐怖裁着,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情拱她,我是刑警寧澤二驰,帶...
    沈念sama閱讀 33,756評(píng)論 4 324
  • 正文 年R本政府宣布,位于F島的核電站秉沼,受9級(jí)特大地震影響桶雀,放射性物質(zhì)發(fā)生泄漏矿酵。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,324評(píng)論 3 307
  • 文/蒙蒙 一矗积、第九天 我趴在偏房一處隱蔽的房頂上張望全肮。 院中可真熱鬧,春花似錦棘捣、人聲如沸辜腺。這莊子的主人今日做“春日...
    開(kāi)封第一講書人閱讀 30,315評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)评疗。三九已至,卻和暖如春茵烈,著一層夾襖步出監(jiān)牢的瞬間百匆,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書人閱讀 31,539評(píng)論 1 262
  • 我被黑心中介騙來(lái)泰國(guó)打工呜投, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留加匈,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 45,578評(píng)論 2 355
  • 正文 我出身青樓仑荐,卻偏偏與公主長(zhǎng)得像雕拼,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子粘招,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,877評(píng)論 2 345

推薦閱讀更多精彩內(nèi)容