Webpack 5 @babel/preset-env 与 @babel/polyfill && core-js:3 regenerator-runtime 之间的关系

背景

最早有 Babel polyfill 的概念是接触 ES6 语法时,大概是 2016 年,被 polyfill 坑的时候是 2021 年开发谷歌插件时,经过 webpack 打包的代码中,我竟然看到有东西在重写 Promise,导致了原生的 Promise 不可用,后来我就开始对 Polyfill 进行了解,本文主要还是想彻底搞明白 Babel 怎么针对不同版本下的 ES 高级语法的支持和转化问题,怎么让自己的代码尽可能地少被 Babel 转化,从而利用浏览器原生 API 来提高代码运行速度,以及缩短打包时间。

什么是 @babel/preset-env

@babel/preset-env 是 Babel 提供的一个智能预设(preset),用于根据目标环境自动决定需要转换哪些现代 JavaScript 语法和引入哪些 Polyfill,以确保代码在指定的运行环境中能够正常运行。它是 Babel 最常用的预设之一,旨在简化 Babel 的配置,提升开发效率,同时优化最终打包后的代码体积和性能。

@babel/preset-env 的主要作用

  1. 自动语法转换(Transpilation): @babel/preset-env 根据你指定的目标环境(如特定版本的浏览器或 Node.js),自动选择需要转换的 JavaScript 语法特性。例如,将箭头函数、类、模块等 ES6+ 语法转换为旧环境支持的语法。

  2. 自动 Polyfill 引入: 除了语法转换,@babel/preset-env 还可以根据目标环境的支持情况,自动引入必要的 Polyfill(通过 core-js),以填补那些旧环境中缺失的内置对象和方法,如 PromiseArray.prototype.includesObject.assign 等。

  3. 优化打包体积: 通过智能地选择需要转换和引入的 Polyfill,@babel/preset-env 可以帮助你减少最终打包后的代码体积,提升应用的加载速度和性能。

什么是 @babel/plugin-transform-runtime?

@babel/plugin-transform-runtime 是 Babel 提供的一个插件,旨在优化 JavaScript 代码的转译过程,减少代码体积,避免全局污染,并改善代码的可维护性。它通过引用 Babel 运行时(@babel/runtime)中的辅助函数和 Polyfill,替代在每个模块中重复生成相同的辅助代码,从而提高构建效率和运行时性能。

主要功能

  1. 辅助函数的复用:

    • 问题:在没有使用该插件的情况下,Babel 在每个需要转换的文件中都会插入相同的辅助函数(如 _extends_asyncToGenerator 等),导致生成的代码体积增大。

    • 解决方案:@babel/plugin-transform-runtime 将这些辅助函数提取到 @babel/runtime 包中,通过引用的方式复用这些函数,避免在每个文件中重复生成相同的代码。

  2. Polyfill 管理:

    • 问题:直接在全局环境中注入 Polyfill(如通过 @babel/preset-envuseBuiltIns: "entry""usage")可能导致全局命名空间污染,尤其是在库开发中会引发兼容性问题。

    • 解决方案:该插件通过引用 @babel/runtime 中的 Polyfill,避免将 Polyfill 注入到全局环境中。这不仅减少了全局污染,还提升了代码的模块化和复用性。

  3. 支持生成器和 async/await

    • 问题:转换生成器函数和 async/await 语法需要 regenerator-runtime,直接在全局环境中引入可能引发冲突和性能问题。

    • 解决方案:插件自动引入 regenerator-runtime,确保生成器和 async/await 的正常运行,同时避免全局污染。

// 不使用 plugin-transform-runtime 插件 辅助函数都会内联到每个文件里面去
"use strict";

function _typeof(obj) { "@babel/helpers - typeof"; ... } // 一个 typeof 辅助函数(省略部分内容)
function _inherits(subClass, superClass) { ... }         // _inherits 辅助函数在此内联
function _createSuper(Derived) { ... }                   // _createSuper 辅助函数在此内联
function _classCallCheck(instance, Constructor) { ... }  // _classCallCheck 辅助函数在此内联

var Parent = /*#__PURE__*/function () {
  function Parent(name) {
    _classCallCheck(this, Parent);
    this.name = name;
  }
  return Parent;
}();

var Child = /*#__PURE__*/function (_Parent) {
  _inherits(Child, _Parent);
  var _super = _createSuper(Child);

  function Child() {
    _classCallCheck(this, Child);
    return _super.apply(this, arguments);
  }

  Child.prototype.sayName = function sayName() {
    return "My name is ".concat(this.name);
  };

  return Child;
}(Parent);

var c = new Child('Alice');
console.log(c.sayName());

// 使用plugin-transform-runtime 则会统一从babel/runtime引用辅助函数
// 这个过程必须是打包过程中,必须要让插件在生命周期环节替换进去
"use strict";

var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
var _inherits = _interopRequireDefault(require("@babel/runtime/helpers/inherits"));
var _createSuper = _interopRequireDefault(require("@babel/runtime/helpers/createSuper"));
var _classCallCheck = _interopRequireDefault(require("@babel/runtime/helpers/classCallCheck"));

var Parent = /*#__PURE__*/function () {
  function Parent(name) {
    (0, _classCallCheck.default)(this, Parent);
    this.name = name;
  }

  return Parent;
}();

var Child = /*#__PURE__*/function (_Parent) {
  (0, _inherits.default)(Child, _Parent);
  var _super = (0, _createSuper.default)(Child);

  function Child() {
    (0, _classCallCheck.default)(this, Child);
    return _super.apply(this, arguments);
  }

  Child.prototype.sayName = function sayName() {
    return "My name is ".concat(this.name);
  };

  return Child;
}(Parent);

var c = new Child('Alice');
console.log(c.sayName());

什么是 babel/polyfill?

@babel/polyfill · Babel

外国人起名字那是相当用心的,polyfill 翻译成中文叫垫片,一开始我真想不明白,为何叫垫片,但是我画了张图,我一下子就明白了,在一个有坑的道路上,如果我们想让坑变平,我们就得往里面填点东西,于是就有了下面的图。

IE 浏览器时代(好多人都没经历过了),有些企业和机构死死地使用 IE6 版本,或者 IE8 版本,绝不升级,因此你想利用 Chrome 新功能开发一些新东西,抱歉,那不行,比如你用个 Promise 写个代码,那是跑不了的,因为 IE 浏览器没这东西,于是有些人就把这些没有的对象都用了一种替换的方式写了出来。这些东西统称为 Polyfill

babel/polyfill 的弃用原因

随着 JavaScript 生态系统的发展,babel/polyfill 的一些设计决策逐渐显现出限制性,导致其不再适应现代开发需求。以下是主要原因:

a. 全局污染

babel/polyfill 会在全局环境中注入大量 Polyfill,这可能导致全局命名空间污染和潜在的命名冲突,尤其是在大型项目或使用多个库的情况下。

b. 不灵活的 Polyfill 引入

babel/polyfill 会引入所有 Polyfill,而不仅仅是项目中实际需要的部分。这导致最终打包文件的体积增大,影响性能。

c. 缺乏模块化支持

随着模块化开发的普及,开发者更倾向于按需引入 Polyfill,以优化性能和代码体积。babel/polyfill 缺乏这种灵活性,不利于现代构建工具(如 Webpack、Rollup)的优化机制。

d. 与 core-jsregenerator-runtime 的整合

Babel 社区和核心团队决定将 Polyfill 的管理和引入分离,提供更灵活和高效的解决方案。因此,babel/polyfill 被正式弃用,推荐使用 core-jsregenerator-runtime

npm install core-js@3 regenerator-runtime --save

regenerator-runtime是什么?

解决 async/await 语法转化的 polyfill,看下面的转化代码用 while 的模式就可想而知性能有多差了

// 原代码
async function fetchData() {
  const response = await fetch('https://api.example.com/data');
  const data = await response.json();
  return data;
}

// 经过regenerator-runtime的代码
"use strict";

var _regeneratorRuntime = require("regenerator-runtime/runtime");

function fetchData() {
  return _regeneratorRuntime.async(function fetchData$(_context) {
    while (1) {
      switch (_context.prev = _context.next) {
        case 0:
          _context.next = 2;
          return _regeneratorRuntime.awrap(fetch('https://api.example.com/data'));

        case 2:
          response = _context.sent;
          _context.next = 5;
          return _regeneratorRuntime.awrap(response.json());

        case 5:
          data = _context.sent;
          return _context.abrupt("return", data);

        case 7:
        case "end":
          return _context.stop();
      }
    }
  });
}

@babel/preset 的.babelrc 配置是有效的

Options · Babel

所以,针对于 node,electron,chrome 都是起到转义作用的

下面这种 node:"16", electron:"22",chrome:"108" 这几个预设,使得"userBuiltIns":"usage",corejs":3 都失去作用,因此我 package.json 不安装 core-jsregenerator-runtime也没啥影响,但当 target:{ie:11} 时,打包就会用 core-js 和 regenerator-runtime,控制台会提醒你找不到这些库

"env": {
    "main": {
      "presets": [
        [
          "@babel/preset-env",
          {
            "modules": "commonjs",
            "targets": {
              "node": "16"
            },
            "useBuiltIns": "usage",
            "corejs": 3,
            "bugfixes": true
          }
        ]
      ]
    },
    "renderer": {
      "presets": [
        [
          "@babel/preset-env",
          {
            "modules": false,
            "targets": {
              "electron": "22"
            },
            "useBuiltIns": "usage",
            "corejs": 3,
            "bugfixes": true
          }
        ]
      ]
    },
    "preload": {
      "presets": [
        [
          "@babel/preset-env",
          {
            "modules": false,
            "targets": {
              "electron": "22"
            },
            "useBuiltIns": "usage",
            "corejs": 3,
            "bugfixes": true
          }
        ]
      ]
    },
    "web": {
      "presets": [
        [
          "@babel/preset-env",
          {
            "modules": false,
            "targets": {
              "chrome": "106"
            },
            "useBuiltIns": "usage",
            "corejs": 3,
            "bugfixes": true
          }
        ]
      ]
    }
  },

综上

  1. Chrome 扩展开发都是基于高版本的谷歌内核,所有 ES 语法都已经支持了,可以将 Chrome 版本设置高一点,比如 Chrome:"106" 这样插件打出来的包速度很快,且体积也会很小,基本不会做语法糖转义

  2. Electron 渲染进程直接设定好 Electron 支持版本,例如我用的 22 版本,或者注入 webview 的 web 打包,则设定 Chrome:"108",都能有效提高打包速度,同时大大降低打包后代码的体积,还能直接试用 Chrome 提供的原生 API 进行开发。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

森叶

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值