Babel 是 JavaScript 的转译器。用于将 ES Next 的代码转换成浏览器或者其他环境支持的代码。注意:不是转化为 ES5 ,因为不同类型以及不同版本的浏览器对 ES Next 新特性的支持程度都不一样,对于目标环境已经支持的部分,Babel 可以不转化,所以 Babel 会依赖运行环境的版本。
CLI
@babel/cli
是的 babel 的 CLI 工具。让我们可以使用babel
命令编译文件,该命令存放在babel/cli/bin
目录下。
Babel 工作流程
babel 运行分为三个阶段:
- 解析 Parser :通过模块
@babel/parser
将源码解析成 AST - 转换 Transfrom:通过一系列插件 转换成新的 AST
- 生成 Generator:模块
@babel/generator
将转换后的 AST 重新生成新的代码
@babel/core
@babel/core 依赖于@babel/parse、 @babel/traverse、@babel/types、@babel/template 等 模块, 通过 @babel/parse 解析器,基于ESTree 规范,将源码转换成 AST。再使用 @babel/traverse 模块遍历 AST,并在遍历过程中调用 visitor 函数进行 AST 的增删改,该模块提供了 path 的 api。然后转换能力全部通过插件进行。
@babel/core 模块提供了以下几个方法可进行转换:
- 字符串形式的 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(抽象语法树)进行转换
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 内部引用了 parser、generator等模块
const res = babel.transform(code, {
// 是否生成ast
ast: true,
// 是否生成解析的代码
code: true,
// 是否生成 sourcemap
sourceMaps: true
// 用到的插件
// plugins: ['@babel/plugin-transform-arrow-functions']
})
console.log(res)
可以看出转换后的结果包含源码的 ast、转换后的 code 以及 map 等。
从 code 可以看出没有进行转换,是因为 core 模块本身不提供转换能力,需要使用插件进行转换。
对箭头函数进行转换:
// index.js
// 设置转换箭头函数的插件
const res = babel.transform(code, {
plugins: ['@babel/plugin-transform-arrow-functions']
})
以上看出,箭头函数转换成了普通函数 ,但是 const 没有转换成 var,是因为只使用了转换箭头函数的插件。
traverse
@babel/traverse 是用来自动遍历AST的工具,它会访问树中的所有节点,在进入每个节点时触发 enter 钩子函数,退出每个节点时触发 exit 钩子函数。开发者可在钩子函数中对 AST 进行修改。
import traverse from '@babel/traverse'
traverse(ast, {
enter(path) {
// 进入 path 后触发
},
exit(path) {
// 退出 path 前触发
}
})
types
@babel/types 是作用于 AST 的类 lodash 库,其封装了大量与 AST 有关的方法,大大降低了转换 AST 的成本。@babel/types 的功能主要有两种:一方面可以用它验证 AST 节点的类型,例如使用 isClassMethod 或 assertClassMethod 方法可以判断 AST 节点是否为 class 中的一个 method;另一方面可以用它构建 AST 节点,例如调用 classMethod 方法,可生成一个新的 classMethod 类型 AST 节点 。
template
@babel/template 实现了计算机科学中一种被称为准引用(quasiquotes)的概念。说白了,它能直接将字符串代码片段(可在字符串代码中嵌入变量)转换为 AST 节点。例如下面的例子中,@babel/template 可以将一段引入 axios 的声明直接转变为 AST 节点。
import template from '@babel/template'
const ast = template.ast(`
import vue from 'vue'
`)
Presets 和 Plugins
Presets
预设是一系列插件的集合。
比如:
@babel/preset-env
@babel/preset-react
@babel/preset-typescript
preset 的顺序
preset 是逆序排列的(从后往前)。主要是为了确保向后兼容。这里涉及到配置的继承和重写概念。
继承和重写是面向对象编程语言中的概念,是指一个类扩展自父类,并且重新实现了一些属性和方法。这种思想不止在编程语言中用到,在配置文件中也广泛使用。比如 eslint 中 extends
的配置。
除了整体重写以外,还支持文件级别和环境级别的重写:
文件级别:
overrides: [
{
test: 'src/test.js',
plugins: ['pluginA']
}
]
环境级别:
{
envName: 'development',
env: {
development: {
plugins: ['pluginA']
},
production: {
plugins: ['pluginB']
}
}
}
preset 的参数
preset 可以接受参数,参数由插件名和参数对象组成一个数组。
@babel/preset-env
最常用的是@babel/preset-env
,是一个智能预设。其转译是按需的,对于环境支持的语法可以不做转换的。开发者只需使用最新的 JavaScript,而无需微观管理目标环境所需的语法转换(以及可选的浏览器 polyfill)。
通过配置 targets
属性指定目标浏览器,让 Babel 只转译环境不支持的语法。如果没有配置会默认转译所有 ES Next 的语法。
// 增加 presets配置
presets: [
'@babel/preset-env'
]
默认转成 ES5:
然后给targets
指定 chrome: '67'
(chrome67 版本是支持 const 和箭头函数的)
presets: [
[
'@babel/preset-env'
{
targets: {
chrome: '67'
}
}
]
]
const 和箭头函数并没有进行转换。
创建 preset
导出一个函数,函数的返回值为配置对象。
preset 可以是插件组成的数组:
module.exports = (api, opts) => ({
plugins: ['pluginA', 'pluginB', 'pluginC']
})
也可以包含其他的 preset 和带参数的插件:
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
插件分为语法插件和转换插件。
语法插件
作用于解析阶段。
大多数语法都可以被 babel 转换,极少数情况下需要使用插件解析特定类型的语法。官方的语法插件以
babel-plugin-syntax
开头。
语法插件虽名为插件,但事实上其本身并不具有功能性。所对应的语法功能其实都已在@babel/parser
里实现,插件的作用只是将对应语法的解析功能打开。
转换插件
作用于转换阶段,用于转换代码。
官方的转换插件以babel-plugin-transform
(正式)或 babel-plugin-proposal
(提案)开头。
转换插件将启用相应的语法插件,因此你不必同时指定这两种插件。
为什么在配置文件中有了预设还需要插件?
- 有些最新的语法可能还处于提案阶段,没有加入预设集合
- 根据项目需求,自己编写的插件
执行顺序
plugin 和 preset 都是通过数组的形式在配置文件中配置,生效顺序是先 plugin 后 preset,plugin 从左到右,preset 从右到左,这样的生效顺序使得配置里的插件可以覆盖 preset 里面插件的配置的,也就是重写。
core-js
Babel 把 ES Next 标准分为 syntax
和 built-in
两种类型。syntax 是语法,比如:const、 () =>
等。babel 默认只转换 syntax 类型。而 Promise、Set、Map 等环境所内置的 API 、静态方法 Array.from、Object.assign
、实例方法Array.prototype.includes
等属于 built-in。而 built-in 类型需要通过 polyfill 来完成转译。
Babel 在 7.4.0 版本中废弃 @babel/polyfill ,改用 core-js 替代。
配置 useBuiltIns
在 @babel/preset-env
中通过 useBuiltIns
参数来控制 built-in 的注入。可选值为 'entry'、'usage'、false
。默认值为 false,不注入 polyfill。
- false :默认值,不注入 polyfill。
- 'entry':需在整个项目的入口处手动引入 core-js ,
import 'core-js'
。该方式编译后 Babel 会把目标环境不支持的所有 built-in 都注入进来,不管这些 API 是否使用到,极大的增加打包后包的体积。 - 'usage':不需在项目的入口处手动引入,Babel 会在编译源码的过程中根据 built-in 的使用情况来选择注入相应的实现。如下图:
配置 corejs 版本
当 useBuiltIns 设置为 'usage' 或者 'entry' 时,还需要设置 @babel/preset-env 的 corejs 参数,用来指定注入 built-in 的实现时,使用 corejs 的版本。否则 Babel 日志输出会有一个警告。
module.exports = {
presets: [
[
'@babel/preset-env',
{
targets: {
edge: '17',
firefox: '60',
chrome: '67',
safari: '11.1'
},
useBuiltIns: 'usage',
corejs: '3.6.5',
// 开启调试模式,编译日志会打印在控制台
debug: true
}
]
]
}
@babel/runtime
以 class 为例:
// source.js
class Person {
name = 'jack'
age = 18
static skill = 'eat'
}
编译结果:
// 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
四个 helper 函数,用于创建构造函数、设置属性、创建实例。这只是 class 语法的辅助函数,其他 syntax 类型的语法也会生成 helper 函数,比如:extends 关键字。然而真实的项目开发中,文件动辄几十、上百甚至上千,这样打包后的文件会非常大。
而 @babel/runtime
模块提供了 helper 函数的集合。在编译后的文件中不是再创建 helper 函数,而是引用@babel/runtime 提供的 helper 函数。使用该模块还需要用到 @babel/plugin-transform-runtime
插件。
yarn add @babel/plugin-transform-runtime -D
yarn add @babel/runtime
@babel/runtime 是一个运行时依赖,所以不需要加-D 参数。
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 函数都是引用@babel/runtime 模块提供的 helper 函数了。
@babel/plugin-transform-runtime
该插件除了可以在文件中引用 helper 函数外,还为编译后的代码创建了一个沙箱环境,避免污染全局作用域。
比如通过引入 polyfill 的方式实现数组的 includes
方法,编译后的结果是:
'use strict'
require('core-js/modules/es.array.includes.js')
var arr = [1, 2, 3]
arr.includes(3)
再看 core-js 中实现 includes 方法的源码可以看出直接往 Array.prototype 上增加 includes 方法,如下:
直接修改原型的会造成全局污染。假如开发的是工具库,被其它项目引用,而恰好该项目自身实现了 Array.prototype.includes 方法,那这样就造成了冲突。
去掉 presets 的corejs
和 useBuiltIns
属性,同时也就不再需要 @babel/runtime 和 core-js 模块了。
module.exports = {
presets: [
[
'@babel/preset-env'
]
],
plugins: [
[
'@babel/plugin-transform-runtime',
{
corejs: 3
}
]
]
}
给@babel/plugin-transform-runtime 插件设置 corejs 配置,包含三个值:
false 默认
2:安装
@babel/runtime-corejs2
3:安装
@babel/runtime-corejs3
根据 corejs 得安装对应的模块,编译后的结果如下:
'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
仅仅是一个局部变量,不会对 Array.prototype 造成污染。
如何验证?
给 presets 配置 corejs 的方式,将编译后的文件在不支持 Array.prototype.includes 的浏览器中运行,在浏览器控制台或者引用该文件之后就可以使用[].includes
,而给@babel/plugin-transform-runtime 插件配置 corejs 的方式,编译后的文件同样的浏览器运行时会报错,不存在此方法。
参考
Babel 文档
Babel 用户手册