2022-10-7 About 12 min

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@2core-js@3

# 10.2 babel-preset-env

babel-preset-env 的功能有如下特点:

  • 提供语法转译功能
  • 将 API 补丁以「全局变量」「手动/自动」「全量引入/按需引入」的形式引入转译产物

babel-preset-env 的配置项有很多,和 runtime 相关的主要配置是: useBuiltInscorejs

比如,待转译的源码 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
      
      1

      Babel 未引入 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
      2

      Babel 引入 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-runtimebabel-preset-envuseBuiltIns 配置是互斥的,只能选一个配置。

    • 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 {}
      
      1

      Babel 在如下配置时:

      {
          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
      9
    • useESModules: boolean

      是否引入 esm 格式的 Babel helpers 函数,默认是 false,即默认不开启。

      举例,目标源码是:

      class A {}
      
      1

      Babel 在如下配置时:

      {
          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
      10
    • regenerator: 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

    如上配置,useBuiltInsentry 时,需要手动在源码添加 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-runtimebabel-preset-envuseBuiltIns 配置是互斥的,只能选一个配置。

    修改 .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 补丁被以局部变量的形式引入
    • _classCallCheckPromise 的实现均以 require 的形式引入,而不是硬编码
    • _classCallCheckPromise 的实现由 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 的方式引入,而不再是硬编码的方式。

Last update: October 10, 2022 17:51
Contributors: hoperyy