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-jsuseBuiltIns: true: 「自动」「全量」引入core-jsuseBuiltIns: "entry": 需要在源码入口文件中「手动」引入core-js比如:
"use strict"; Promise;1
2
3没什么变化,这是因为源码中没有引入
core-js。源码调整为:
import 'core-js' Promise1
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 补丁比如转译如下代码时:
Promise1转译结果是:
"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 污染全局变量 Promisefalse手动 根据手动引入方式 污染全局变量 true自动 全量 污染全局变量 "usage"自动 按需 污染全局变量 "entry"手动 全量 污染全局变量 可以看到,
useBuiltIns提供的PromisePolyfil 依然是会污染全局变量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 是污染全局的。
比如,源码:
Promise1Babel 未引入 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的方式引入,而不再是硬编码的方式。