金数据技术博客 · №1
一次 polyfill 对 babel 的思考🤔
一、起因
正在写 bug🐛 的我,突然被叫去看一个问题,页面突然白屏了。于是本能的反应是快速打开控制台,发现 js 有报错,报错信息是 .flat() is not a function,原来是浏览器版本不支持 flat API,做一下 polyfill 就好了。那么我们如何 polyfill 呢?🤔 接下来就来详细分析一下 polyfill 的过程。
二、Babel 的作用
首先在进行 polyfill 之前,我们复习一下 Babel 这个编译器。官方文档
Babel 主要用于将采用 ECMAScript 2015+ 语法编写的代码转换为向后兼容的 JavaScript 语法,以便能够运行在当前和旧版本的浏览器或其他环境中。 作用如下:
- 语法转换
- 通过 Polyfill 方式在目标环境中添加缺失的特性(通过第三方 polyfill 模块,例如 core-js,实现)
- 源码转换 (codemods)
换而言之就是 Babel 能让我们现在就使用新的语法,无需等待浏览器的支持。
看上去很美好,但还是有一些原生 API 方法, Babel 并没用做转换处理,例如:flat、flatMap、includes 等。这时我们就需要为 Babel 编译器做一些配置,也就是 polyfill。
三、@babel/polyfill
正常我们使用 Babel 是不需要安装 @babel/polyfill,但是如果我们需要兼容版本较低的浏览器,就可以使用该库。
@babel/polyfill 它会模拟完整的 ES2015+ 环境用于我们的程序,这样我们使用的 Promise 或 WeakMap 这样的新内置函数,Array.from 或 Object.assign 这样的静态方法,Array.prototype.includes 这样的实例方法等,会被 polyfill 添加到全局范围。
使用方法也相对简单,只需要安装 @babel/polyfill:
yarn add @babel/polyfill然后在我们的入口文件引入:
import "@babel/polyfill"来看一个示例:
我们编写一个 index.js 文件如下:
import '@babel/polyfill'
const a = [1, 2, 3, 4].flat()
const b = [1, 2, 3, 4].flatMap()
const c = [1, 2, 3, 4].includes(1)然后使用 webpack 工具编译,输出后的结果如下:
截图中红色区域就是我们引入的 @babel/polyfill。
@babel/polyfill 的入口文件,实质上是把 ES2015+ 的 polyfill 和一个 runtime 转译生成器函数引入。
这样无疑增加了我们打包的体积,我只使用了 flat、flatMap、includes,却引入了整个 polyfill。
在新的 Babel 版本(7.4.0)中 @babel/polyfill 已经被废弃,不在推荐使用。这里简单了解一下即可。
四、被遗忘的 core-js
这是什么鬼 👻 ,日常我们更多的可能是使用 react 等库进行前端开发,很少会关注底层的一些依赖,了解它对于我们 polyfill 很重要。
core-js 是一个 JavaScript 的模块化标准库。包括 ECMAScript 到 2021 年的 polyfill。 官方文档
core-js 的几个核心概念:
- 它尽可能的进行模块化,让你能选择你需要的功能
- 它可以不污染全局空间
- 它和 babel 高度集成,可以对
core-js的引入进行最大程度的优化
core-js 目前有 2 个版本,2 和 3 ,由于 3 版本进行了一些破坏性变更导致无法兼容 2 版本,现在 2 版本被废弃使用。
core-js@3 的重要改变:
- 对于 ECMAScript 中已经稳定的功能,core-js 已经几乎完全支持,并在
core-js@3中引入了一些新的功能 - 对于一些已经加入到 ES2016-ES2019 中的提案,现在已经被标记为稳定功能
- 增加了 proposals 配置项,对处在提案阶段的 api 提供支持,但是因为提案阶段并不稳定,在正式加入标准之前,可能会有大的改动,需要谨慎使用;对于一些改变巨大的提案,也进行了对应的更新
- 增加了对一些 web 标准的支持,比如 URL 和 URLSearchParams
- 删除了一些过时的特性
由于 core-js@2 版本包的体积太大(~2M),并且有很多重复的文件被引用。core-js@3 对包进行拆分,三个核心的包分别是:
- core-js:定义全局的 polyfill(~500k, 40k minified and gzipped)
- core-js-pure:提供不污染全局环境的 polyfill,等价于 core-js@2/library(~440k)
- core-js-compat:包含了 core-js 模块和 API 必要的数据,通过 browserslist 来生成所需要的 core-js 模块的列表
对于 core-js@3 的入口文件,我们可以这样使用:
// polyfill all `core-js` features:
import "core-js";
// polyfill only stable `core-js` features - ES and web standards:
import "core-js/stable";
// polyfill only stable ES features:
import "core-js/es";
// if you want to polyfill `Set`:
// all `Set`-related features, with ES proposals:
import "core-js/features/set";
// stable required for `Set` ES features and features from web standards
// (DOM collections iterator in this case):
import "core-js/stable/set";
// only stable ES features required for `Set`:
import "core-js/es/set";
// the same without global namespace pollution:
import Set from "core-js-pure/features/set";
import Set from "core-js-pure/stable/set";
import Set from "core-js-pure/es/set";
// if you want to polyfill just required methods:
import "core-js/features/set/intersection";
import "core-js/stable/queue-microtask";
import "core-js/es/array/from";我们还可以使用 core-js-compat 用来提供不用目标引擎所需要的core-js的模块信息:
const {
list, // array of required modules
targets, // object with targets for each module
} = require('core-js-compat')({
targets: '> 2.5%', // browserslist query
filter: 'es.', // optional filter - string-prefix, regexp or list of modules
});
console.log(targets);五、了解 browserslist
我们使用 React 开发,在 package.json 中有这么一段配置:
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
}或在根目录存在一个 .browserslistrc 的配置文件。
那么这个 browserslist 有什么用呢?
browserslist 能在不同前端工具之间共享目标浏览器和 Node.js 版本的配置。
browserslist 使用 caniuse-lite 和 caniuse 进行查询。
那么我们如何知道这个配置都兼容是什么,官方也提供了查询。
例如我们有如下配置:
"browserslist": [
">0.2%",
"not dead",
"not op_mini all"
]然后运行:
npx browserslist输出:
这个配置的大概意思:
>0.2%:拥有至少 0.2% 全球市场份额的所有浏览器not dead:排除过去 24 个月内没有官方支持的浏览器not op_mini all排除 Opera Mini 浏览器
这里不做过多的展开,有兴趣的可以查看。 官网文档
六、了解 @babel/preset-env
前面花了这么多篇幅介绍相关的工具和库,都是为了介绍 @babel/preset-env 做铺垫,@babel/preset-env 可以说是我们做 polyfill 的核心包。
@babel/preset-env 是什么呢?
首先,介绍历史背景,对了解和学习 @babel/preset-env 有帮助。
最初,为了让开发者能够尽早用上新的 ES 特性,babel 团队开发了 babel-preset-latest。这个 preset 比较特殊,它是多个 preset 的集合(es2015+),并且随着 ECMA 规范的更新更增加它的内容。
比如(2018.06.02),它包含的 preset 包括:es2017、es1016、es2015。
到了明年,可能它包含的 preset 就包括:es2020、es2019、es2018、es2017、es2016、es2015。
随着时间的推移,babel-preset-latest 包含的插件越来越多,这带来了如下问题:
- 加载的插件越来越多,编译速度会越来越慢;
- 随着用户浏览器的升级,ECMA 规范的支持逐步完善,编译至低版本规范的必要性在减少(比如ES6 -> ES5),多余的转换不单降低执行效率,还浪费带宽。
因为上述问题的存在,babel 官方推出了 @babel/preset-env 插件。它可以根据开发者的配置,按需加载插件。配置项大致包括:
- 需要支持的平台:比如node、浏览器等。
- 需要支持的平台的版本:比如支持 node@6.1等。
一句话解释:
@babel/preset-env 可以根据配置的目标浏览器或者运行环境来自动将 ES2015+ 的代码转换为 es5。
其实它和 Babel 做的事情有部分是重叠,由于 Babel 还有编译等核心功能,所有对于 ECMA 规范的预设就交给了 @babel/preset-env 专门处理。
@babel/preset-env 与 core-js 和 browserslist 搭配,共同完成 ECMA 语法规范的预设。
接下来到我们的实践环节。
在开始我们的实践之前,先声明在 babel7.4.0 的版本中已经支持 core-js@3 的版本。core-js@3 的出现,也迫使我们放弃 @babel/polyfill (只支持 core-js2)。
polyfill 的第一种方式
先准备环境,安装 babel 核心库和 @babel/preset-env:
yarn add @babel/core @babel/cli @babel/preset-env --save-dev由于我们废弃了 @babel/polyfill,需安装 core-js@3 和 regenerator-runtime
yarn add core-js@3 regenerator-runtime添加 babel.config.json 配置文件:
{
"presets": [
[
"@babel/reset-env",
{
"useBuiltIns": "entry",
"corejs": 3
}
]
]
}在 package.json 中添加 browserslist 配置:
"browserslist": [
"> 0.02%",
"not dead"
]添加我们的测试代码:
// a.js
export function flatFn(array) {
return array.flat()
}
export function flatMap(array) {
return array.flatMap(x => [x * 2])
}
// b.js
function wait(delay) {
return new Promise(resolve => setTimeout(resolve, delay))
}
export default wait
// index.js
import 'core-js/stable'
import 'regenerator-runtime/runtime'
import {flatFn, flatMap} from './a'
import wait from './b'
const array = [1, 2, 3, 4, 5]
flatFn(array)
flatMap(array)
wait().then(r => r)这里需要注意我们在入口 index.js 中,引入了 core-js/stable 和 regenerator-runtime/runtime,因为我们废弃了 @babel/polyfill。
然后运行脚本:
yarn build编译之后它把所有的 polyfill 注入到我们的入口文件中,截取部分代码如图所示:
接下来,重点讲一下 babel 配置文件中的 useBuiltIns 这个参数:
"usage" | "entry" | false, defaults to false.entry:将import 'core-js'和import 'regenerator-runtime',替换(或打散)为适配目标环境的各个引用。
// app.js
import 'core-js/stable';
import 'regenerator-runtime/runtime'
// babel-loader options
{
"presets": [
[
"@babel/env",
{
"useBuiltIns": "entry",
"corejs": 3,
"targets": {
"browsers": [
"last 2 versions",
"not ie <= 10"
]
}
}
]
]
}usage:每个文件中使用 polyfill 时,为它们添加导入。也就是按需加载。
注意! 使用
usage时,不需要手动导入 corejs | regenerator-runtime,否则会提示错误
修改一下我们之前的 babel 配置文件,如下:
{
"presets": [
[
"@babel/env",
{
"useBuiltIns": "usage",
"corejs": 3
}
]
]
}他会为我们每一个文件添加 polyfill。
修改我们的 webpack 配置:
rules: [
{
test: /\.js$/,
// useBuiltIns: 'usage' 时设置
// 这里排除了 core-js,webpack/buildin ,否则会报错,因为babel默认会将corejs自身也进行编译
// https://github.com/zloirock/core-js/issues/743
exclude: [
/\bcore-js\b/,
/\bwebpack\/buildin\b/
],
use: {
loader: 'babel-loader'
}
},
],然后我们编译一下,现在按需加载,体积减少很多。截取部分代码如图所示:
false: 不会像usage为每个文件添加 polyfill;也不会像entry,将import 'core-js'import 'regenerator-runtime'替换为适配目标环境的各个引用。对在入口文件引入的 polyfill 不做任何处理。
修改一下我们 babel 配置文件,如下:
{
"presets": [
[
"@babel/preset-env",
{
"useBuiltIns": false
}
]
]
}编译结果如下:
false 还是进行了一个全量的引入。
polyfill 的第二种方式
引入了 corejs、regenerator-runtime 这样的 polyfill 会有一定的副作用,比如:
- 引入了新的全局对象:比如 Promise、WeakMap 等。
- 修改现有的全局对象:比如修改了 Array、String 的原型链等。(如第一种方式的截图中)
在应用开发中,上述行为问题不大,基本可控。但如果在库、工具的开发中引入 polyfill,则会带来潜在的问题。
举个例子,我在项目中定义了跟规范不一致的 Array.from() 函数,同时引入了一个库(注入了 polyfill),此时,这个库可能覆盖了自定义的 Array.from() 函数,导致出错。
那么接下来就是 @babel/plugin-transform-runtime 的出场机会了。它将开发者依赖的全局内置对象等,抽取成单独的模块,并通过模块导入的方式引入,避免了对全局作用域的修改(污染)。官方文档。
runtime 这里简单介绍一下
babel 的 runtime,包含两个部分。 @babel/runtime、@babel/plugin-transform-runtime。
@babel/runtime:它其实是一个 babel 内部的库,使用在生产环境的 dependency,在 transform-runtime 作用的过程中,都会使用 @babel/runtime 内部的模块,来代替重复的 helper 函数、对全局空间有污染的 core-js 和 regenerator-runtime 相关变量。
@babel/plugin-transform-runtime:这个插件主要有两个方面的用途。
-
babel 在转码过程中,会加入很多 babe l自己的 helper 函数,这些 helper 函数,在每个文件里可能都会重复存在,transform-runtime 插件可以把这些重复的 helper 函数,转换成公共的、单独的依赖引入,从而节省转码后的文件大小。
-
transform-runtime 可以帮助这种项目创建一个沙盒环境,即使在代码里用到了新的 ES 特性,它能将这些特性对应的全局变量,转换为对 core-js 和 regenerator-runtime 非全局变量版本的引用。这其实也应该看作是一种给代码提供 polyfill 的方式。
接下来进入我们熟悉的实践环节。
安装开发环境:
yarn add @babel/runtime @babel/runtime-corejs3
yarn add @babel/plugin-transform-runtime --save-dev注意 @babel/runtime 要做为项目的 dependencies
然后修改我们的 babel 配置文件:
{
"presets": [
[
"@babel/preset-env"
]
],
"plugins": [
[
"@babel/plugin-transform-runtime",
{
"corejs": {
"version": 3,
"proposals": true
}
}
]
]
}编译运行:
与第一种方式的 polyfill 对比可以发现,runtime 将新的 API 进行了重命名,它是不会重写覆盖原生方法的。
需要注意 @babel/plugin-transform-runtime 的默认配置中,是不会注入对提案的 polyfill 代码。如果想要支持提案中的 API,只需要增加和 @babel/preset-env 类似的配置项。
"corejs": {
"version": 3,
"proposals": true
}七、小结
这里可能有人好奇,如果我们 @babel/plugin-transform-runtime 开启 corejs 并且 @babel/preset-env 也开启 useBuiltIns 会怎么样。
结论是:被使用到的 API polyfill 将会采用 runtime 的不污染全局方案(注意:@babel/preset-env targets 设置将会失效),而不被使用到的将会采用污染全局的。
对于 @babel/preset-env 我们可以设置 debug 参数,来查看那些 API 被 polyfill,配置如下:
{
"presets": [
[
"@babel/env",
{
"debug": true,
"useBuiltIns": "usage",
"corejs": 3
}
]
]
}效果如下:
对于 polyfill 的使用场景:
- 如果是业务项目开:
@babel/plugin-transform-runtime,建议关闭 corejs,polyfill 的引入由@babel/preset-env完成,即开启useBuiltIns(如需其他配置,自行根据诉求配置)。
{
"presets": [
[
"@babel/preset-env",
{
"targets": {
"chrome": 58
},
"useBuiltIns": "usage",
"corejs": {
"version": 3,
"proposals": true
}
}
]
],
"plugins": [
[
"@babel/plugin-transform-runtime"
]
]
}- 如果是 Library 开发:
@babel/plugin-transform-runtime,建议开启 corejs,polyfill 由@babel/plugin-transform-runtime引入。@babel/preset-env关闭useBuiltIns。
{
"presets": [
[
"@babel/preset-env",
]
],
"plugins": [
[
"@babel/plugin-transform-runtime",
{
"corejs": {
"version": 3,
"proposals": true
}
}
]
]
}可能这里又有新的困惑:
为什么 @babel/preset-env 不能使用不污染全局的 polyfill。(不污染全局的 polyfill 必须由 @babel/plugin-transform-runtime 引入)
为什么要使用不污染全局的 polyfill 就必须要使用 @babel/plugin-transform-runtime,而与此同时我必须妥协掉 @babel/preset-env targets 带来的体积优势。
没有解决方案?
抱歉在现有的 babel 体系下还真的没有好办法来解决这个问题,当然 babel 也意识到了这个问题,于是有了 babel-polyfills(不是 @babel/polyfills)这里不在详细展开,有兴趣的可以自行查看。
对没错,我们还遗漏了 react 工程中如何 polyfill。🙂







