一文聊完前端项目中的Babel配置

2022-10-08 10:40:12 浏览数 (1)

写在前边

大家好,今天来和大家聊聊 Babel Runtime 相关知识。

As a Front-end engineer,浏览器兼容性对于每个人来讲都是必不可少的话题。

在现代大多数项目中,我们都无需手动在代码中处理不同浏览器的兼容性写法,这正是 babel 的作用。

接下来,这篇文章我会着重和大家聊聊有关 @babel/plugin-transform-runtime 的详细用法,希望可以帮助到大家。

不出意外的话,这应该是我的Babel 专栏中关于配置项讲解的最后一篇文章。

我要说的话

关于 Babel 的用法、插件编写以及不同项目下的适用场景我在前几篇文章中或多或少都有一些介绍:

  • 「前端基建」带你在Babel的世界中畅游
  • 从Tree Shaking来走进Babel插件开发者的世界
  • 「前端基建」探索不同项目场景下Babel最佳实践方案

上述三篇文章中从浅到深依次讲述了 Babel 配置指南、Babel 插件开发者手册以及不同项目场景下的 Babel 最佳实践心得。

如果你对 Babel 还不是很了解,强烈建议优先阅读上述三篇文章。本文更多的是针对于 @babel/plugin-transform-runtime 各项配置的解释补充。

@babel/runtime

什么是 @babel/runtime

在开始 @babel/plugin-transform-runtime 的讲述前,我们先来看看 @babel/runtime 是什么东西。

@babel/runtime is a library that contains Babel modular runtime helpers.

可以看到官方文档对于 @babel/runtime 的介绍非常简单: 一个包含 Babel 模块化运行时助手的库。

那么,怎么理解这个所谓的运行时呢?此时就要拉出来另一个概念:@babel/preset-env

简单来说 @babel/preset-env 是一系列 babel-plugin 的预设集合,默认情况下它允许我们在代码中使用一系列高版本 ECMAScript 语法,比如:

代码语言:javascript复制
// Source Code
const a = () => {
  // dosomething
};
// compile core
"use strict";

var a = function a() {
    // dosomething
};

当然,preset-env 也提供了 polyfill 等可选配置,稍后我们会详细讨论它。

那么它和 @babel/runtime 有什么关系呢,我们来看看另一段代码:

代码语言:javascript复制
// source code
class a {}
// compile by preset-env
"use strict";

function _defineProperties(target, props) { for (var i = 0; i < props.length; i  ) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } }

function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); Object.defineProperty(Constructor, "prototype", { writable: false }); return Constructor; }

function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }

var a = /*#__PURE__*/_createClass(function a() {
  _classCallCheck(this, a);
});

上述代码我们可以看到,当我们在代码中使用 class 语法时。使用 babel-preset-env 进行处理时,会为文件内部注入一系列判断代码来实现 class 的低版本兼容性效果。

所谓 @babel/runtime 正是为了解决这个问题出现的,

@babel/runtime 针对于代码中这些重复注入的辅助语句可以达到运行时引入的效果,从而缩小代码体积。比如:

代码语言:javascript复制
// source code
class a {}
// compile code by @babel/runtime
"use strict";

var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");

var _createClass2 = _interopRequireDefault(require("@babel/runtime/helpers/createClass"));

var _classCallCheck2 = _interopRequireDefault(require("@babel/runtime/helpers/classCallCheck"));

var a = /*#__PURE__*/(0, _createClass2["default"])(function a() {
  (0, _classCallCheck2["default"])(this, a);
});

我们可以清晰的看到原本通过 preset-env 对于 class 的注入的辅助代码变成了运行时的模块引入的代码了。

这也就是 @babel/runtime 的作用: 将转译的辅助代码从文件中硬编码方式变为运行时的模块注入,从而(在某些条件下,比如重复代码过多时)缩小编译后的代码体积。

误区

@babel/runtime 并不包含任何 polyfill 的注入

首先 @babel/runtime 仅仅是一个包含运行时 helpers 的库,它会将 Babel 编译输出中的跨文件重复辅助代码变为运行时引入,仅此而已。

代码语言:javascript复制
// source code
const promise = () => new Promise();

const map = new Map();
// compile code with babel/runtime
"use strict";

var promise = function promise() {
  return new Promise();
};

var map = new Map();

可以看到代码中的 promisemap 没有任何变化,@babel/runtime 库并不会帮我们注入任何 polyfill 代码。

@babel/runtime@babel/preset-env 的关系

@babel/runtime 是一个运行时的模块化库,当我们使用 @babel/preset-env 转译代码时。

如果我们使用了 @babel/runtime 的话,针对于重复的硬编码 helper 方法会变为模块化的方式在运行时引入。

反之,则亦然。

换句话说,如果你仅仅使用 @babel/runtime 的话并不使用 preset-env,那么其实是没有任何效果的。

代码语言:javascript复制
// source code
class a {
  // do something
}
// compile code with @babel/runtime without @babel/preset-env
class a {
    // do something
}

你可以将 @babel/runtime 理解成为 @babel/preset-env 的扩展工具库,虽然这只是针对 ECMA 语法部分的转译来说。

如何开启 @babel/runtime

这个问题其实并不是什么误区,其实它非常简单。

我们会在稍后提到它。

@babel/transform-runtime

接下来我们来聊一聊 @babel/transform-runtime

基础

相信了解过 babel 的小伙伴,或多或少都听过一句话。

类库项目的构建如果需要注入 polyfill 的话,最好使用 @babel/transform-runtime**,因为它提供了一种不污染全局作用域的方式**。

而业务项目中最好使用 preset-env useBuintIns 配置来注入 polyfill**,这种方式会污染全局作用域。**

如果你对上述几句话不是特别清楚的话,推荐你去详细阅读下我的这篇 「前端基建」探索不同项目场景下Babel最佳实践方案,我就不在这里详细展开了。

所谓 @babel/plugin-transform-runtime 插件主要为我们提供了以下三个功能:

  • 首当其冲的一定是当我们需要一种不污染全局环境的 polyfill 时,我们可以通过 @babel/plugin-transform-runtime 来帮我们提供,这对于类库的打包起到了非常 Nice 的作用(通过 corejs 选项开启)。
  • 其次,它提供了自动删除内敛 Babel helper 并使用 @babel/runtime/helpers 来进行运行时注入(可使用 helpers 选项切换)。
  • 最后,当我们在代码中使用 generators/async 函数时,它会自动根据 @babel/runtime/regenerator进行运行时注入(可通过 regenerator 选项切换)。

接下来,就让我们深入 @babel/plugin-transform-runtime 插件来一探究竟。

配置项解读

corejs

讲解

所谓的 corejs 正是 @babel/plugin-transform-runtime 来实现 @polyfill 的核心库。

简单点来说,只有指定了 corejs 版本的话,@babel/plugin-transform-runtime 才会根据指定的 corejs 版本对于我们的源代码动态添加 polyfill

默认值为 false 这表示默认不需要注入任何 polyfill

同时,**@babel/plugin-transform-runtime** 中提供的 core-js 库是一种不污染全局变量方式的 polyfill 方式注入。

我们来看看 runtime-corejs3 中的文件内容:

可以看到这里边会包含非常多的 ECMAScript 新版内置模块(Promise、Map 等)、静态方法(Arrar.from、Array.of)以及一些实例方法(instance 文件夹中)。

当我们这样使用时:

代码语言:javascript复制
// config file
module.exports = (api) => {
  api.cache.never();
  return {
    plugins: [
      [
        '@babel/plugin-transform-runtime',
        {
          corejs: 3, // 使用 corejs 3版本
        },
      ],
    ],
  };
};

// source code
const promise = new Promise();
// compile code
import _Promise from "@babel/runtime-corejs3/core-js-stable/promise";
const promise = new _Promise();

我们可以清楚的看到当我们使用 corejs:3 时针对于代码中的 promise 会增加一层 polyfill 垫片的作用。

同时它会从 @babel/runtime-corejs3/core-js-stable/promise 引入 Promise 同时返回给 _Promise 内部变量使用,并不会污染全局作用域。

关于 corejs 存在以下的版本:

corejs选项

安装命令

false

npm install --save @babel/runtime

2

npm install --save @babel/runtime-corejs2

3

npm install --save @babel/runtime-corejs3

  • 当为 false 时,表示仅仅包含 @babel/runtime@babel/runtime 的作用我们刚刚已经详细讲过了。(注意 false 配置并不代表开启 @babel/runtime
  • corejs 2 版本,目前已经不再维护了,所以这里不推荐大家使用。
  • corejs 3 版本,是目前最常用的版本,对比 2 版本。3 版本会额外增加一些实例方法的 polyfill 比如 "foobar".includes("foo") 中的 includes 方法 2 中是不存在的。

所以说,所谓的 corejs 配置就是针对于运行时的 polyfill 配置。如果开启了 corejs(不为false) 那么 @babel/plugin-transform-runtime 会在代码运行时为我们动态注入 polyfill 内容。

误区

需要特别留意的是 @babel/plugin-transform-runtimecorejs 选项和 preset-envcorejs 配置看起来虽然是相似的。

但是他们引入的包内容是完全不同的,**preset-env 中的 corejs 配置依赖的是 core-js(大版本 2 or 打版本 3) 这个包,这个包中的 polyfill 会污染全局作用域。**

@babel/plugin-transform-runtime 中的 corejs 选项依赖的是 runtime-corejs3/runtime-corejs2 这两个包,这两个包内提供的则是一种不污染全局作用域的 polyfill 方式。

当然,@babel/plugin-transform-runtime 的 corejs 配置默认为 false,而当使用 preset-env 设置 useBuintIns: usage or entry 时,corejs 默认为 2。

helpers

接下来的 helpers 配置就会比较简单了。

因为我们在上述说过正常情况下 preset-env 会将一些多余的语法转椅硬编码编译在源代码文件中,而我们可以利用 @babel/runtime 将重复的语法做成运行时的注入。

那么,如果开启 @babel/runtime 呢? 当然是使用 helpers 属性,它的默认值是 true

当我们开启 helpers: true 时,结合 preset-env 选项。它会将我们一些重复的转译语法变成运行时注入。比如:

代码语言:javascript复制
// configfile
module.exports = (api) => {
  api.cache.never();
  return {
    presets: ['@babel/preset-env'],
    plugins: [
      [
        '@babel/plugin-transform-runtime',
        {
          corejs: false,
        },
      ],
    ],
  };
};

// source code 
class a {}
// compile code
"use strict";

var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");

var _createClass2 = _interopRequireDefault(require("@babel/runtime/helpers/createClass"));

var _classCallCheck2 = _interopRequireDefault(require("@babel/runtime/helpers/classCallCheck"));

var a = /*#__PURE__*/(0, _createClass2["default"])(function a() {
  (0, _classCallCheck2["default"])(this, a);
});

可以看到针对于编译 class 时重复的语法全部变成了运行时注入。

需要留意的是如果你使用 corejs:false 的话 helpers 模块会从 @babel/runtime/helpers 中引入。

而如果你使用了 corejs 那么对应的 helpers 会从对于的 corejs 包中引入,比如假如你使用了 core-js:3 针对于 class 的引入:@babel/runtime-corejs3corejs:2 同理。

不过这些对于使用者来说是无关紧要的,也许你永远不用关心它从哪里引入。只要当你使用对应 corejs 版本时记得安装对应的包即可。

regenerator

讲解

regenerator 配置的值默认为 true,它代表当我们在代码中使用 async/await 或者 generator 函数时,切换是否从全局作用域中获取。

默认为 true,表示生成运行时的 async/awaitgenerator 模块注入并不从全局作用域获取。

怎么理解这个从全局作用域中获取呢?我们稍微来看看当我将它设置 false 时的编译结果:

代码语言:javascript复制
// source Code
function* sayHello() {}
// compile Code 
"use strict";

var _interopRequireDefault = require("@babel/runtime-corejs3/helpers/interopRequireDefault");

var _asyncToGenerator2 = _interopRequireDefault(require("@babel/runtime-corejs3/helpers/asyncToGenerator"));

function sayHello() {
  return _sayHello.apply(this, arguments);
}

function _sayHello() {
  _sayHello = (0, _asyncToGenerator2["default"])( /*#__PURE__*/regeneratorRuntime.mark(function _callee() {
    return regeneratorRuntime.wrap(function _callee$(_context) {
      while (1) {
        switch (_context.prev = _context.next) {
          case 0:
          case "end":
            return _context.stop();
        }
      }
    }, _callee);
  }));
  return _sayHello.apply(this, arguments);
}

上述编译后的代码引用了两个 helpers ,不过这无关紧要。

细心的小伙伴可能会发现了,编译后的 generator 函数依赖了全局的 regeneratorRuntime 这个对象。

这也就意味着当 regenerator: false 时针对于 async/generator 函数的转译 babel 需要依赖于全局作用域的 regeneratorRuntime 这个对象。

反之,再来看看相同的代码当我们设置为 regenerator: true 时它的体现:

代码语言:javascript复制
"use strict";

var _interopRequireDefault = require("@babel/runtime-corejs3/helpers/interopRequireDefault");

// 重点在这里
var _regenerator = _interopRequireDefault(require("@babel/runtime-corejs3/regenerator"));

var _asyncToGenerator2 = _interopRequireDefault(require("@babel/runtime-corejs3/helpers/asyncToGenerator"));

function sayHello() {
  return _sayHello.apply(this, arguments);
}

function _sayHello() {
  _sayHello = (0, _asyncToGenerator2["default"])( /*#__PURE__*/_regenerator["default"].mark(function _callee() {
    return _regenerator["default"].wrap(function _callee$(_context) {
      while (1) {
        switch (_context.prev = _context.next) {
          case 0:
          case "end":
            return _context.stop();
        }
      }
    }, _callee);
  }));
  return _sayHello.apply(this, arguments);
}

相同的源代码,我们可以清晰的看到当设置 regenerator: true 时明显生成函数的 polyfill 已经不依赖于全局作用域了。

而是会在运行时从模块导入,这也就意味着 regenerator: true 可以让我们不依赖于全局污染的生成器模块来使用 async/awiat 或者 generator 模块。

注意点

If you are compiling generators or async function to ES5, and you are using a version of @babel/core or @babel/plugin-transform-regenerator older than 7.18.0, you must also load the regenerator runtime package. It is automatically loaded when using @babel/preset-env's useBuiltIns: "usage" option or @babel/plugin-transform-runtime.

这是官方文档在 @babel/polyfill 中的描述。在旧版本的 babel 中默认是不会将 generator 的帮助模块(也就是全局的 regenerator 对象注入)。

所以当我们在使用低版本的 @babel/core 或者 @babel/plugin-transform-regenerator 时,如果需要依赖全局作用域的 regenerator 对象时,需要额外在项目中引入 regenerator-runtime 这个库。

当然在 7.18.0 全局的 regenerator 已经变成了类似于 helpers 模块的注入了,当我们在项目中使用到 async/await/generator 时,preset-env 会自动帮我们注入对应的全局 regenerator 函数声明。

这也许就是有时你会碰到的 regeneratorRuntime.mark is not a function,当然,我更推荐你使用新版本,它会避免这个问题。详情你可以参考这个Issue

需要额外注意的是,当你使用 @babel/plugin-transform-regenerator 时,是否需要注入全局的 regenerator 是依据 helpers regenerator 的。

我说点人话,针对于新版本(7.18.0)以上如果我的代码是这么写的:

代码语言:javascript复制
function* sayhello() {}

配置文件是这样的:

代码语言:javascript复制
module.exports = (api) => {
  api.cache.never();
  return {
    presets: ['@babel/preset-env']
  };
};

它会生成:

代码语言:javascript复制
"use strict";

function _typeof(obj) { "@babel/helpers - typeof"; return _typeof = "function" == typeof Symbol &amp;&amp; "symbol" == typeof Symbol.iterator ? function (obj) { return typeof obj; } : function (obj) { return obj &amp;&amp; "function" == typeof Symbol &amp;&amp; obj.constructor === Symbol &amp;&amp; obj !== Symbol.prototype ? "symbol" : typeof obj; }, _typeof(obj); }

function _regeneratorRuntime() { 
    // 省略无数个代码
}

var _marked = /*#__PURE__*/_regeneratorRuntime().mark(sayhello);

function sayhello() {
  return _regeneratorRuntime().wrap(function sayhello$(_context) {
    while (1) {
      switch (_context.prev = _context.next) {
        case 0:
        case "end":
          return _context.stop();
      }
    }
  }, _marked);
}

可以看到,当我单独使用 preset-env 时代码中使用到了 generator 函数,它会自动注入对应的 _regeneratorRuntime 函数。

之后,同一段源代码当我这样配置:

代码语言:javascript复制
module.exports = (api) => {
  api.cache.never();
  return {
    presets: ['@babel/preset-env'],
    plugins: [
      [
        '@babel/plugin-transform-runtime',
        {
          helpers: true, // 其实默认就是 true
          regenerator: true,
        },
      ],
    ],
  };
};

编译后的代码:

代码语言:javascript复制
"use strict";

var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");

var _regenerator = _interopRequireDefault(require("@babel/runtime/regenerator"));

var _marked = /*#__PURE__*/_regenerator["default"].mark(sayhello);

function sayhello() {
  return _regenerator["default"].wrap(function sayhello$(_context) {
    while (1) {
      switch (_context.prev = _context.next) {
        case 0:
        case "end":
          return _context.stop();
      }
    }
  }, _marked);
}

不出意外 _regenerator 变成了 runtime 的模块注入,并不依赖全局作用域。

当我再次修改配置文件:

代码语言:javascript复制
module.exports = (api) => {
  api.cache.never();
  return {
    presets: ['@babel/preset-env'],
    plugins: [
      [
        '@babel/plugin-transform-runtime',
        {
          helpers: true, // 其实默认就是 true
          regenerator: false,
        },
      ],
    ],
  };
};

编译后的代码:

代码语言:javascript复制
"use strict";

var _marked = /*#__PURE__*/regeneratorRuntime.mark(sayhello);

function sayhello() {
  return regeneratorRuntime.wrap(function sayhello$(_context) {
    while (1) {
      switch (_context.prev = _context.next) {
        case 0:
        case "end":
          return _context.stop();
      }
    }
  }, _marked);
}

可以看到,当我关闭 regenerator 同时开启 helpers:true 时,并不会注入任何 regeneratorRuntime 相关的全局 helpers 注入。

这是因为我们设置了 regenerator: false 表示依赖全局的 regenerator ,同时我们使用了 helpers: true 表示所有 helpers 需要 runtime 模块注入。

那么,当然对于 regenerator 并不会生成对于的硬编码 _regeneratorRuntime 注入了。

再来,当我再次修改配置文件后:

代码语言:javascript复制
module.exports = (api) => {
  api.cache.never();
  return {
    presets: ['@babel/preset-env'],
    plugins: [
      [
        '@babel/plugin-transform-runtime',
        {
          helpers: false, // 其实默认就是 true
          regenerator: false,
        },
      ],
    ],
  };
};

同样的源代码,编译后为:

代码语言:javascript复制
"use strict";

function _typeof(obj) { "@babel/helpers - typeof"; return _typeof = "function" == typeof Symbol &amp;&amp; "symbol" == typeof Symbol.iterator ? function (obj) { return typeof obj; } : function (obj) { return obj &amp;&amp; "function" == typeof Symbol &amp;&amp; obj.constructor === Symbol &amp;&amp; obj !== Symbol.prototype ? "symbol" : typeof obj; }, _typeof(obj); }

function _regeneratorRuntime() {
    // ... 
}

var _marked = /*#__PURE__*/_regeneratorRuntime().mark(sayhello);

function sayhello() {
  return _regeneratorRuntime().wrap(function sayhello$(_context) {
    while (1) {
      switch (_context.prev = _context.next) {
        case 0:
        case "end":
          return _context.stop();
      }
    }
  }, _marked);
}

可以看到此时会硬编码 _regeneratorRuntime 全局函数,会污染全局作用域。

稍微总结一下上边的体现:

  • 不使用 @babel/plugin-transform-runtime,单独使用 preset-env 时。针对于 async/await/generator 会在代码中硬编码 _regeneratorRuntime 污染全局作用域。
  • preset-env 配合 @babel/plugin-transform-runtime
代码语言:txt复制
- `helpers: false &amp;&amp; regenerator: false` 生成 `_regeneratorRuntime` 污染全局作用域。
- `helpers: true &amp;&amp; regenerator: false`  不生成 `_regeneratorRuntime` 污染全局作用域,但是需要依赖全局的 `_regeneratorRuntime`,否则会报错。
- `helpers: true &amp;&amp; regenerator: true` (插件默认值),生成运行时的模块注入不依赖全局作用域,同时也不污染全局作用域。
- `helpers: false &amp;&amp; regenerator: true` :生成 `_regeneratorRuntime` 污染全局作用域,和第一种情况一致。

只要 helpers: false 其实无论你的 regenerator 如何设置都是无效的,都会交给 preset-env 来处理你的 async/await/generator 函数从而生成污染全局作用域的硬编码。

helpers: true 时,才会根据 regenerator 来决定后续行为。如果 regenerator:true 那么生成 runtime 不依赖全局同时不污染作用域的 regeneratorRuntime

反之,设置 regenerator: false 时,需要依赖全局的 regeneratorRuntime 这也就意味着需要额外依赖 regenerator runtime这个库。

useESModules

用法

之后我们再来看看所谓的 useESModules 配置。

当设置 useESModules: true 时,当使用 @babel/plugin-transfrom-runtime 转译代码时,会启动 @babel/plugin-transform-modules-commonjs 插件将注入的 helpers 模块转化为 CJS 模块导出语句。

简单点来说 useESModules: true 表示注入的 helpers 模块为 ESM 导出,而设置为 false 时表示使用 CJS 导出。

我们来看一个 Demo 就一目了然:

代码语言:javascript复制
// source code
class Hello {}

// plugin config 
module.exports = (api) => {
  api.cache.never();
  return {
    presets: ['@babel/preset-env'],
    plugins: [
      [
        '@babel/plugin-transform-runtime',
        {
          helpers: true, 
          useESModules: false,
        },
      ],
    ],
  };
};

// compile code
"use strict";

var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
// 从 esm 中导入
var _createClass2 = _interopRequireDefault(require("@babel/runtime/helpers/esm/createClass"));

var _classCallCheck2 = _interopRequireDefault(require("@babel/runtime/helpers/esm/classCallCheck"));

var Hello = /*#__PURE__*/(0, _createClass2["default"])(function Hello() {
  (0, _classCallCheck2["default"])(this, Hello);
});

可以看到上述的 helpers 引入变成了 @babel/runtime 中的 esm 这个模块。

同样的源代码,当我们设置为 useESModules: false 时:

代码语言:javascript复制
// compile code
"use strict";

var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
// 从正常的 CJS 包中导入模块
var _createClass2 = _interopRequireDefault(require("@babel/runtime/helpers/createClass"));

var _classCallCheck2 = _interopRequireDefault(require("@babel/runtime/helpers/classCallCheck"));

var Hello = /*#__PURE__*/(0, _createClass2["default"])(function Hello() {
  (0, _classCallCheck2["default"])(this, Hello);
});

比如,classCallCheck 在开启 useESModules 的表现下分别为:

代码语言:javascript复制
// useESModules: false
exports.__esModule = true;

exports.default = function (instance, Constructor) {
  if (!(instance instanceof Constructor)) {
    throw new TypeError('Cannot call a class as a function');
  }
};
代码语言:javascript复制
// useESModules: true
export default function (instance, Constructor) {
  if (!(instance instanceof Constructor)) {
    throw new TypeError('Cannot call a class as a function');
  }
}

通常在前端项目中,我们都会利用一些构建工具,比如 webpack/rollup 之类。

合理的利用 useESModules 配置会让构建出更小的 bundler ,原因也很简单因为它不需要保留任何 commonjs 语义的代码。

注意点

This option has been deprecated: starting from version 7.13.0@babel/runtime's package.json uses "exports" option to automatically choose between CJS and ESM helpers.

当然这个配置项在 7.13.0 之后被废弃掉了,我们可以看到了针对于 7.13.0 之后 @babel/runtime 这个包会根据 package.json 中的 exports 字段来决定以何种模块规范自动导出。

如果你不是特别了解 exports 关键字的话可以查看这篇 从 package.json 来聊聊如何管理一款优秀的 Npm 包。

其次,useESModules 配置仅仅针对于引入的 helpers 函数有效,反而言之如果你设置了 helpers: false 那么自然 useESModules 是完全没有任何效果的。

absoluteRuntime

absoluteRuntime 接受一个 string 或者 boolean 的值,默认为 false。它可以让我们更加自由的使用 transform-runtime

默认情况下,transform-runtime 会从当前项目的 node_mouldes 文件夹中寻找 @babel/runtime 模块从而引入对应的 helpers 模块。

代码语言:javascript复制
var _createClass2 = _interopRequireDefault(require("@babel/runtime/helpers/createClass"));

这也就意味着,当前项目中的 node_modules 中必须存在对应的 @babel/runtime 这个包。

在某些情况下,比如 monorepo 项目、npm link 的包或者一些用户外部调用的 CLI 使用默认的 helpers 查找规则是会产生问题的(寻找不到对应的 @babel/runtime 模块)。

absoluteRuntime 就是为了解决上述问题而产生的配置,它允许我们在 Babel 开始编译前预先解析一次 runtime 所在位置,从而将指定的绝对路径拼接到输出的代码之前。

当我们设置 absoluteRuntime: true 时,我们在来看看编译后的引入模块:

代码语言:javascript复制
// 这里为使用了 pnpm ,所以扫描到的 runtime 目录是我磁盘目录上的绝对路径地址
var _createClass2 = _interopRequireDefault(require("/Users/wanghaoyu/Desktop/babel/node_modules/.pnpm/@babel runtime@7.19.0/node_modules/@babel/runtime/helpers/createClass.js"));

需要额外留意的是,如果你的项目中编译后依赖了 @babel/runtime 对应的包(简单来说并没有将 runtime 编译进入而是作为 dependency),那么对于编译后的绝对路径是不可取的。

version

接下来聊聊 version 配置项,这是一个非常简单的配置项。

默认情况下,transform-runtime 会认为 @babel/runtime@7.0.0 已经安装了,当我们安装了更高版本的 @babel/runtime (或者对应的 corejs 版本)时,此时我们可以通过 version 字段来制定对应 runtime 的版本从而使用更多先进的 feature。

比如,当我们依赖 @babel/runtime-corejs2@7.7.4 编译我们的代码时:

代码语言:javascript复制
{
  "plugins": [
    [
      "@babel/plugin-transform-runtime",
      {
        "absoluteRuntime": false,
        "corejs": 2,
        "version": "^7.7.4"
      }
    ]
  ]
}

对比 preset-env polyfill 方案

上述我们详细聊了聊 @babel/plugin-transform-runtime 的各个常用配置项的含义,通过 corejs 配置我们明白 @babel/plugin-transform-runtime 可以为我们的代码添加 polyfill 。

那么,在 preset-env 中的 useBuiltIns 也可以为我们的项目添加 polyfill 支持,接下里我们就聊聊这两者有什么区别。

作用域范围

首先,老生常谈 @babel/plugin-transform-runtime 编译后添加的 polyfill 并不会污染全局作用域,而 preset-env 并不会污染全局作用域。

比如同一段代码 const promise = new Promise(),我们先来看看 @babel/plugin-transform-runtime

代码语言:javascript复制
// by @babel/plugin-transform-runtime 
var _interopRequireDefault = require("@babel/runtime-corejs3/helpers/interopRequireDefault");

var _promise = _interopRequireDefault(require("@babel/runtime-corejs3/core-js-stable/promise"));

var promise = new _promise["default"]();

可以看到 @babel/plugin-transform-runtime 编译后的 promise 是作为局部变量 _promise 引入的,这也就意味着它并不会污染全局作用域

我们再来看看通过 preset-envuseBuiltIns: usage 编译后的结果:

代码语言:javascript复制
"use strict";

require("core-js/modules/es6.object.to-string.js");

require("core-js/modules/es6.promise.js");

var promise = new Promise();

上述代码中的 Promise 的确添加了 polyfill 但是明显可以看到这是一种污染全局作用域的作用,会为全局添加 Promise

浏览器 Target

当我们使用 preset-env 时,它支持一个额外的配置名为 targets 配置,它表示源代码需要兼容的浏览器列表。

比如一个 const 语法,在新版 Chrome 中已经支持了 const 语法,此时如果我们将目标浏览器设置为 chrome: 101 那么 const 就不需要进行转译,这可以大大减小编译后的代码体积。

同样对于 polyfill 的添加也是如此,我们同样以一段 Promise 源代码来一探究竟:

代码语言:javascript复制
const promise = new Promise()

当我们设置如下配置时:

代码语言:javascript复制
// config file 开启 usage ,关闭 plugin-tansform-runtime 
module.exports = (api) => {
  api.cache.never();
  return {
    presets: [
      [
        '@babel/preset-env',
        {
          useBuiltIns: 'usage',
          // 指定目标浏览器为 chrome 101
          targets: {
            chrome: 101,
          },
        },
      ],
    ],
    plugins: [
      [
        '@babel/plugin-transform-runtime',
        {
          helpers: true, // 其实默认就是 true
          corejs: false,
        },
      ],
    ],
  };
};

// compiled code
const promise = new Promise();

因为 chrome 101 版本中已经内置了 Promise ,所以当我们支持目标浏览器为 Chrome 101 时自然并不需要做任何处理。

同样,当我们关闭 preset-env 使用 @babel/plugin-transform-runtime 来添加 polyfill 看看:

代码语言:javascript复制
// config file
module.exports = (api) => {
  api.cache.never();
  return {
    presets: [
      [
        '@babel/preset-env',
        {
          useBuiltIns: false,
          targets: {
            chrome: 101,
          },
        },
      ],
    ],
    plugins: [
      [
        '@babel/plugin-transform-runtime',
        {
          helpers: true, // 其实默认就是 true
          corejs: 3,
        },
      ],
    ],
  };
};
// compiled code
var _interopRequireDefault = require("@babel/runtime-corejs3/helpers/interopRequireDefault");

var _promise = _interopRequireDefault(require("@babel/runtime-corejs3/core-js-stable/promise"));

const promise = new _promise.default();

结果已经一目了然了,**preset-env** polyfill 方案支持通过 targets 动态调整 polyfill 的支持从而缩小编译后的代码体积,而 @babel/plugin-transform-runtime 并不支持 targets 设置会全量引入 polyfill。

总结

关于 @babel/plugin-transform-runtimepreset-env 的 polyfill 方案在我个人看来并没有任何绝对的好坏,不同的业务场景下这两种方案都会有不一样的效果,具体取舍更多的还是仁者见仁,智者见智。

当然,目前大多数成熟的组件库细心的小伙伴其实已经发现并不会使用 babel 添加任何 polyfill 支持,而是将 polyfill 步骤留给用户自己抉择。

比如 ant-design 官网中明确标名的:

关于 preset-env@babel/plugin-transform-runtime 的不同业务场景下 polyfill 的抉择,有兴趣了解的小伙伴可以参考这篇 「前端基建」探索不同项目场景下Babel最佳实践方案 。

借题聊聊 SWC

SWC 是一个基于 Rust 编写的可拓展的平台,适用于下一代快速开发工具。

SWC 可以被用作编译和打包,所谓的编译就类似于 Babel 的功能(将高版本 JS/TS 代码编译为主流浏览器皆支持的低版本语法)。

虽然官方表示 SWC 提供了 swcpack 支持 Bundling(打包) 功能,但是笔者目前感觉仍然不是特别成熟。目前社区中对于 swcpack 的热度也一直不温不火,有兴趣的同学可以自行尝试。

当然,为什么要额外提一句 SWC 。这里并不打算详细展开它,更多的在提到代码转译时目前看来 SWC 相较于代码转译虽然没有 Babel 那么成熟但是已经可以满足绝大多数需求了(目前 Next.js, Parcel, and Deno 等等已经投入使用了)。

相较于进行代码编译,如果你是一个没有任何历史包袱的新项目 SWC 倒是一个不错的选择。当项目越来越庞大时代码打包过程中绝大多数耗时其实正是发生在转译过程中,而 SWC 可以带给你的证实飞一般的速度。

结尾

关于 @babel/plugin-transform-runtime 的分享在这里就和大家告一段落了。

当然,如果小伙伴们对于文中有任何疑问也可以在评论区留下你的想法。

之后如果有机会的话我会继续这个专题和大家分享一些实用的 Babel-Plugin 的编写以及 SWC 方向的拓展话题。

0 人点赞