一、背景&目标
1.1 问题背景
在之前的前端监控中,经常发现存在类似「Object.values(...).flat is not a function」(fxxx is not a function)的报错内容,出现此类报错直接原因是对象上没有对应的方法直接调用导致,可能的代码有两种:
- 调用对象的方法,但实际没有定义过这个方法;
- 调用浏览器不兼容的 api,导致找不到这个方法;
以上代码导致的报错均属于 js error。
1.2 问题定位
首先,通过前端监控的 JS 代码反解能力,定位到错误代码为项目中的 router 文件中使用 flat api 出现问题。
1.3 问题原因
这个问题的产生原因也非常简单,在项目中 browserslistrc 配置如下:
// .browserslistrc
> 1%
last 2 versions
not ie <= 8
根据配置检查的兼容性如下:
查看详情戳这里
因 flat api 较新(ES10),所以在部分低版本浏览器无法兼容。
这个问题无非就是兼容性问题,兼容性的解决有很多种方案,今天我就大家一起来了解一下前端兼容性的方案。
1.4 目标
- 解决 API 兼容性问题;
- 兼容方案接入简单(仅需很简单的配置);
- 兼容方案容易传播,与技术栈无关,容易上手,无需过多前置知识;
二、方案调研
2.1 @babel/preset-env +core-js
这种方案是最常见的兼容低版本浏览器方案,
.babelrc配置:
{
"presets": [
[
"@babel/preset-env",
{
"useBuiltIns": "usage",
"corejs": "xxx"
}
]
],
}
browserslist 配置:
{
"development": ["last 1 chrome version", "last 1 firefox version", "last 1 safari version"],
"production": ["> 0.1% and not dead", "Chrome >= 85", "Firefox >= 100", "Edge >= 88"],
}
这种方案优点很明显。引入简单自动引入所需的,无需开发者考虑需要引入哪些垫片。缺点打包产物体积增大。
注意:垫片(Polyfill)只能定义一些不支持的方法/对象,不支持新的语法糖(可选链,箭头函数,const,let这种)使用垫片前得确保语法最好都是ES5的。
2.2 script module & nomodule
通过 CDN 的方式引入垫片,或者在需要的时候才加载垫片资源。
在 HTML5 中,script 标签支持一个新的属性 module (原生支持模块功能),我们可以使用这个特性来检测浏览器对 ES6的支持程度。支持 type="module" 的浏览器也支持 async、await、class、Promise 等新特性。和 module 对应的还有一个 "nomodule" 属性,在不支持 type="module" 的浏览器可以使用 "nomodule" 来进行处理。
nomodule 指向的 js 资源只包含垫片,只在低版本浏览器中加载 polyfills 资源。
<script nomodule src="js/main-polyfills.js"></script>
<script src="js/main.js"></script>
低版本浏览器加载 nomodule 中的资源,支持 module 属性的加载 module 中的资源
<script nomodule src="js/main-polyfills.js"></script>
<script type="module" src="js/main.js"></script>
这种实现方案,实现比较简单,浏览器原生能力,但是可控粒度不够细,使用前需要调研,垫片资源是否能在目标环境中正常的加载,原因在于 type="module" 和 nomodule 本身也存在浏览器兼容问题。
2.3 全量引入所需的 polyfill 资源
在需要的文件头部插入如下代码:
// npm install --save @babel/polyfill
import 'babel-polyfill';
export const foo = (a, b) => Object.assign(a, b);
babel-polyfill 在项目代码前插入所有的 polyfill 代码,为你的程序打造一个完美的 es2015 运行环境。babel 建议在网页应用程序里使用 babel-polyfill,只要不在意它略有点大的体积(min 后 86kb),直接用它肯定是最稳妥的。值得注意的是,因为 babel-polyfill 带来的改变是全局的,所以无需多次引用,也有可能因此产生冲突,所以最好还是把它抽成一个 common module,放在项目 的 vendor 里,或者干脆直接抽成一个文件放在 cdn 上。
如果你是在开发一个库或者框架,那么 babel-polyfill 的体积就有点大了,尤其是在你实际使用的只有一个 Object.assign 的情况下。更可怕的是对于一个库来说,改变全局环境是使不得的。谁也不希望使用了你的库,还附带了一家老小的 polyfill 改变了全局对象。
注意:从 Babel 7.4.0 开始,这个包已被弃用,取而代之的是直接包含core-js/stable(填充 ECMAScript 特性),并且如果需要使用不是第 4 阶段的提案,@babel/polyfill 将不会自动为您导入这些提案。你将不得不从 core-js 单独导入它们。
2.4 手动按需引入
手动按需引入即使用到需要兼容的 API 或者语法就手动的导入兼容模块,并在文件中导入该兼容模块。
import Promise from 'babel-runtime/core-js/promise'
2.5 script polyfill
这种方式简单粗暴,直接通过 script 标签引入CDN 中需要的 polyfill 资源,也可将自定义的 polyfill 放入到 CDN 中。
<!-- Promise垫片 -->
<script src="https://cdn.xxx.min.js"></script>
<script src="js/main.js"></script>
这种方案优点就是简单,缺点就是不管浏览器 需不需要都会加载,会造成一定的资源浪费。
2.6 polyfill.io
Polyfill.io 通过分析请求头信息中的 UserAgent 实现自动加载浏览器所需的 polyfills。Polyfill.io 有一份默认功能列表,包括了最常见的 polyfills:document.querySelector、Element.classList、ES5 新增的 Array 方法、Date.now、ES6 中的 Object.assign、Promise 等。例如需要在项目中添加 Promise polyfill:
<!-- Promise垫片 -->
<script src="https://polyfill.io/v3/polyfill.min.js?features=Promise"></script>
<script src="js/main.js"></script>
并且 promise polyfill 资源只在不支持promise的浏览器上才会被返回。
这种方案使用简单,可以通过其提供的工具,在线网站,自动生成代码所需垫片的 url,比较推荐, 可以最大程度上的节省资源。
测试演示如下:
2.7 小结
开发的页面需要兼容低版本浏览器,就需要在项目中使用 polyfill 资源,整体对比,推荐方案:
- @babel/preset-env +core-js(推荐)
- polyfill.io(非常推荐)
@babel/preset-env +core-js 是相对比较普遍的一种方案,但是 polyfill 资源,会导致打包体积增大,在不需要垫片的浏览器环境中会造成资源的浪费,拖慢首屏加载。polyfill.io 引入简单,根据浏览器的UA信息,做到真正的动态按需加载,最大程度的节省资源,作为一个服务,易于拓展,一个 CDN 服务,可以实现一行代码接入。
三、@babel/preset-env +core-js 方案实现
3.1 整体流程
3.2 详细设计
3.2.1 梳理系统需要兼容的浏览器版本
在实践的过程中,可以根据系统需要兼容的浏览器版本,梳理 Browserslist 列表。例如:
> 0.1% and not dead
Chrome >= 85
Firefox >= 100
Edge >= 88
可以通过 browsersl.ist 来确认兼容的浏览器版本信息。
查看详情戳这里
3.2.2 开发 browserslist npm
如果存在多个业务系统,需要做兼容性处理,那么开发一个 npm 包,绝对是一劳永逸的办法。并且这个 npm 开发起来也不麻烦,例如:
你的兼容包中,其实只需要导出兼容配置就好。
module.exports = {
development: ['last 1 chrome version', 'last 1 firefox version', 'last 1 safari version'],
production: ['> 0.1% and not dead', 'Chrome >= 85', 'Firefox >= 100', 'Edge >= 88'],
}
3.2.3 系统接入
然后在项目中引入依赖:
npm install 「npm 包名称」 --save-dev
// or
yarn add 「npm 包名称」 --dev
添加 package.json 配置:
// package.json
"browserslist": [
"extends 「npm 包名称」"
]
这样就结束了。是不是很简单。
3.2.4 添加 preset-env
但是这里有一点还需要注意一下,不同的项目,添加 preset-env 的方式不太一样。原因是 preset-env 可以通过 .babelrc 、babel.config.js、babel.config.json 等文件进行配置。
3.2.4.1 Vue CLI 项目
使用 Vue CLI 的项目,使用 @vue/babel-preset-app 进行预设配置,@vue/babel-preset-app 默认包含 @babel/preset-env。
注意:此预设专门用于通过 Vue CLI 创建的项目,不考虑外部用例。
在项目目录的 babel.config.js 中,添加 presets 配置:
module.exports = {
presets: [
[
'@vue/app',
{
useBuiltIns: 'usage',
},
],
],
};
注意:使用 @vue/app 本质就是在使用 @vue/babel-preset-app 进行预设配置,但是一定记得先安装依赖。
3.2.4.2 Create-react-app 项目
添加 .babelrc 文件:
// .babelrc
{
"presets": [
["@babel/preset-env", {
"useBuiltIns": "usage",
"corejs": "3"
}]
]
}
开启 react-app-rewired 配置:
// config-overrides.js
const { ..., useBabelRc,} = require('customize-cra');
module.exports = override(
...
useBabelRc(),
...
);
------- or -----
在项目目录的 config-overrides.js 中,添加 presets 配置:
const { ..., addBabelPresets,} = require('customize-cra');
module.exports = override(
...addBabelPresets(
[
'@babel/preset-env',
{
useBuiltIns: 'usage',
corejs: { version: 'xxx', proposals: true },
loose: true,
}
]
),
// ...
)
注意:如果缺失依赖,请安装 "core-js": "xxx", "@babel/core": "xxx"。
3.2.4.3 普通 Webpack 配置项目
添加 .babelrc 文件:
// .babelrc
{
"presets": [
["@babel/preset-env", {
"useBuiltIns": "usage",
"corejs": "3"
}]
]
}
四、polyfill.io 方案实现
polyfill.io 方案相对 @babel/preset-env +core-js 来说引入简单,根据浏览器的UA信息,做到真正的动态按需加载,最大程度的节省资源,作为一个服务,易于拓展,一个 CDN 服务,可以实现一行代码接入。 polyfill.io 这种方案其实也有多种可使用方案。
4.1 方案
4.2 整体流程
4.3 使用方式
4.3.1 已知 feature
当你知道你项目中需要哪些 feature (应该比较少见)。可直接在 html 模板中 script 引入,通过配置参数 feature 加上需要的垫片
如果没有 feature 将会返回默认少量垫片内容
同步方式:
<script src="https://polyfill.meituan.com/polyfill.min.js?feature=flat,map......"></script>
异步方式:
这里的异步只是异步加载垫片,加载完成触发指定的回调函数
<script src="https://polyfill.meituan.com/polyfill.min.js?features=flat,map&callback=main" async></script>
Pollify.io 会根据 feature 和浏览器返回不同的兼容代码块。
feature 值对应的垫片查看(可查看对应值支持的垫片列表):
4.3.2 动态扫描 feature
动态扫描 feature 方式,是在不知道项目中存在多少 feature 的情况下,通过工具扫描 feature 生成链接。
以如下代码为测试用例:
console.log('测试测试', [1].flat());
通过命令生成垫片 URL:
npx create-polyfill-service-url analyse --file xxx
4.4 性能测试
我们再来看看不同 polyfill 的性能测试
4.4.1 节点检查
阿里 polyfillservice 服务共 13 节点。
美团 polyfillservice 服务工2节点。
polyfill.io 的 polyfillservice 服务共 4 节点(全部在美国)。
4.4.2 站点测速
阿里 polyfillservice 服务站点速度在 20ms ~ 30ms,非常快
美团 polyfillservice 服务站点速度 80 ms ~ 100 ms,相对来说有一点慢。
polyfill.io 的 polyfillservice 服务站点速度,在三者来说是最慢的。
4.4.3 小结
就性能来说,阿里的 polyfillservice 是最好的,速度快,当然 polyfill.io 是免费的。并且如果你自己有服务器,可以自己搭建一个 polyfillservice 服务。如果大家对搭建 polyfillservice 服务感兴趣评论区告诉我,我出一篇「polyfill.io 服务搭建指南」。
五、回滚方案
介绍完, @babel/preset-env +core-js,polyfill.io,最后再来说说这两种方案的回滚方案。如果你在用这两种方案时,发现出现问题,如何回滚?
5.1 @babel/preset-env +core-js 回滚方案
- 删除 config-overrides.js / babel.config.js 中的预设配置(required);
- 删除 browserslist 新的浏览器兼容配置;
- 删除 package.json 中 core-js 、@babel/core 的依赖;
- 提交代码,重新部署(required);
5.2 polyfill.io 回滚方案
- 删除 html 中的 script 引入代码(required);
- 提交代码,重新部署(required);