DEV Community

icy0307
icy0307

Posted on

浏览器兼容性代码

如何处理浏览器的兼容性代码和包大小息息相关。

polyfill的通常意义, 在wiki上是这么说的.

💡 In web development, a polyfill is code that implements a feature on web browsers that do not natively support the feature. Most often, it refers to a JavaScript library that implements an HTML5 or CSS web standard, either an established standard on older browsers, or a proposed standard on existing browsers.

然而在现实世界上,领域不同还有不同的jargon(黑话)。了解这些黑话和不同的黑话对于领域范围对处理包大小的实际问题非常有意义。

JS

当我们在讨论js相关的polyfill的时候,实际在说什么?

js的兼容性代码总共需要考虑两大块内容,ECMAScript和web standards api。其中ECMAScript又分为两大部分: 语言的syntax和语言所指的功能。另外浏览器,还有web api提供的能力.

语言的syntax: 如 arrow function, spread operator, async await , generator

语言的feature:• 如ECMAScript: Reflect, Array.prototype.includes

web standard: 如 setTimeout and setInterval

Image description

这对工具有什么影响?

工具分为两大类:compiler 和 polyfill library

compiler如: Babel, TypeScript, esbuild(vite和webpack会处理关于module类的syntax)

polyfill library如:core-js, es-shim, tslib等

compiler 负责 让modern syntax 在现代浏览器工作。他们将浏览器不支持的syntax, 通过一些helper function(@bable/helpler, tslib),转成浏览器能认识的样子。

polyfill library的任务是让native function能工作。es-shim如他的名字一样,支持 ECMAScript里的feature. 而core-js除此之外还支持部分web api

Image description

babel一类的compiler可以对代码进行分析,并自动根据对应的浏览器,通过polyfill provider选择import合适的polyfill library,如core-js或es-shim ,将依赖注入。

如何打包这些polyfill

polyfill and ponyfill

在详述如何打包polyfill之前,我们要先解释一个概念: ponyfill

💡 A ponyfill
 is almost the same as a polyfill, but not quite. Instead of patching functionality for older browsers, a ponyfill provides that functionality as a standalone module you can use

根据polyfill和ponyfill的分类,我们就有两种打包方式。其中polyfill指的是指global context的,可能会污染全局。ponyfill指的是不会污染全局没有副作用的版本。

一个典型的例子就是 core-js和 core-js-pure

import 'core-js/actual/promise';// 含有副作用的全局polyfill
import Promise from 'core-js-pure/actual/promise'; //无副作用的局部polyfill.如果引用不存在,这部分会被dead code remove
Enter fullscreen mode Exit fullscreen mode

如何打包

For App

对于APP来说。 一般来说我们可能不怕全局污染(当作为微前端应用的一部分就要重新考虑)。

最原始的做法

最原始的做法是: 所有的polyfill的直接在应用入口文件定义。
例如在polyfill-io上,选择你需要的polyfill,生成一个“polyfill.min.js”,并在html所有script开始前引用它。
或者使用babel,将要用的功能插件,例如"@babel/plugin-transform-spread”枚举出来。
这个做法的问题在于,靠人工制定哪个环境下需要哪些功能。需要人工知道哪些浏览器支持ECMA的哪些标准,并不方便放弃或增加对于浏览器的支持。如果node_modules里有人用了什么不知道的feature,代码就没有经过polyfill.

改进的做法

工业化的第二个改进,是引入类似browserlist的声明。在package.json或其配置文件里声明需要对哪些浏览器起效。

    // package.json
    {
      "browserslist": [
        "last 1 version",
        "> 1%",
        "not dead"
      ]
    }
Enter fullscreen mode Exit fullscreen mode

在入口处引入polyfill.例如“import "core-js";”. 然后compiler根据browserlist里的声明。将目标浏览器下缺失的每个功能列出来。全局polyfill, 使得后面的代码无需引入polyfill也能直接引用。

    import "core-js/modules/es.string.pad-start";
    import "core-js/modules/es.string.pad-end";
Enter fullscreen mode Exit fullscreen mode

这就是babel/preset-env的useBuiltIns: 'entry'的或babel-polyfills系列的"method": "entry-global"行为。
在入口文件根据目标浏览器,把可能用到的每一个ECMAScript feature引入。这样的好处是,只需要在入口处声明一次,并且方便更改浏览器支持。如果某个包没有参与app的编译也无所谓。因为所有的功能都在入口文件列出了。
但这样也同样带来了问题,由于没有分析代码用到了哪些feature,所以需要把所有功能在入口列出。
这样虽然根据浏览器裁剪了部分polyfill,但任然不符合模块化的要求,代码可能很后面才用到或者压根用不到,造成首屏和整体包体积膨胀。譬如core-js,gzip后任然有70kB。这些polyfill是全局副作用,还会占据首屏运行时间。

最好的做法

最佳的做法是用到什么引入什么。babel这样的compiler分析这个库的所有代码(包括node_modules)。根据分析结果, 将引用插入每个文件开头。


    // 源文件
    foo.flatMap(x => [x, x+1]);
    // babel-plugin-polyfill-corejs3 usage-pure 转换后, 不污染全局
    _flatMapInstanceProperty(foo).call(foo, x => [x, x + 1]);
    // babel-plugin-polyfill-corejs3 usage-global
    // 或preset-env useBuiltIns: 'usage'转换后 污染全局
    import "core-js/modules/es.array.flat-map.js";
    foo.flatMap(x => [x, x + 1]);
Enter fullscreen mode Exit fullscreen mode

这样减少了文件执行时间和文件大小。由于是app, 我们可以选择在全局进行polyfill。

babel该如何配置
  1. 使用@babel/preset-env避免手工枚举plugin, 注意proposal需要单独声明出来。
  2. 如果需要全局polyfill,使用@babel/preset-envuseBuiltIns: 'usage'[babel-plugin-polyfill-corejs3](https://github.com/babel/babel-polyfills/blob/main/packages/babel-plugin-polyfill-corejs3)的usage-global。后者是前者最新用法的代替。区别在于原来只能使用corejs作为polyfill。现在可以自己指定polyfill-provider。譬如你还可以使用[babel-plugin-polyfill-es-shims](https://github.com/babel/babel-polyfills/blob/main/packages/babel-plugin-polyfill-es-shims)/
  3. 如果需要局部ponyfill,使用[babel-plugin-polyfill-corejs3](https://github.com/babel/babel-polyfills/blob/main/packages/babel-plugin-polyfill-corejs3)的usage-global。
  4. 使用@babel/plugin-transform-runtime来 避免helper代码重复。一定要注意version与所使用的runtime版本匹配。这部分会造成非常可观的代码膨胀。
作为库作者如何打包

最好的方式莫过于不打包。不去预设用户需要哪些polyfill。因为知道用户需要什么的只有用户自己。一旦预设的polyfill超出了用户的需要,就会造成不需要的代码被下载。库应该参与项目的构建。

不过现实是大部分为了使用者方便,很多项目polyfill提前引用或打包。虽然这是不被推荐的行为。如果真的需要这么做的话,仍然要注意:

  1. 使用ponyfill而不是polyfill,不要污染全局。
  2. 使用babel/plugin-transform-runtime来,并把 @babel/runtime, core-js 声明为dependency(不是peer dep或者dev dep), 并注意 不要打包
  3. 库一般会提供多个target, 如cjs,es5,es2015等等,shippedProposals, esm等选项都要做出相应的改变。
  4. version字段与你使用的runtime版本匹配。因为默认值是@babel/runtime@7.0.0,会造成版本不匹配,进而导致本该inject helper的地方,代码直接内连。例如regenerator的boilerplate代码就特别多。

一个完整的例子:

//babel.config.js
{
  "presets": [
    ["@babel/preset-env", { // 根据browser list进行polyfill
      "targets": { "firefox": 42 }
            "modules": useESModules ? false : 'commonjs',//根据你的构建产物判断是否使用esm
    }]
  ],
  "plugins": [
    ["@babel/transform-runtime", {// 不是每个文件都插入代码,而是从@babel/runtime/helpers import
            useESModules //根据你的构建产物判断是否使用esm
            version: '^7.4.4', // 不要使用默认版本,避免无法inject造成代码膨胀
        }],
    ["polyfill-corejs3", { 
      "method": "usage-pure" // 用ponyfill
    }]
  ]
}
Enter fullscreen mode Exit fullscreen mode

ECMAScript syntax

打包方式基本和上文说的一致,需要区分app和library

typescript, babel都可以进行转换。

需要注意的是,babel/runtime或tslib都需要显示的声明成production dependencies. 并使用上文提到的@babel/plugin-transform-runtime来避免重复代码

WEB API

core-js提供一部分web-api的实现。

但web-api的问题在于,这部分功能大多数需要浏览器支持,不支持可能就是没有办法实现。所以当你用一个功能的时候,先在CANIUSE上判断您的大多数用户是否可以使用,并提供降级方案。

Css

不同的浏览器下,对css的支持也有差异。通常用prefix来区分。例如 “-moz”代表mozilla旗下的fire-fox, -webkit代表苹果使用的webkit. 分清哪些场景使用哪些属性,单靠手工在caniuse查询依然十分不可靠。我们可以使用css中的polyfill: autoprefixer 同样根据browserlist自动完成。

Browserlist的角色和问题

browserlist可以方便我们更新支持的browser,并且所有工具链都可以根据统一的target来统一完成对目标浏览器的适配。

例如,defaults的默认值是> 0.5%, last 2 versions, Firefox ESR, not dead

browserlist的数据来自于caniuse-lite。可以使用[update-browserslist-db](https://github.com/browserslist/update-db)来定期更新。

所以我们需要提交lock, 指出browserlist依赖的caniuse-lite版本,否则不同构建产生的polyfill都可能不一样。

另外一个应用可能含有很多仓库。对于同一个项目,使用shared-config来让各个项目统一。

ref:

https://en.wikipedia.org/wiki/Polyfill_(programming)

http://es6-features.org/#StatementBodies

https://github.com/babel/babel/issues/10008#user-content-annex-b

https://github.com/babel/babel/discussions/14443

https://github.com/babel/babel/issues/9853

http://es6-features.org/#BinaryOctalLiteral

https://github.com/zloirock/core-js/labels/web standards

https://caniuse.com/

https://babeljs.io/docs/babel-plugin-transform-runtime

Top comments (0)