写在前边
大家好,今天来和大家聊聊 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 语法,比如:
// Source Code
const a = () => {
// dosomething
};
// compile core
"use strict";
var a = function a() {
// dosomething
};
当然,
preset-env
也提供了polyfill
等可选配置,稍后我们会详细讨论它。
那么它和 @babel/runtime
有什么关系呢,我们来看看另一段代码:
// 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
针对于代码中这些重复注入的辅助语句可以达到运行时引入的效果,从而缩小代码体积。比如:
// 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
编译输出中的跨文件重复辅助代码变为运行时引入,仅此而已。
// 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();
可以看到代码中的 promise
和 map
没有任何变化,@babel/runtime 库并不会帮我们注入任何 polyfill 代码。
@babel/runtime
和 @babel/preset-env
的关系
@babel/runtime
是一个运行时的模块化库,当我们使用 @babel/preset-env
转译代码时。
如果我们使用了 @babel/runtime
的话,针对于重复的硬编码 helper 方法会变为模块化的方式在运行时引入。
反之,则亦然。
换句话说,如果你仅仅使用 @babel/runtime
的话并不使用 preset-env
,那么其实是没有任何效果的。
// 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-runtime
的 corejs
选项和 preset-env
的 corejs
配置看起来虽然是相似的。
但是他们引入的包内容是完全不同的,**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
选项。它会将我们一些重复的转译语法变成运行时注入。比如:
// 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-corejs3
,corejs:2
同理。
不过这些对于使用者来说是无关紧要的,也许你永远不用关心它从哪里引入。只要当你使用对应 corejs 版本时记得安装对应的包即可。
regenerator
讲解
regenerator
配置的值默认为 true
,它代表当我们在代码中使用 async/await
或者 generator
函数时,切换是否从全局作用域中获取。
默认为 true,表示生成运行时的 async/await
、generator
模块注入并不从全局作用域获取。
怎么理解这个从全局作用域中获取呢?我们稍微来看看当我将它设置 false
时的编译结果:
// 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
时它的体现:
"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 than7.18.0
, you must also load theregenerator runtime
package. It is automatically loaded when using@babel/preset-env
'suseBuiltIns: "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 && "symbol" == typeof Symbol.iterator ? function (obj) { return typeof obj; } : function (obj) { return obj && "function" == typeof Symbol && obj.constructor === Symbol && 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 && "symbol" == typeof Symbol.iterator ? function (obj) { return typeof obj; } : function (obj) { return obj && "function" == typeof Symbol && obj.constructor === Symbol && 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
:
- `helpers: false && regenerator: false` 生成 `_regeneratorRuntime` 污染全局作用域。
- `helpers: true && regenerator: false` 不生成 `_regeneratorRuntime` 污染全局作用域,但是需要依赖全局的 `_regeneratorRuntime`,否则会报错。
- `helpers: true && regenerator: true` (插件默认值),生成运行时的模块注入不依赖全局作用域,同时也不污染全局作用域。
- `helpers: false && 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
时:
// 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
的表现下分别为:
// 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
'spackage.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
模块。
var _createClass2 = _interopRequireDefault(require("@babel/runtime/helpers/createClass"));
这也就意味着,当前项目中的 node_modules
中必须存在对应的 @babel/runtime
这个包。
在某些情况下,比如 monorepo 项目、npm link 的包或者一些用户外部调用的 CLI 使用默认的 helpers
查找规则是会产生问题的(寻找不到对应的 @babel/runtime
模块)。
absoluteRuntime
就是为了解决上述问题而产生的配置,它允许我们在 Babel 开始编译前预先解析一次 runtime
所在位置,从而将指定的绝对路径拼接到输出的代码之前。
当我们设置 absoluteRuntime: true
时,我们在来看看编译后的引入模块:
// 这里为使用了 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
编译我们的代码时:
{
"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
// 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-env
的 useBuiltIns: usage
编译后的结果:
"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 源代码来一探究竟:
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
看看:
// 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-runtime
和 preset-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 方向的拓展话题。