天呀鲁,一文看前端构建兼容性方案

911 阅读9分钟

一、背景&目标

1.1 问题背景

在之前的前端监控中,经常发现存在类似「Object.values(...).flat is not a function」(fxxx is not a function)的报错内容,出现此类报错直接原因是对象上没有对应的方法直接调用导致,可能的代码有两种:

  1. 调用对象的方法,但实际没有定义过这个方法;
  2. 调用浏览器不兼容的 api,导致找不到这个方法;

以上代码导致的报错均属于 js error。

image.png

1.2 问题定位

首先,通过前端监控的 JS 代码反解能力,定位到错误代码为项目中的 router 文件中使用 flat api 出现问题。

image.png image.png

1.3 问题原因

这个问题的产生原因也非常简单,在项目中 browserslistrc 配置如下:

// .browserslistrc
> 1%
last 2 versions
not ie <= 8

根据配置检查的兼容性如下: image.png

查看详情戳这里

因 flat api 较新(ES10),所以在部分低版本浏览器无法兼容。

image.png

这个问题无非就是兼容性问题,兼容性的解决有很多种方案,今天我就大家一起来了解一下前端兼容性的方案。

1.4 目标

  • 解决 API 兼容性问题;
  • 兼容方案接入简单(仅需很简单的配置);
  • 兼容方案容易传播,与技术栈无关,容易上手,无需过多前置知识;

二、方案调研

image.png

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,比较推荐, 可以最大程度上的节省资源。

测试演示如下: a742edf5efbf307d7810110e8a9c269014705350.gif

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 整体流程

image.png

3.2 详细设计

3.2.1 梳理系统需要兼容的浏览器版本

在实践的过程中,可以根据系统需要兼容的浏览器版本,梳理 Browserslist 列表。例如:

> 0.1% and not dead
Chrome >= 85
Firefox >= 100
Edge >= 88

可以通过 browsersl.ist 来确认兼容的浏览器版本信息。 image.png 查看详情戳这里

3.2.2 开发 browserslist npm

如果存在多个业务系统,需要做兼容性处理,那么开发一个 npm 包,绝对是一劳永逸的办法。并且这个 npm 开发起来也不麻烦,例如: image.png 你的兼容包中,其实只需要导出兼容配置就好。

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 方案

image.png

4.2 整体流程

image.png

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 值对应的垫片查看(可查看对应值支持的垫片列表):

官方地址:polyfill.io/v3/url-buil…

image.png

4.3.2 动态扫描 feature

动态扫描 feature 方式,是在不知道项目中存在多少 feature 的情况下,通过工具扫描 feature 生成链接。

image.png 以如下代码为测试用例:

console.log('测试测试', [1].flat());

通过命令生成垫片 URL:

npx create-polyfill-service-url analyse --file xxx

f181cdf2f48bb7a046128a5339d043891578611.gif

4.4 性能测试

我们再来看看不同 polyfill 的性能测试

4.4.1 节点检查

阿里 polyfillservice 服务共 13 节点。 image.png 美团 polyfillservice 服务工2节点。

image.png polyfill.io 的 polyfillservice 服务共 4 节点(全部在美国)。

image.png

检查地址:myssl.com/cdn_check.h…

4.4.2 站点测速

阿里 polyfillservice 服务站点速度在 20ms ~ 30ms,非常快 image.png 美团 polyfillservice 服务站点速度 80 ms ~ 100 ms,相对来说有一点慢。 image.png polyfill.io 的 polyfillservice 服务站点速度,在三者来说是最慢的。 image.png

4.4.3 小结

就性能来说,阿里的 polyfillservice 是最好的,速度快,当然 polyfill.io 是免费的。并且如果你自己有服务器,可以自己搭建一个 polyfillservice 服务。如果大家对搭建 polyfillservice 服务感兴趣评论区告诉我,我出一篇「polyfill.io 服务搭建指南」。

五、回滚方案

介绍完, @babel/preset-env +core-js,polyfill.io,最后再来说说这两种方案的回滚方案。如果你在用这两种方案时,发现出现问题,如何回滚?

5.1 @babel/preset-env +core-js 回滚方案

  1. 删除 config-overrides.js / babel.config.js 中的预设配置(required);
  2. 删除 browserslist 新的浏览器兼容配置;
  3. 删除 package.json 中 core-js 、@babel/core 的依赖;
  4. 提交代码,重新部署(required);

5.2 polyfill.io 回滚方案

  1. 删除 html 中的 script 引入代码(required);
  2. 提交代码,重新部署(required);

参考