Babel 对源码的转译涉及 2 个部分:
一个是语法类。比如箭头函数 () => {}
转译为普通函数 function() {}
。
一个是 API 补丁类(Polyfill)。针对转译产物"运行时(runtime)"做的一些处理,该部分主要处理 API 对宿主环境的兼容问题、转译产物尺寸优化等问题,比如为低版本浏览器提供数组的 includes
方法模拟实现: [1, 2, 3].includes(...)
。
runtime 是相对于语法类转译的状态而言的,指的是转译产物在宿主环境运行时的状态。
本章将介绍 Babel 在转译 "运行时(runtime)" 时涉及的各类工具。
# 10.1 core-js
core-js
提供了对 ECMAScript 标准、ECMAScript 提案、WEB 部分标准 API 等方面,对于目标宿主环境的补丁实现。
对其常见的一个用法是兼容低版本浏览器,比如 Promise
在部分低的版本浏览器下是不支持的(如IE 6-10),core-js
提供了 Promise
对象的模拟实现。
随着 ECMAScript、WEB 标准的不断发展,core-js
也在不断迭代,截至本书编写时,core-js
主要有两个版本系列: core-js@2
、core-js@3
。
# 10.2 babel-preset-env
babel-preset-env 的功能有如下特点:
- 提供语法转译功能
- 将 API 补丁以「全局变量」「手动/自动」「全量引入/按需引入」的形式引入转译产物
babel-preset-env 的配置项有很多,和 runtime 相关的主要配置是: useBuiltIns
和 corejs
。
比如,待转译的源码 Promise
,通过 babel-preset-env 转译时,相应配置项的含义如下:
useBuiltIns
: 配置「全局变量」「自动/手动」「按需/全量」引入 core-js 的 API 补丁useBuiltIns: false (default)
: 「手动」引入core-js
useBuiltIns: true
: 「自动」「全量」引入core-js
useBuiltIns: "entry"
: 需要在源码入口文件中「手动」引入core-js
比如:
"use strict"; Promise;
1
2
3没什么变化,这是因为源码中没有引入
core-js
。源码调整为:
import 'core-js' Promise
1
2转译结果为:
"use strict"; require("core-js/modules/es6.array.copy-within.js"); // 全量的 core-js 子包 ... require("core-js/modules/web.dom.iterable.js"); Promise;
1
2
3
4
5
6
7
8
9
10可以看到,转译结果有以下特点:
- 引入的
Promise
补丁污染了全局变量 - 补丁是全量引入的
- 引入的
useBuiltIns: "usage"
: 会「自动」「按需」引入core-js
的 API 补丁比如转译如下代码时:
Promise
1转译结果是:
"use strict"; require("core-js/modules/es6.object.to-string.js"); // 影响全局变量 Promise require("core-js/modules/es6.promise.js"); Promise;
1
2
3
4
5
6
7
8可以看到,转译结果有以下特点:
- 引入的补丁污染了全局变量
- 补丁是按需自动引入的
如果目标环境需要引入的话,转译时会自动引入对
Promise
的 API 补丁。
总结一下:
useBuiltIns
的值是否自动引入 Promise
补丁是否全量引入 core-js 污染全局变量 Promise
false
手动 根据手动引入方式 污染全局变量 true
自动 全量 污染全局变量 "usage"
自动 按需 污染全局变量 "entry"
手动 全量 污染全局变量 可以看到,
useBuiltIns
提供的Promise
Polyfil 依然是会污染全局变量Promise
的。corejs: string | { version: string, proposals: boolean }
只有在 babel-preset-env 的 `useBuiltIns` 配置项的值为 `usage | entry` 的情况下,`corejs` 的配置才会生效。 `corejs` 配置项引入了 `core-js` 工具包,它提供了高版本 API 的兼容性实现。默认 `corejs@2`(随着发展,默认值可能调整为 `corejs@3`)。 `corejs` 配置的特点如下: + `corejs` 可以设置精确的版本号 如 `corejs: "3.8"`。 + `corejs` 可配置是否支持提案 API 比如目标源码是: ```js // 截稿时,globalThis 是提案阶段 API globalThis ``` 当 babel-preset-env 的配置项如下(不开启对提案 API 的补丁支持),: ```js { useBuiltIns: 'usage', corejs: { version: 3, proposals: false } } ``` 转译结果是: ```js "use strict"; globalThis; ``` 如果开启对提案 API 的补丁支持(配置 `proposals: true`),转译结果是: ```js "use strict"; require("core-js/modules/esnext.global-this.js"); globalThis; ```
# 10.3 babel-plugin-transform-runtime
2 个核心作用
作用 1: 提供 module helper
取消硬编码的辅助函数(inline helper),改为模块引入(module helper)的形式,减少转译后文件体积。
比如,源码:
class Base {} // class 为语法部分
1不同配置下的转译结果不同。
不引入 babel-plugin-transform-runtime
Babel 配置:
{ presets: [ [ '@babel/preset-env' ] ] }
1
2
3
4
5转译结果是:
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } var Base = function Base() { _classCallCheck(this, Base); };
1
2
3
4
5
6
7
8
9引入 babel-plugin-transform-runtime
Babel 配置:
{ presets: [ [ '@babel/preset-env' ] ], plugins: [ [ '@babel/plugin-transform-runtime' ] ] }
1
2
3
4
5
6
7
8转译结果是:
var _classCallCheck = _interopRequireDefault(require("@babel/runtime/helpers/classCallCheck")); var Base = function Base() { (0, _classCallCheck["default"])(this, Base); };
1
2
3
4
5_classCallCheck
方法被以第三方模块引入的方式定义,避免出现重复的函数定义。另外可以看到,在没有配置 babel-plugin-transform-runtime 的
corejs
参数时,_classCallCheck
默认引用自babel-runtime
以提供各类 helpers。也可以配置
corejs
调整 helpers 的来源(如从 babel-runtime 包改为 core-js 包)。
作用 2: 提供局部 API 补丁
将 API 补丁以局部变量的形式引入到转译产物,避免污染全局对象。
未引入 babel-plugin-transform-runtime 时,babel-preset-env 引入的 API polyfill 是污染全局的。
比如,源码:
Promise
1Babel 未引入 babel-plugin-transform-runtime 时:
{ presets: [ [ '@babel/preset-env', { useBuiltIns: 'usage' } ] ] }
1
2
3
4
5转译结果:
require("core-js/modules/es.promise"); // 会影响全局变量 Promise new Promise()
1
2Babel 引入 babel-plugin-transform-runtime 时:
{ presets: [ [ '@babel/preset-env', { useBuiltIns: 'usage' } ] ], plugins: [ [ '@babel/plugin-transform-runtime', { corejs: 3 } ] ] }
1
2
3
4
5
6
7
8转译结果是:
// 局部变量 var _promise = _interopRequireDefault(require("@babel/runtime-corejs3/core-js-stable/promise")); new _promise["default"]();
1
2
3可以看到,babel-plugin-transform-runtime 会将补丁以局部变量的形式提供给转译结果。
babel-plugin-transform-runtime 配置
babel-plugin-transform-runtime
与babel-preset-env
的useBuiltIns
配置是互斥的,只能选一个配置。corejs: false | 2 | 3 | { version: 2 | 3, proposals: boolean }
配置对 core-js 包的使用false
直接使用
babel-runtime
包提供的辅助函数(helpers),而不是core-js
提供的辅助函数。该配置项需要项目增加对
babel-runtime
包的依赖(目前 name 是@babel/runtime
)。2
使用
babel-runtime-corejs2
包提供的辅助函数。corejs: 2
仅支持全局变量(如Promise
)和静态属性(如Array.from
)。该配置项需要项目引入依赖
babel-runtime-corejs2
包。3
使用
babel-runtime-corejs3
提供的辅助函数。corejs: 3
除了支持全局变量(如Promise
)和静态属性(如Array.from
),也支持实例的属性(如[].includes
)。该配置项需要项目引入依赖
babel-runtime-corejs3
包。{ version: 2 | 3, proposals: boolean }
与 babel-preset-env 中关于
corejs
的配置一致,描述是否提供提案 API 补丁注入支持。
helpers: boolean
表示 babel-plugin-transform-runtime 是否开启将硬编码形式的 helpers 函数,转为模块式的引入。默认是
true
。举例,目标源码是:
class A {}
1Babel 在如下配置时:
{ presets: [ [ '@babel/preset-env', { useBuiltIns: 'usage' } ] ], plugins: [ [ '@babel/plugin-transform-runtime', { helpers: false } // 不开启模块化引入 helpers 的功能 ] ] }
1
2
3
4
5
6
7
8
9
10
11转译结果是:
"use strict"; // 这是硬编码定义的 helpers 函数 function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } var A = function A() { _classCallCheck(this, A); };
1
2
3
4
5
6
7
8
9
10
11
12当设置
helpers: true
时,转译结果是:"use strict"; var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault"); var _classCallCheck2 = _interopRequireDefault(require("@babel/runtime/helpers/classCallCheck")); var A = function A() { (0, _classCallCheck2.default)(this, A); };
1
2
3
4
5
6
7
8
9useESModules: boolean
是否引入
esm
格式的 Babel helpers 函数,默认是false
,即默认不开启。举例,目标源码是:
class A {}
1Babel 在如下配置时:
{ presets: [ [ '@babel/preset-env', { useBuiltIns: 'usage' } ] ], plugins: [ [ '@babel/plugin-transform-runtime', { useESModules: false } // 默认值,不引入 esm 格式的 Babel helpers 函数 ] ] }
1
2
3
4
5
6
7
8
9
10
11转译结果是:
"use strict"; var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault"); // 这是非 esm 格式的 helper 函数 var _classCallCheck2 = _interopRequireDefault(require("@babel/runtime/helpers/classCallCheck")); var A = function A() { (0, _classCallCheck2.default)(this, A); };
1
2
3
4
5
6
7
8
9
10当设置
useESModules: true
时,转译结果是:"use strict"; var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault"); // 这是 esm 格式的 helper 函数 var _classCallCheck2 = _interopRequireDefault(require("@babel/runtime/helpers/esm/classCallCheck")); var A = function A() { (0, _classCallCheck2.default)(this, A); };
1
2
3
4
5
6
7
8
9
10regenerator: boolean
表示是否提供辅助方法用于转译
generator
函数,默认是true
。举个实际的例子就比较清楚了,待转译的源码:
function* run() {}
1如果 Babel 配置是:
{ presets: [ [ '@babel/preset-env' ] ], plugins: [ [ '@babel/plugin-transform-runtime', { regenerator: true } // 默认值,提供辅助方法用于转译 generator 函数 ] ] }
1
2
3
4
5
6
7
8
9
10
11转译结果是:
"use strict"; var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault"); // 提供的辅助函数用于转译 generator 函数 var _regenerator = _interopRequireDefault(require("@babel/runtime/regenerator")); var _marked = /*#__PURE__*/_regenerator.default.mark(run); function run() { return _regenerator.default.wrap(function run$(_context) { while (1) { switch (_context.prev = _context.next) { case 0: case "end": return _context.stop(); } } }, _marked); }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20如果设置
regeneraotr: false
,转译结果是:"use strict"; var _marked = /*#__PURE__*/regeneratorRuntime.mark(run); function run() { return regeneratorRuntime.wrap(function run$(_context) { while (1) { switch (_context.prev = _context.next) { case 0: case "end": return _context.stop(); } } }, _marked); }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15可以看到,
regeneraotr: false
时,转译结果代码依赖一个全局对象regeneratorRuntime
,从安全性上说,对全局变量的依赖是不稳定的。有一章将详细介绍 Babel 对 async / generator 的转译。
# 10.4 案例
先总结一下 Babel 对 runtime 的转译配置(伪代码):
配置组合 | 语法部分(babel-preset-env 的功能) | API 补丁是否引入 | API 补丁副作用 | 辅助函数引入方式 |
---|---|---|---|---|
[babel-preset-env] | 执行转译 | 无 | 无 | 硬编码 |
[babel-preset-env, { useBuiltIns: entry, corejs: 2/3 }] | 执行转译 | 全量、需源码中手动引入 core-js | 污染全局对象和原型链 | 硬编码 |
[babel-preset-env, { useBuiltIns: usage, corejs: 2/3 }] | 执行转译 | 按需、自动引入 core-js | 污染全局对象和原型链 | 硬编码 |
[babel-preset-env] + [babel-plugin-transform-runtime] | 执行转译 | 无 | 无 | 模块引用 |
[babel-preset-env] + [babel-plugin-transform-runtime, { corejs: 2/3 }] | 执行转译 | 按需、自动引入 core-js | 不污染全局对象和原型链 | 模块引用 |
接下来以案例解释上述工具在 Babel 转译过程的作用。
# 准备工作
待转译的源文件为 index.js
class Base {} new Promise()
1
2这段源码包含了语法和 API 部分:
class
为语法部分Promise
为API部分
配置文件使用
.babelrc
转译产物文件为
index-compiled.js
使用命令行
我们希望这段源码转为 ES5 版本,该怎么做呢?假设,用 Babel 命令行执行转译,通过
.babelrc
配置 Babel,转译产物文件为 index-compiled.js。npx babel index.js --config-file ./.babelrc --out-file index-compiled.js
安装 node 或 npm 后,可直接使用 npx 唤起各类命令行执行
# 不同配置的效果
.babelrc
空配置转译后,
index-compiled.js
的内容依然是源码的样子:class Base {} new Promise()
1
2使用 babel-preset-env 转译语法部分
配置
.babelrc
:{ "presets": [ [ "@babel/preset-env" ] ] }
1
2
3
4
5
6
7执行转译后,
index-compiled.js
的内容为:"use strict"; function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } var Base = function Base() { _classCallCheck(this, Base); }; new Promise();
1
2
3
4
5
6
7
8
9转译产物有以下特点:
- 语法转译:
Class
转为了普通函数 - 新增辅助函数: 新引入了工具方法
_classCallCheck
,用于校验构造函数被调用的方式
剩下的问题:
- 工具方法
_classCallCheck
是硬编码,增加了转移后的文件体积 - 未引入 API 补丁:
Promise
作为高级 API,没有引入补丁,低版本运行环境可能有兼容性问题
- 语法转译:
使用 babel-preset-env 的
useBuiltIns: "entry"
配置引入全量、全局的 API 补丁接下来修改
.babelrc
配置:{ "presets": [ [ "@babel/preset-env", { "useBuiltIns": "entry", "corejs": 3 } ] ] }
1
2
3
4
5
6
7
8
9
10
11如上配置,
useBuiltIns
为entry
时,需要手动在源码添加import 'core-js'
:import 'core-js' class Base {} new Promise()
1
2
3转译产物为:
"use strict"; require("core-js/modules/es.symbol"); ... require("core-js/modules/es.promise"); require("core-js/modules/es.promise.finally"); ... function _classCallCheck(...) { ... } } var Base = function Base() { _classCallCheck(this, Base); }; new Promise();
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19可以看到,相对源码,转译产物特点如下:
- 全局、全量引入了
core-js
提供的各类 API 实现 - 其他特点和仅配置了
babel-preset-env
的情况一致- 语法转译:
Class
转为了普通函数 - 新增辅助函数: 引入了工具方法
_classCallCheck
,校验构造函数被调用的方式
- 语法转译:
这里的问题是:
- 全局引入了 API 实现
- 全量引入了 API 实现,而源码中仅仅需要的是
Promise
的实现
- 全局、全量引入了
使用 babel-preset-env 的
useBuiltIns: "usage"
配置引入按需、全局的 API 补丁我们先解决上文中的第二个问题,改全量为按需,这需要设置 babel-preset-env 的
useBuiltIns
配置项为usage
。修改
.babelrc
配置:{ "presets": [ [ "@babel/preset-env", { "useBuiltIns": "usage", "corejs": 3 } ] ] }
1
2
3
4
5
6
7
8
9
10
11对了,源码中不再需要手动引入
core-js
了,恢复一下:class Base {} new Promise()
1
2转译后的代码:
"use strict"; require("core-js/modules/es.object.to-string"); require("core-js/modules/es.promise"); function _classCallCheck(instance, Constructor) { ... } } var Base = function Base() { _classCallCheck(this, Base); }; new Promise();
1
2
3
4
5
6
7
8
9
10
11
12
13转译后的代码特点:
- 按需引入了全局的
Promise
实现 - 其他特点和仅配置了
babel-preset-env
的情况一致- 语法转译:
Class
转为了普通函数 - 新增辅助函数: 引入了工具方法
_classCallCheck
,校验构造函数被调用的方式
- 语法转译:
它的问题是:
Promise
的 API 实现仍然是全局引入的
- 按需引入了全局的
使用 babel-plugin-transform-runtime 实现按需、局部变量形式引入 API 补丁
引入全局的
Promise
实现,一般而言没什么问题,但对于工具库等场景的开发而言,污染全局变量会引起不可预知的问题,需要将Promise
的兼容性实现改为局部变量。而 babel-preset-env 的
"useBuiltIns"
配置关注的是如何将 API 的实现以全局变量的形式引入转译产物。在此,需要使用babel-plugin-transform-runtime
引入局部的 Promise 实现。并且,babel-plugin-transform-runtime
与babel-preset-env
的useBuiltIns
配置是互斥的,只能选一个配置。修改
.babelrc
配置:{ "presets": [ [ "@babel/preset-env" ] ], "plugins": [ [ "@babel/plugin-transform-runtime", { "corejs": 3 } ] ] }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15转译产物为:
"use strict"; var _interopRequireDefault = require("@babel/runtime-corejs3/helpers/interopRequireDefault"); var _promise = _interopRequireDefault(require("@babel/runtime-corejs3/core-js-stable/promise")); var _classCallCheck2 = _interopRequireDefault(require("@babel/runtime-corejs3/helpers/classCallCheck")); var Base = function Base() { (0, _classCallCheck2["default"])(this, Base); }; new _promise["default"]();
1
2
3
4
5
6
7
8
9
10
11
12
13终于,我们看到:
- Promise 的 API 补丁被以局部变量的形式引入
_classCallCheck
和Promise
的实现均以require
的形式引入,而不是硬编码_classCallCheck
和Promise
的实现由babel-runtime-corejs3
提供,不再是babel-runtime
的 helpers 提供
使用 babel-plugin-transform-runtime 除去重复的辅助代码
细心的同学可能注意到,上例中,引入
babel-plugin-transform-runtime
后,_classCallCheck
这类工具方法被以第三方引用的方式定义,而非原来的硬编码。babel-plugin-transform-runtime
确实可以解决辅助工具方法被硬编码带来的重复编码问题。如果
.babelrc
配置为:{ "presets": [ [ "@babel/preset-env", { "useBuiltIns": "usage", "corejs": 3 } ] ], "plugins": [ [ "@babel/plugin-transform-runtime" ] ] }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16那么,转译产物为:
"use strict"; var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault"); require("core-js/modules/es6.promise"); require("core-js/modules/es6.object.to-string"); var _classCallCheck2 = _interopRequireDefault(require("@babel/runtime/helpers/classCallCheck")); var Base = function Base() { (0, _classCallCheck2["default"])(this, Base); }; new Promise();
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15可以看到,
_classCallCheck
通过require
的方式引入,而不再是硬编码的方式。