背景
Polyfill你可以理解为“腻子”,就是装修的时候,可以把缺损的地方填充抹平。
举个例子,html5的storage(session,local), 不同浏览器,不同版本,有些支持,有些不支持。
polyfill是一个很重要的概念,它让你可以无顾虑地使用更新的JavaScript特性,而不需要关注浏览器兼容性。
一般来说,我们会用core-js来做polyfill,常见的玩法有:
- 整个引入polyfill,这会包含一个非常大的文件,比如最新的3.7.0版本一共158KB,在gzip后45.9KB。
- 让babel去处理,使用@babel/preset-env加上useBuiltins配置来裁剪polyfill。
一般来说,对于一个独立的产品,使用babel处理后,将core-js按需拿出来打包到最终的产出里就能解决大部分问题。但如果在特殊场合下,这种方式也会有一些问题:
- 有多个浏览器兼容性一致的项目,它们可以共享一个polyfill,但因为被打包到了最终产物中,这个共享并不容易实现。
- 当在代码中用到不同特性的时候,babel会导入不同的polyfill内容,这导致某些希望稳定不变的产物(如vendors)的hash变化,缓存失效。
core-js 与垫片
core-js 是一个 JavaScript 标准库,它包含了 ECMAScript 2020 在内的多项特性的 polyfills,以及 ECMAScript 在 proposals 阶段的特性、WHATWG/W3C 新特性等。因此它是一个现代化前端项目的“标准套件”。
除了 core-js 本身的重要性,它的实现理念、设计方式都值得我们学习。事实上,core-js 是一扇大门:
-
通过 core-js,我们可以窥见前端工程化的方方面面;
-
core-js 又和 Babel 深度绑定,因此学习 core-js,也能帮助开发者更好地理解 babel 生态,进而加深对前端生态的理解;
-
通过对 core-js 的解析,我们正好可以梳理前端一个极具特色的概念——polyfill(垫片/补丁)。
core-js
core-js 是一个由 Lerna 搭建的 Monorepo 风格的项目,在它的 packages 中,我们能看到五个相关包:
core-js 实现的基础垫片能力,是整个 core-js 的逻辑核心。 比如我们可以按照如下代码引入全局 polyfills:
import 'core-js';
或者按照:
import 'core-js/features/array/from';
的方式,按需在业务项目的入口引入某些 polyfills。 core-js 为什么有这么多的 packages 呢?实际上,它们各司其职,又紧密配合,接下来我们来具体分析。
core-js-pure 提供了不污染全局变量的垫片能力,比如我们可以按照:
import _from from 'core-js-pure/features/array/from';
import _flat from 'core-js-pure/features/array/flat';
的方式,来实现独立的导出命名空间,进而避免全局变量的污染。
core-js-compact 维护了按照browserslist规范的垫片需求数据,来帮助我们找到“符合目标环境”的 polyfills 需求集合,比如以下代码:
const {
list, // array of required modules
targets, // object with targets for each module
} = require('core-js-compat')({
targets: '> 2.5%'
});
就可以筛选出全球使用份额大于 2.5% 的浏览器范围,并提供在这个范围下需要支持的垫片能力。
core-js-builder 可以结合 core-js-compact 以及 core-js,并利用 webpack 能力,根据需求打包出 core-js 代码。比如:
require('core-js-builder')({
targets: '> 0.5%',
filename: './my-core-js-bundle.js',
}).then(code => {}).catch(error => {});
将会把符合需求的 core-js 垫片打包到my-core-js-bundle.js文件当中。整个流程可以用代码演示为:
require('./packages/core-js-builder')({ filename: './packages/core-js-bundle/index.js' }).then(done).catch(error => {
// eslint-disable-next-line no-console
console.error(error);
process.exit(1);
});
总之,根据分包的设计,我们能发现,core-js 将自身能力充分解耦,提供出的多个包都可以被其他项目所依赖。比如:
-
core-js-compact 可以被 Babel 生态使用,由 Babel 分析出根据环境需要按需加载的垫片;
-
core-js-builder 可以被 Node.js 服务使用,构建出不同场景的垫片包。
宏观上的设计,体现了工程复用能力。下面我们通过一个微观 polyfill 案例,从一个具体的垫片实现,进一步加深理解。
shim和polyfill有什么区别
在JavaScript的世界里,有两个词经常被提到,shim和polyfill.它们指的都是什么,又有什么区别?
一个shim是一个库,它将一个新的API引入到一个旧的环境中,而且仅靠旧环境中已有的手段实现。
一个polyfill就是一个用在浏览器API上的shim.我们通常的做法是先检查当前浏览器是否支持某个API,如果不支持的话就加载对应的polyfill.然后新旧浏览器就都可以使用这个API了.术语polyfill来自于一个家装产品Polyfilla:
Polyfilla是一个英国产品,在美国称之为Spackling Paste(译者注:刮墙的,在中国称为腻子).记住这一点就行:把旧的浏览器想象成为一面有了裂缝的墙.这些[polyfills]会帮助我们把这面墙的裂缝抹平,还我们一个更好的光滑的墙壁(浏览器)
Paul Irish发布过一个Polyfills的总结页面“HTML5 Cross Browser Polyfills”.es5-shim是一个shim(而不是polyfill)的例子,它在ECMAScript 3的引擎上实现了ECMAScript 5的新特性,而且在Node.js上和在浏览器上有完全相同的表现(译者注:因为它能在Node.js上使用,不光浏览器上,所以它不是polyfill).
polyfill方案
html5各个特性支持的Polyfill,你需要哪个,就引入哪个。当然,你也可以自己写 :参考
github.com/Modernizr/M…
手动:不推荐
缺点:
非官方实现,很容易出现于规范不一致导致的问题,一些polyfill实现起来也很麻烦,仅用于临时一些修复。不是一种工程化的解决方式,方案原始而难以维护,同时对于 polyfill 的实现要求较高。
举例:以 ES6 的 object#assign 为例 ,即使在 IE 11 上,仍会报错;我们需要打上相应的补丁。可以用第三方成熟的 package ,也可以使用 MDN 提供的模板进行打补丁
Object.assign = require('object-assign')
// or
// Refer: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/assign
if (typeof Object.assign != 'function') {
// Must be writable: true, enumerable: false, configurable: true
Object.defineProperty(Object, 'assign', {
value: function assign(target, varArgs) {
// .length of function is 2
'use strict'
if (target == null) {
// TypeError if undefined or null
throw new TypeError('Cannot convert undefined or null to object')
}
var to = Object(target)
for (var index = 1; index < arguments.length; index++) {
var nextSource = arguments[index]
if (nextSource != null) {
// Skip over if undefined or null
for (var nextKey in nextSource) {
// Avoid bugs when hasOwnProperty is shadowed
if (Object.prototype.hasOwnProperty.call(nextSource, nextKey)) {
to[nextKey] = nextSource[nextKey]
}
}
}
}
return to
},
writable: true,
configurable: true,
})
}
问题是解决了,但优势和劣势也相当明显:优势是保持最小化引入,不会有额外的冗余代码开销,保证了应用的性能。劣势是手动导入不易管理和维护,对于多样化的 polyfill 和变化多端的 Web 应用维护成本比较大
根据覆盖率自动打补丁
在黑魔法 Webpack 的加持下,我们可以更现代化的方式打补丁。这其中相关的依赖有: @babel/preset-env、 @babel/plugin-transform-runtime 、 core-js,@babel/polyfill 。先逐一介绍它们:
-
@babel/preset-env- 按需编译和按需打补丁 -
core-jsJavaScript 标准库core-js 是实现 JavaScript 标准运行库之一,它提供了从 ES3 ~ ES7+ 以及还处在提案阶段的 JavaScript 的实现。
-
@babel/plugin-transform-runtime- 重利用 Babel helper 方法的 babel 插件@babel/plugin-transform-runtime 是对 Babel 编译过程中产生的 helper 方法进行重新利用(聚合),以达到减少打包体积的目的。此外还有个作用是为了避免全局补丁污染,对打包过的 bunler 提供"沙箱"式的补丁。 -
@babel/polyfill- core-js 和 regenerator-runtime 补丁的实现库。
@babel/polyfill 通过定制 polyfill 和 regenerator,提供了一个 ES2015+ 环境 polyfill的库。因为它是由其他两个库实现的,直接引入其他两个库即可,所以已被**废弃**。
babel-polyfill 结合 @babel/preset-env + useBuiltins(entry) + preset-env targets 的方案如今更为流行,@babel/preset-env 定义了 Babel 所需插件预设,同时由 Babel 根据 preset-env targets 配置的支持环境,自动按需加载 polyfills,使用方式如下:
@babel/preset-env 会根据目标环境来进行编译和打补丁。具体来讲,是根据参数 targets 来确定目标环境,默认情况下它编译为 ES2015,可以根据项目需求进行配置:
{
"presets": [
["@babel/env", {
useBuiltIns: 'entry',
targets: {
chrome: 44
}
}]
]
}
这样我们在工程代码入口处的:
import '@babel/polyfill';
会被编译为:
import "core-js/XXXX/XXXX";
import "core-js/XXXX/XXXXX";
这样的方式省力省心。也是 core-js 和 Babel 深度绑定并结合的典型案例。
细细想想,其实还有不少问题,
- 特性列表是按浏览器整理的,那怎么知道哪些特性我用了,哪些没有用到,没有用到的部分也引入了是不是也是冗余?
@babel/preset-env有提供 exclude 的配置,如果我配置了 exclude,后面是否得小心翼翼地确保不要用到 exclude 掉的特性 - 补丁是打包到静态文件的,如果我配置 targets 为
chrome: 62, ie: 9,那意味着 chrome 62 也得载入 ie 9 相关的补丁,这也是一份冗余 - 我们是基于 core-js 打的补丁,所以只会包含 ecmascript 规范里的内容,其他比如说 dom 里的补丁,就不在此列,应该如何处理?
根据浏览器特性,动态打补丁
UA检测 polyfill
我们可以根据我们使用的新特性传输给服务器,然后服务器检测浏览器的UA,就可以决定真正返回的polyfill了,这个思路实现了真正的环境按需polyfill,不像browserslist一样会取所有兼容环境的并集。
优点:
- 能够真正按照运行环境进行polyfill
缺点:
-
多一次http请求
-
UA标志混乱,很可能导致Polyfill缺失
-
增加服务区成本
Polyfill.io 就是实现这个方案的服务,它会根据浏览器的 UA 不同,返回不一样的补丁。如想要 Promise 补丁,在页面引入:
<script src="https://polyfill.io/v3/polyfill.js?features=Promise"></script>
这种方案以 Polyfill.io 为代表,它提供了 CDN 服务,使用者可以按照所需环境,生成打包链接:
在高版本浏览器上,可能会返回空内容,因为该浏览器已经支持了 ES2015 特性。如果在低版本浏览器上,将会得到真实的 polyfills bundle。
从工程化的角度来说,一个趋于完美的 polyfill 设计应该满足的核心原则是按需加载补丁,这个按需加载主要包括两方面:
-
按照用户终端环境
-
按照业务代码使用情况
因为按需加载补丁,意味着更小的 bundle size,直接决定了应用的性能。
阿里fork了它的开源服务,做了如下定制
将polyfill包进行拆分,加入了polyfill.io不认可但是工程需要的polyfill比如说RegeneratorRuntime
编写了标准的UA识别逻辑,大幅大幅降低了响应时间 通常响应时间在 30ms 以内
利用CDN提提供了高可用性的服务
**阿里polyfill:**polyfill.alicdn.com/polyfill.mi…
总结:
关于补丁方案的未来,我觉得按需特性探测 + 在线补丁才是终极方案。
按需特性探测保证特性的最小集;在线补丁做按需下载。
按需特性探测可以用 @babel/preset-env 配上 targets 以及试验阶段的 useBuiltIns: usage,保障特性集的最小化。之所以说是未来,因为 JavaScript 的动态性,语法探测不太可能探测出所有特性,但上了 TypeScript 之后可能会好一些。另外,要注意一个前提是 node_modules 也需要走 babel 编译,不然 node_modules 下用到的特性会探测不出来。
在线补丁可以用类似前面介绍的 polyfill.io/ 提供的方案,让浏览器只下载必要的补丁,通常大公司用的话会部署一份到自己的 cdn 上。