基本概念

● Babel 是个 JS 编译器,与大多数编译器一样,包含“parsing”、“transforming”、“generation”三个处理阶段。如下图所示,代码首先经由 @babel/parser(曾用名 Babylon) 解析成抽象语法树(AST),然后对 AST 做遍历(@babel/traverse)和转换(各种@babel/plugin-..),最后根据转换后的 AST 生成新的常规 JS 代码(@babel/generator)。

● Babel 本身可看作是个流水线空盒子,每个阶段具体处理过程是通过各种插件(plugin)实现的,通过配置不同的插件组合达到不同的处理目的。
● Babel 的plugin有两类:
(1)Syntax Plugins(语法插件),用于使 babel 支持对特定语法的 parse。
(2)Transform Plugins(转译插件),用于 transforming 阶段的插件,此类插件包含了 parse 阶段相关的 Syntax Plugin 并会在代码转译为目标等级代码期间自动使用它们。
● 如果仅编译小范围内容,可以通过仅引入对应的 plugin 实现。更常见的方式是使用presets(预设),它是为特定目的预置的一组 plugin 的集合。
● 根据不同转换目的,常见的 presets 如下,
本文主要面向转换 ES 语法的场景

(1)用于转换 ES 语法的 @babel/preset-env。
(2)用于转换 React 语法的 @babel/preset-react。这个 preset 中包含的最著名插件是用于转换 JSX 语法的 @babel/plugin-transform-react-jsx。
(3)用于转换 TypeScript 语法的@babel/preset-typescript。
● preset 及 plugin 的使用,除了 npm 安装,还需要通过 Babel 配置文件(如“.babelrc”)配置,配置内容是个对象,对象格式示例如下,对象属性字段 “presets” 和 “plugins” 的取值分别是个数组,数组内容是具体 preset 或 plugin 名称的字符串,而当 preset 或 plugin 也要有自己的 options 时就使用数组表示,数组第一个元素是 preset 或 plugin 名称的字符串,第二个元素是对应的 options 对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
{
"presets": [
[
"@babel/preset-env",
{
"modules": false,
"useBuiltIns": "usage",
"corejs": 3,
"targets": {
"safari": "6"
}
}
]
],
"plugins": ["@babel/plugin-external-helpers"]
}

● 配置多个 preset 与 plugin 情况下的执行顺序:

1
2
3
(1)总体先执行 Plugin 再执行 Preset ,与二者声明次序无关。
(2)多个Plugin 按照声明次序顺序执行。
(3)多个Preset 按照声明次序逆序执行。

● ES 的转换通常指从 ES 高版本转换到低版本,从而让新的 ES 代码能在老版本的 ES 环境中运行。这里要转换的内容包含两类:新语法和新特性。新语法指的是 class、import/export、箭头函数、const、对象解构这类原有特性的新写法。新特性指的是原来版本不存在的特性,如 Promise、async/await、Array.prototype.includes()。对新语法支持在 @babel/preset-env 里,对新特性的支持在统称为 polyfill 的库中(具体是 @babel/runtime 等,详见下文)。通过合理配置,@babel/preset-env 会在解析转换过程中引入 polyfill 的内容。

● TypeScript 通过配置 target 字段也支持 ES 的语法转换,但不支持 polyfill 的自动添加。

使用 Babel 的方式

◇ 可以通过以下三种方式使用 Babel 做编译处理
(1)通过 @babel/cli(曾用名 babel-cli)使用”babel”命令执行。
(2)通过 webpack(babel-loader)和 rollup(@rollup/plugin-babel)等打包或编译工具调用。
(3)通过 @babel/register(曾用名 babel-register)为 Node.js 的 require 函数增加钩子,以在运行时调用 require 加载.es6, .es, .jsx 和.js 类型文件的时候先用 babel 进行转码处理,由于是实时转码更适合在 Node.js 开发环境使用。
◇ 无论使用哪种方式,都要先安装@babel/core 和提供 Babel 配置文件。

@babel/preset-env

◆ Babel 官方推荐使用 @babel/preset-env(曾用名 babel-preset-env,曾经的 babel-preset-es2015 和 babel-preset-es2016 以及 babel-preset-es2017 等特定版本的 preset 都已经被它取代)。
◆ @babel/preset-env 包含 es 所有版本的 preset,会根据所配置的目标环境自动选择特定版本 preset 及相关 plugin,这里的自动选择机制的实现基于 browserslist、compat-table、electron-to-chromium 等开源项目维护的浏览器与 es 新特性及 plugin 的映射关系。
◆ 配置 Babel 目标环境的方式有如下两种:
(1)browserslist:在没有配置”ignoreBrowserslistConfig”且”targets”没有配置 browserslist 相关选项时被使用,示例如下,browserslist 会根据配置生成一个目标浏览器的列表,这个列表可以通过 npx browserslist “配置字符串“ 命令查看。另外有网站提供了目标浏览器列表的图形化展示。

1
"browserslist": "> 0.25%, not dead"  // 市场占有率大于0.25%的浏览器(不包括IE10和BlackBerry等停止升级的浏览器)

(2)targets:”targets”配置支持更多的语法及其他可配置项:string | Array| { [string]: string },详细,如:

1
2
3
4
5
6
7
8
9
10
{
"targets": "> 0.25%, not dead" // 与上一条browserslist等价
}

{
"targets": { // 指定特定具体目标环境
"chrome": "58",
"ie": "11"
}
}

◆ browserslist 和 targets 的书写位置可选以下任一种:
(1)babel 配置文件中 全局 或 preset 或 plugin 的 option。
(2)package.json 文件中的字段。
(3)对于 browserslist,官方推荐写在.browserslistrc 文件,以适用于“ browserslist 同时提供给 babel 之外的库使用”的场景。

◆ 如果未配置目标运行平台,@babel/preset-env 会转换所有 ECMAScript 2015+ 的代码,这样就失去了面向各平台定制的意义,不推荐。

@babel/preset-env 的常用配置项

modules
(1)是否将 ES6 的 module 语法统一转换为指定的另一种 module 形式。
(2)取值:”amd” | “umd” | “systemjs” | “commonjs” | “cjs”(commonjs 的简写) | “auto” | false,默认值为”auto”,表示根据 caller 配置项判断,使用打包工具调用 Babel 情况下打包工具会自动提供 caller 配置项的值。
targets.esmodules
(1)默认值是 false,即认为目标平台不支持 ES 模块的定义与导入导出方式。
(2)支持标签的浏览器是支持 ES 模块的。
(3)设置 esmodules 之后其他 targets 设置将被忽略。
useBuiltIns
(1)标识如何处理全局垫片(polyfill,详见下一节),取值:”usage” | “entry” | false。
(2)默认值为 false 不做处理,此时 Babel 只转换新语法,不会转换 polyfill 所包含的 Promise 和 Symbol 等新的内置类型、Array.from 和 Object.assign 等新的静态方法、Array.prototype.includes 等新的实例方法、generator 方法。
(3)取值”usage”或”entry”时都需要安装@babel/polyfill(官方推荐按需安装 core-js 或 regenerator-runtime,详见下一节)配合使用,并设置与”useBuiltIns”并列的”corejs”配置项指明所使用的 core-js 版本。
(4)取值“entry”,需要在项目代码中手动引入 core-js 或 regenerator-runtime,且当仅 import 某个单独特性时支持模糊写法(如:import “core-js/es/array”),@babel/preset-env 会根据目标环境引入与特性关联的各具体文件(如在目标 safari 6 配置下会引入 core-js/modules/es.array.from、core-js/modules/es.array.for-each、core-js/modules/es.array.last-index-of、core-js/modules/es.array.copy-within 等文件的内容);
(5)取值“usage”,无需在项目代码中手动引入任何 core-js 或 regenerator-runtime,@babel/preset-env 会根据代码实际使用了哪些新特性并结合目标环境引入具体文件内容。
corejs
取值 string 或{ version: string, proposals: boolean },与”useBuiltIns”配合使用,见”useBuiltIns”(3)的说明。

polyfill

@babel/polyfill

■ @babel/polyfill(曾用名 babel-polyfill)包含 core-js(polyfill ES features)和 regenerator-runtime(to use transpiled generator functions)两部分,用于模拟一个完整的 ES2015+目标环境。
■ 官方推荐按需分别安装 core-js 或 regenerator-runtime 替代 @babel/polyfill(20230219 更新:@babel/polyfill 这个包已被弃用),并且不要直接整体引入,而推荐通过上节描述的 @babel/preset-env 的 UseBuiltins 选项选择性地引入所需特性,从而避免不必要的输出包体积增加。
■ @babel/polyfill 垫片通过在全局作用域增加类和函数定义、以及为内置类型和内置对象增加静态方法和增加 prototype 属性等修改全局环境的方式实现的,在不希望产生副作用的场景下,特别是项目定位是 tool/library 而不是 application 的时候,这种实现方式不适合。
■ polyfill 方式的配置:
(1)babel 配置文件中配置 @babel/preset-env 的”useBuiltIns”和”corejs”项,例如:

1
2
3
4
5
6
7
8
9
..."presets": [
...
["@babel/preset-env",{
"useBuiltIns": "usage",
"corejs": "3.28",
...
}],
...
]...

(2)如果使用 rollup,还需配置 @rollup/plugin-babel 的”babelHelpers”值为“bundled”。

@babel/runtime

○ @babel/runtime(曾用名 babel-runtime)作用同@babel/polyfill,区别是它通过注入模块化 helper(modular runtime helpers)的方式实现 core-js 和 regenerator-runtime 两部分垫片。
○ runtime 相关包目前有三个:
(1)@babel/runtime(仅包含 regenerator-runtime )。
(2)@babel/runtime-corejs2(包含 regenerator-runtime 和 core-js)。
(3)@babel/runtime-corejs3(包含 regenerator-runtime 和 core-js-pure),它与@babel/runtime-corejs2 的区别是除了支持全局类型与类型新的静态方法垫片之外,也支持类型新的实例方法垫片。所以通常安装这个 runtime 包。
○ 除了安装 @babel/runtime-corejs3 之外,也需要安装@babel/plugin-transform-runtime 插件用于处理重复 helper 代码的复用。
○ runtime 方式的配置:
(1)@babel/plugin-transform-runtime 插件没有像 polyfill 方式那样的“useBuiltIns: ‘usage’”选项,自身也没有“browserslist”或“targets”选项,会对代码中出现的所有垫片特性注入 helper(相关 issue:733010250)。20230221 更新:实践发现 babel 配置的全局 targets 项 ↓ 和全局“.browserslistrc”文件方式可使 @babel/plugin-transform-runtime 插件按需注入 helper。
(2)babel 配置中增加 @babel/plugin-transform-runtime 的如下配置:

1
2
3
4
5
6
7
8
9
...
"targets": "> 0.25%, not dead", // ↑ 使 @babel/plugin-transform-runtime 插件按需注入 helper
"plugins": [
...
["@babel/plugin-transform-runtime",{
"corejs": 3,
}],
...
]...

(3)如果使用 rollup,还需配置@rollup/plugin-babel 的”babelHelpers”值为“runtime”。

从打包内容看 polyfill 方式与 runtime 方式区别

◆ 以 webpack 打包目标环境不支持 Array.from 和 Array.prototype.includes 的以下原始内容为例。

1
2
Array.from("how are you");
["a","b","c"].includes("a");

◆ 使用的 polyfill 方式的相应输出大致如下(已简化)

1
2
3
4
5
6
7
8
9
10
11
...
/* 38 */ /*Array.from的实现与全局注册*/
...
/* 39 */ /*Array.prototype.includes的实现与全局注册*/
...
var core_js_modules_es_array_from__IMPORTED_MODULE = __webpack_require__(38);
var core_js_modules_es_array_includes__IMPORTED_MODULE = __webpack_require__(39);

Array.from("how are you");
["a","b","c"].includes("a");
...

◆ 使用的 runtime 方式的相应输出大致如下(已简化)

1
2
3
4
5
6
7
8
9
10
11
...
/* 38 */ /*Array.from的模块化实现*/
...
/* 39 */ /*Array.prototype.includes的模块化实现*/
...
var _babel_runtime_array_from__IMPORTED_MODULE = __webpack_require__(38);
var _babel_runtime_instance_includes__IMPORTED_MODULE = __webpack_require__(39);

_babel_runtime_array_from__IMPORTED_MODULE()("how are you");
_babel_runtime_instance_includes__IMPORTED_MODULE().call( ["a", "b", "c"], "a"));
...

◆ 最大区别在于 polyfill 方式由于修改了全局环境,原始代码写法被保留而不会被改写。
20230221 更新:polyfill 方式当前已支持通过polyfill provider RFC实现非全局垫片,issue 说明babel-plugin-polyfill-corejs3 的使用

Babel 配置文件

○ Babel 有两种配置文件,可以同时使用或单独使用。
(1)项目配置(Project-wide configuration),对整个项目生效,包含项目下的 node_modules(除非通过”exclude”排除),适合项目范围内广泛生效的配置,有”babel.config.js 文件”和”babel.config.json 文件”两种具体形式。
(2)文件关联配置(File-relative configuration),有”.babelrc(同.babelrc.js)文件”
和”项目 package.json 文件中的 babel 字段”两种具体形式。
○ 关于 Project-wide 配置:
(1)root 属性指明当前项目的根目录,Babel 默认以当前路径为 root 属性值,并在 root 路径下查找与应用”babel.config.js”文件配置。
○ 关于 File-relative 配置:
(1)适合为项目的子部分做配置。
(2)Babel 从被编译文件向上逐级查找与应用 File-relative 配置,未查找到则终止于 package.json 文件。
(3)查找 File-relative 配置的范围仅限通过 babelrcRoots 属性包含的路径,默认与 root 属性值一致,查找范围之外的文件关联配置 Babel 不会处理。
(4)Babel 7 新增特性: File-relative 配置的作用范围仅限 package 自己,不包含文件层级下面的 node_modules,也不会检测 node_modules 内的.babelrc,即使通过“include”包含了 node_modules。