Babel 是 JavaScript 的轉(zhuǎn)譯器。用于將 ES Next 的代碼轉(zhuǎn)換成瀏覽器或者其他環(huán)境支持的代碼人断。注意:不是轉(zhuǎn)化為 ES5 逾苫,因?yàn)椴煌愋鸵约安煌姹镜臑g覽器對(duì) ES Next 新特性的支持程度都不一樣愿伴,對(duì)于目標(biāo)環(huán)境已經(jīng)支持的部分,Babel 可以不轉(zhuǎn)化添瓷,所以 Babel 會(huì)依賴運(yùn)行環(huán)境的版本梅屉。
CLI
@babel/cli
是的 babel 的 CLI 工具。讓我們可以使用babel
命令編譯文件鳞贷,該命令存放在babel/cli/bin
目錄下坯汤。
Babel 工作流程
babel 運(yùn)行分為三個(gè)階段:
- 解析 Parser :通過模塊
@babel/parser
將源碼解析成 AST - 轉(zhuǎn)換 Transfrom:通過一系列插件 轉(zhuǎn)換成新的 AST
- 生成 Generator:模塊
@babel/generator
將轉(zhuǎn)換后的 AST 重新生成新的代碼
@babel/core
@babel/core 依賴于@babel/parse、 @babel/traverse搀愧、@babel/types惰聂、@babel/template 等 模塊, 通過 @babel/parse 解析器咱筛,基于ESTree 規(guī)范搓幌,將源碼轉(zhuǎn)換成 AST。再使用 @babel/traverse 模塊遍歷 AST迅箩,并在遍歷過程中調(diào)用 visitor 函數(shù)進(jìn)行 AST 的增刪改溉愁,該模塊提供了 path 的 api。然后轉(zhuǎn)換能力全部通過插件進(jìn)行饲趋。
@babel/core 模塊提供了以下幾個(gè)方法可進(jìn)行轉(zhuǎn)換:
- 字符串形式的 JavaScript 代碼可直接使用 babel.transform 來編譯:
babel.transform('code', options) // { code, map, ast }
- 如果是文件可使用異步 API:
babel.transformFile("file.js", options, (err, result: { code, map, ast }) => {})
- 或使用同步 API:
babel.transformFileSync('file.js', options) // { code, map, ast }
- 直接從 Babel AST(抽象語法樹)進(jìn)行轉(zhuǎn)換
babel.transformFromAst(ast, code, options) // { code, map, ast }
源碼如下:
// source.js
const arr = [1, 2, 3, 4]
arr.forEach(v => v * 2)
// index.js
const babel = require('@babel/core')
const fs = require('fs')
const code = fs.readFileSync('./source.js').toString()
// @babel/core 內(nèi)部引用了 parser拐揭、generator等模塊
const res = babel.transform(code, {
// 是否生成ast
ast: true,
// 是否生成解析的代碼
code: true,
// 是否生成 sourcemap
sourceMaps: true
// 用到的插件
// plugins: ['@babel/plugin-transform-arrow-functions']
})
console.log(res)
可以看出轉(zhuǎn)換后的結(jié)果包含源碼的 ast、轉(zhuǎn)換后的 code 以及 map 等奕塑。
從 code 可以看出沒有進(jìn)行轉(zhuǎn)換堂污,是因?yàn)?core 模塊本身不提供轉(zhuǎn)換能力,需要使用插件進(jìn)行轉(zhuǎn)換龄砰。
對(duì)箭頭函數(shù)進(jìn)行轉(zhuǎn)換:
// index.js
// 設(shè)置轉(zhuǎn)換箭頭函數(shù)的插件
const res = babel.transform(code, {
plugins: ['@babel/plugin-transform-arrow-functions']
})
以上看出盟猖,箭頭函數(shù)轉(zhuǎn)換成了普通函數(shù) ,但是 const 沒有轉(zhuǎn)換成 var寝贡,是因?yàn)橹皇褂昧宿D(zhuǎn)換箭頭函數(shù)的插件扒披。
traverse
@babel/traverse 是用來自動(dòng)遍歷AST的工具,它會(huì)訪問樹中的所有節(jié)點(diǎn)圃泡,在進(jìn)入每個(gè)節(jié)點(diǎn)時(shí)觸發(fā) enter 鉤子函數(shù)碟案,退出每個(gè)節(jié)點(diǎn)時(shí)觸發(fā) exit 鉤子函數(shù)。開發(fā)者可在鉤子函數(shù)中對(duì) AST 進(jìn)行修改颇蜡。
import traverse from '@babel/traverse'
traverse(ast, {
enter(path) {
// 進(jìn)入 path 后觸發(fā)
},
exit(path) {
// 退出 path 前觸發(fā)
}
})
types
@babel/types 是作用于 AST 的類 lodash 庫价说,其封裝了大量與 AST 有關(guān)的方法,大大降低了轉(zhuǎn)換 AST 的成本风秤。@babel/types 的功能主要有兩種:一方面可以用它驗(yàn)證 AST 節(jié)點(diǎn)的類型鳖目,例如使用 isClassMethod 或 assertClassMethod 方法可以判斷 AST 節(jié)點(diǎn)是否為 class 中的一個(gè) method;另一方面可以用它構(gòu)建 AST 節(jié)點(diǎn)缤弦,例如調(diào)用 classMethod 方法领迈,可生成一個(gè)新的 classMethod 類型 AST 節(jié)點(diǎn) 。
template
@babel/template 實(shí)現(xiàn)了計(jì)算機(jī)科學(xué)中一種被稱為準(zhǔn)引用(quasiquotes)的概念。說白了狸捅,它能直接將字符串代碼片段(可在字符串代碼中嵌入變量)轉(zhuǎn)換為 AST 節(jié)點(diǎn)衷蜓。例如下面的例子中,@babel/template 可以將一段引入 axios 的聲明直接轉(zhuǎn)變?yōu)?AST 節(jié)點(diǎn)尘喝。
import template from '@babel/template'
const ast = template.ast(`
import vue from 'vue'
`)
Presets 和 Plugins
Presets
預(yù)設(shè)是一系列插件的集合磁浇。
比如:
preset 的順序
preset 是逆序排列的(從后往前)。主要是為了確保向后兼容朽褪。這里涉及到配置的繼承和重寫概念置吓。
繼承和重寫是面向?qū)ο缶幊陶Z言中的概念,是指一個(gè)類擴(kuò)展自父類缔赠,并且重新實(shí)現(xiàn)了一些屬性和方法衍锚。這種思想不止在編程語言中用到,在配置文件中也廣泛使用橡淑。比如 eslint 中 extends
的配置构拳。
除了整體重寫以外,還支持文件級(jí)別和環(huán)境級(jí)別的重寫:
文件級(jí)別:
overrides: [
{
test: 'src/test.js',
plugins: ['pluginA']
}
]
環(huán)境級(jí)別:
{
envName: 'development',
env: {
development: {
plugins: ['pluginA']
},
production: {
plugins: ['pluginB']
}
}
}
preset 的參數(shù)
preset 可以接受參數(shù)梁棠,參數(shù)由插件名和參數(shù)對(duì)象組成一個(gè)數(shù)組置森。
@babel/preset-env
最常用的是@babel/preset-env
,是一個(gè)智能預(yù)設(shè)符糊。其轉(zhuǎn)譯是按需的凫海,對(duì)于環(huán)境支持的語法可以不做轉(zhuǎn)換的。開發(fā)者只需使用最新的 JavaScript男娄,而無需微觀管理目標(biāo)環(huán)境所需的語法轉(zhuǎn)換(以及可選的瀏覽器 polyfill)行贪。
通過配置 targets
屬性指定目標(biāo)瀏覽器,讓 Babel 只轉(zhuǎn)譯環(huán)境不支持的語法模闲。如果沒有配置會(huì)默認(rèn)轉(zhuǎn)譯所有 ES Next 的語法建瘫。
// 增加 presets配置
presets: [
'@babel/preset-env'
]
默認(rèn)轉(zhuǎn)成 ES5:
然后給targets
指定 chrome: '67'
(chrome67 版本是支持 const 和箭頭函數(shù)的)
presets: [
[
'@babel/preset-env'
{
targets: {
chrome: '67'
}
}
]
]
const 和箭頭函數(shù)并沒有進(jìn)行轉(zhuǎn)換。
創(chuàng)建 preset
導(dǎo)出一個(gè)函數(shù)尸折,函數(shù)的返回值為配置對(duì)象啰脚。
preset 可以是插件組成的數(shù)組:
module.exports = (api, opts) => ({
plugins: ['pluginA', 'pluginB', 'pluginC']
})
也可以包含其他的 preset 和帶參數(shù)的插件:
module.exports = (api, opts) => ({
presets: [require('@babel/preset-env')],
plugins: [[require('pluginA'), { loose: true }]]
})
比如 Vue-CLI 的 preset 就依賴于@babel/preset-env
:
vue-cli 的 babel presets:
@vue/cli-plugin-babel/preset
源碼:
Plugins
插件分為語法插件和轉(zhuǎn)換插件。
語法插件
作用于解析階段实夹。
大多數(shù)語法都可以被 babel 轉(zhuǎn)換橄浓,極少數(shù)情況下需要使用插件解析特定類型的語法。官方的語法插件以
babel-plugin-syntax
開頭亮航。
語法插件雖名為插件荸实,但事實(shí)上其本身并不具有功能性。所對(duì)應(yīng)的語法功能其實(shí)都已在@babel/parser
里實(shí)現(xiàn)缴淋,插件的作用只是將對(duì)應(yīng)語法的解析功能打開准给。
轉(zhuǎn)換插件
作用于轉(zhuǎn)換階段泄朴,用于轉(zhuǎn)換代碼。
官方的轉(zhuǎn)換插件以babel-plugin-transform
(正式)或 babel-plugin-proposal
(提案)開頭圆存。
轉(zhuǎn)換插件將啟用相應(yīng)的語法插件叼旋,因此你不必同時(shí)指定這兩種插件。
為什么在配置文件中有了預(yù)設(shè)還需要插件沦辙?
- 有些最新的語法可能還處于提案階段,沒有加入預(yù)設(shè)集合
- 根據(jù)項(xiàng)目需求讹剔,自己編寫的插件
執(zhí)行順序
plugin 和 preset 都是通過數(shù)組的形式在配置文件中配置油讯,生效順序是先 plugin 后 preset,plugin 從左到右延欠,preset 從右到左陌兑,這樣的生效順序使得配置里的插件可以覆蓋 preset 里面插件的配置的,也就是重寫由捎。
core-js
Babel 把 ES Next 標(biāo)準(zhǔn)分為 syntax
和 built-in
兩種類型兔综。syntax 是語法,比如:const狞玛、 () =>
等软驰。babel 默認(rèn)只轉(zhuǎn)換 syntax 類型。而 Promise心肪、Set锭亏、Map 等環(huán)境所內(nèi)置的 API 、靜態(tài)方法 Array.from硬鞍、Object.assign
慧瘤、實(shí)例方法Array.prototype.includes
等屬于 built-in。而 built-in 類型需要通過 polyfill 來完成轉(zhuǎn)譯固该。
Babel 在 7.4.0 版本中廢棄 @babel/polyfill 锅减,改用 core-js 替代。
配置 useBuiltIns
在 @babel/preset-env
中通過 useBuiltIns
參數(shù)來控制 built-in 的注入伐坏≌唬可選值為 'entry'、'usage'著淆、false
劫狠。默認(rèn)值為 false,不注入 polyfill永部。
- false :默認(rèn)值独泞,不注入 polyfill。
- 'entry':需在整個(gè)項(xiàng)目的入口處手動(dòng)引入 core-js 苔埋,
import 'core-js'
懦砂。該方式編譯后 Babel 會(huì)把目標(biāo)環(huán)境不支持的所有 built-in 都注入進(jìn)來,不管這些 API 是否使用到,極大的增加打包后包的體積荞膘。 - 'usage':不需在項(xiàng)目的入口處手動(dòng)引入罚随,Babel 會(huì)在編譯源碼的過程中根據(jù) built-in 的使用情況來選擇注入相應(yīng)的實(shí)現(xiàn)。如下圖:
配置 corejs 版本
當(dāng) useBuiltIns 設(shè)置為 'usage' 或者 'entry' 時(shí)羽资,還需要設(shè)置 @babel/preset-env 的 corejs 參數(shù)淘菩,用來指定注入 built-in 的實(shí)現(xiàn)時(shí),使用 corejs 的版本屠升。否則 Babel 日志輸出會(huì)有一個(gè)警告潮改。
module.exports = {
presets: [
[
'@babel/preset-env',
{
targets: {
edge: '17',
firefox: '60',
chrome: '67',
safari: '11.1'
},
useBuiltIns: 'usage',
corejs: '3.6.5',
// 開啟調(diào)試模式,編譯日志會(huì)打印在控制臺(tái)
debug: true
}
]
]
}
@babel/runtime
以 class 為例:
// source.js
class Person {
name = 'jack'
age = 18
static skill = 'eat'
}
編譯結(jié)果:
// dist.js
'use strict'
require('core-js/modules/es.object.define-property.js')
function _defineProperties(target, props) {
for (var i = 0; i < props.length; i++) {
var descriptor = props[i]
descriptor.enumerable = descriptor.enumerable || false
descriptor.configurable = true
if ('value' in descriptor) descriptor.writable = true
Object.defineProperty(target, descriptor.key, descriptor)
}
}
function _createClass(Constructor, protoProps, staticProps) {
if (protoProps) _defineProperties(Constructor.prototype, protoProps)
if (staticProps) _defineProperties(Constructor, staticProps)
Object.defineProperty(Constructor, 'prototype', { writable: false })
return Constructor
}
function _classCallCheck(instance, Constructor) {
if (!(instance instanceof Constructor)) {
throw new TypeError('Cannot call a class as a function')
}
}
function _defineProperty(obj, key, value) {
if (key in obj) {
Object.defineProperty(obj, key, {
value: value,
enumerable: true,
configurable: true,
writable: true
})
} else {
obj[key] = value
}
return obj
}
var Person = /*#__PURE__*/ _createClass(function Person() {
_classCallCheck(this, Person)
_defineProperty(this, 'name', 'jack')
_defineProperty(this, 'age', 18)
})
_defineProperty(Person, 'skill', 'eat')
可以看到腹暖,編譯后 built-in
類型在 文件中通過 require 引入了 polyfill汇在,而 syntax 類型的語法則在文件中定義了_defineProperties、_createClass脏答、_classCallCheck糕殉、_defineProperty
四個(gè) helper 函數(shù),用于創(chuàng)建構(gòu)造函數(shù)殖告、設(shè)置屬性阿蝶、創(chuàng)建實(shí)例。這只是 class 語法的輔助函數(shù)丛肮,其他 syntax 類型的語法也會(huì)生成 helper 函數(shù)赡磅,比如:extends 關(guān)鍵字。然而真實(shí)的項(xiàng)目開發(fā)中宝与,文件動(dòng)輒幾十焚廊、上百甚至上千,這樣打包后的文件會(huì)非常大习劫。
而 @babel/runtime
模塊提供了 helper 函數(shù)的集合咆瘟。在編譯后的文件中不是再創(chuàng)建 helper 函數(shù),而是引用@babel/runtime 提供的 helper 函數(shù)诽里。使用該模塊還需要用到 @babel/plugin-transform-runtime
插件袒餐。
yarn add @babel/plugin-transform-runtime -D
yarn add @babel/runtime
@babel/runtime 是一個(gè)運(yùn)行時(shí)依賴,所以不需要加-D 參數(shù)谤狡。
module.exports = {
presets: [
[
'@babel/preset-env',
{
useBuiltIns: 'usage',
corejs: '3.6.5',
debug: true
}
]
],
plugins: ['@babel/plugin-transform-runtime']
}
再次編譯:
'use strict'
var _interopRequireDefault = require('@babel/runtime/helpers/interopRequireDefault')
var _createClass2 = _interopRequireDefault(
require('@babel/runtime/helpers/createClass')
)
var _classCallCheck2 = _interopRequireDefault(
require('@babel/runtime/helpers/classCallCheck')
)
var _defineProperty2 = _interopRequireDefault(
require('@babel/runtime/helpers/defineProperty')
)
var Person = /*#__PURE__*/ (0, _createClass2['default'])(function Person() {
;(0, _classCallCheck2['default'])(this, Person)
;(0, _defineProperty2['default'])(this, 'name', 'jack')
;(0, _defineProperty2['default'])(this, 'age', 18)
})
;(0, _defineProperty2['default'])(Person, 'skill', 'eat')
可以看到 helper 函數(shù)都是引用@babel/runtime 模塊提供的 helper 函數(shù)了灸眼。
@babel/plugin-transform-runtime
該插件除了可以在文件中引用 helper 函數(shù)外,還為編譯后的代碼創(chuàng)建了一個(gè)沙箱環(huán)境墓懂,避免污染全局作用域焰宣。
比如通過引入 polyfill 的方式實(shí)現(xiàn)數(shù)組的 includes
方法,編譯后的結(jié)果是:
'use strict'
require('core-js/modules/es.array.includes.js')
var arr = [1, 2, 3]
arr.includes(3)
再看 core-js 中實(shí)現(xiàn) includes 方法的源碼可以看出直接往 Array.prototype 上增加 includes 方法捕仔,如下:
直接修改原型的會(huì)造成全局污染匕积。假如開發(fā)的是工具庫盈罐,被其它項(xiàng)目引用,而恰好該項(xiàng)目自身實(shí)現(xiàn)了 Array.prototype.includes 方法闪唆,那這樣就造成了沖突盅粪。
去掉 presets 的corejs
和 useBuiltIns
屬性,同時(shí)也就不再需要 @babel/runtime 和 core-js 模塊了悄蕾。
module.exports = {
presets: [
[
'@babel/preset-env'
]
],
plugins: [
[
'@babel/plugin-transform-runtime',
{
corejs: 3
}
]
]
}
給@babel/plugin-transform-runtime 插件設(shè)置 corejs 配置票顾,包含三個(gè)值:
false 默認(rèn)
2:安裝
@babel/runtime-corejs2
3:安裝
@babel/runtime-corejs3
根據(jù) corejs 得安裝對(duì)應(yīng)的模塊,編譯后的結(jié)果如下:
'use strict'
var _interopRequireDefault = require('@babel/runtime-corejs3/helpers/interopRequireDefault')
var _includes = _interopRequireDefault(
require('@babel/runtime-corejs3/core-js-stable/instance/includes')
)
var arr = [1, 2, 3]
;(0, _includes['default'])(arr).call(arr, 3)
看出 _includes
僅僅是一個(gè)局部變量帆调,不會(huì)對(duì) Array.prototype 造成污染库物。
如何驗(yàn)證?
給 presets 配置 corejs 的方式贷帮,將編譯后的文件在不支持 Array.prototype.includes 的瀏覽器中運(yùn)行,在瀏覽器控制臺(tái)或者引用該文件之后就可以使用[].includes
诱告,而給@babel/plugin-transform-runtime 插件配置 corejs 的方式撵枢,編譯后的文件同樣的瀏覽器運(yùn)行時(shí)會(huì)報(bào)錯(cuò),不存在此方法精居。