背景
最早有 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
的主要作用
-
自动语法转换(Transpilation):
@babel/preset-env
根据你指定的目标环境(如特定版本的浏览器或 Node.js),自动选择需要转换的 JavaScript 语法特性。例如,将箭头函数、类、模块等 ES6+ 语法转换为旧环境支持的语法。 -
自动 Polyfill 引入: 除了语法转换,
@babel/preset-env
还可以根据目标环境的支持情况,自动引入必要的 Polyfill(通过core-js
),以填补那些旧环境中缺失的内置对象和方法,如Promise
、Array.prototype.includes
、Object.assign
等。 -
优化打包体积: 通过智能地选择需要转换和引入的 Polyfill,
@babel/preset-env
可以帮助你减少最终打包后的代码体积,提升应用的加载速度和性能。
什么是 @babel/plugin-transform-runtime?
@babel/plugin-transform-runtime
是 Babel 提供的一个插件,旨在优化 JavaScript 代码的转译过程,减少代码体积,避免全局污染,并改善代码的可维护性。它通过引用 Babel 运行时(@babel/runtime
)中的辅助函数和 Polyfill,替代在每个模块中重复生成相同的辅助代码,从而提高构建效率和运行时性能。
主要功能
-
辅助函数的复用:
-
问题:在没有使用该插件的情况下,Babel 在每个需要转换的文件中都会插入相同的辅助函数(如
_extends
、_asyncToGenerator
等),导致生成的代码体积增大。 -
解决方案:
@babel/plugin-transform-runtime
将这些辅助函数提取到@babel/runtime
包中,通过引用的方式复用这些函数,避免在每个文件中重复生成相同的代码。
-
-
Polyfill 管理:
-
问题:直接在全局环境中注入 Polyfill(如通过
@babel/preset-env
的useBuiltIns: "entry"
或"usage"
)可能导致全局命名空间污染,尤其是在库开发中会引发兼容性问题。 -
解决方案:该插件通过引用
@babel/runtime
中的 Polyfill,避免将 Polyfill 注入到全局环境中。这不仅减少了全局污染,还提升了代码的模块化和复用性。
-
-
支持生成器和
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?
外国人起名字那是相当用心的,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-js
和 regenerator-runtime
的整合
Babel 社区和核心团队决定将 Polyfill 的管理和引入分离,提供更灵活和高效的解决方案。因此,babel/polyfill
被正式弃用,推荐使用 core-js
和 regenerator-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 配置是有效的
所以,针对于 node,electron,chrome 都是起到转义作用的
下面这种 node:"16", electron:"22",chrome:"108" 这几个预设,使得"userBuiltIns":"usage",corejs":3 都失去作用,因此我 package.json 不安装 core-js
和 regenerator-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
}
]
]
}
},
综上
-
Chrome 扩展开发都是基于高版本的谷歌内核,所有 ES 语法都已经支持了,可以将 Chrome 版本设置高一点,比如 Chrome:"106" 这样插件打出来的包速度很快,且体积也会很小,基本不会做语法糖转义
-
Electron 渲染进程直接设定好 Electron 支持版本,例如我用的 22 版本,或者注入 webview 的 web 打包,则设定 Chrome:"108",都能有效提高打包速度,同时大大降低打包后代码的体积,还能直接试用 Chrome 提供的原生 API 进行开发。