New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Polyfill 方案的过去、现在和未来 #80

Open
sorrycc opened this Issue Jan 29, 2019 · 16 comments

Comments

Projects
None yet
@sorrycc
Copy link
Owner

sorrycc commented Jan 29, 2019

任何一个小知识点,深挖下去,也是非常有意思的。

什么是补丁?

A polyfill, or polyfiller, is a piece of code (or plugin) that provides the technology that you, the developer, expect the browser to provide natively. Flattening the API landscape if you will.

我们希望浏览器提供一些特性,但是没有,然后我们自己写一段代码来实现他,那这段代码就是补丁。

比如 IE11 不支持 Promise,而我们又需要在项目里用到,写了这样的代码:

<script>
  Promise.resolve('bar')
    .then(function(foo) {
      document.write(foo);
    });
</script>

这时在 IE 下运行就会报错了,

然后在此之前加上补丁,

<script src="https://cdn.jsdelivr.net/npm/promise-polyfill@8/dist/polyfill.min.js"></script>
<script>
  Promise.resolve('bar')
    .then(function(foo) {
      document.write(foo);
    });
</script>

刷新浏览器,就可以正常运行了,

过去

shim + sham

如果你是一个 3 年陈 + 的前端,应该会有听说过 shim、sham、es5-shimes6-shim 等等现在看起来很古老的补丁方式。

那么,shim 和 sham 是啥?又有什么区别?

  • shim 是能用的补丁
  • sham 顾名思义,是假的意思,所以 sham 是一些假的方法,只能使用保证不出错,但不能用。至于为啥会有 sham,因为有些方法的低端浏览器里根本实现不了

babel-polyfill.js

在 shim 和 sham 之后,还有一种补丁方式是引入包含所有语言层补丁的 babel-polyfill.js。比如:

<script src="https://cdnjs.cloudflare.com/ajax/libs/babel-polyfill/7.2.5/polyfill.js"></script>

然后就 es6、es7 特性随便写了。

但缺点是,babel-polyfill 包含所有补丁,不管浏览器是否支持,也不管你的项目是否有用到,都全量引了,所以如果你的用户全都不差流量和带宽(比如内部应用),尽可以用这种方式。

现在

现在还没有银弹,各种方案百花齐放。

@babel/preset-env + useBuiltins: entry + targets

babel-polyfill 包含所有补丁,那我只需要支持某些浏览器的某些版本,是否有办法只包含这些浏览器的补丁?这就是 @babel/preset-env + useBuiltins: entry + targets 配置的方案。

我们先在入口文件里引入 @babel/polyfill

import '@babel/polyfill';

然后配置 .babelrc,添加 preset @babel/preset-env,并设置 useBuiltInstargets

{
  "presets": [
    ["@babel/env", {
      useBuiltIns: 'entry',
      targets: { chrome: 62 }
    }]
  ]
}

useBuiltIns: entry 的含义是找到入口文件里引入的 @babel/polyfill,并替换为 targets 浏览器/环境需要的补丁列表。

替换后的内容,比如:

import "core-js/modules/es7.string.pad-start";
import "core-js/modules/es7.string.pad-end";
...

这样就只会引入 chrome@62 及以上所需要的补丁,什么 Promise 之类的都不会再打包引入。

是不是很好用?

😄

有什么问题?

🤔

细细想想,其实还有不少问题,

  1. 特性列表是按浏览器整理的,那怎么知道哪些特性我用了,哪些没有用到,没有用到的部分也引入了是不是也是冗余?@babel/preset-env 有提供 exclude 的配置,如果我配置了 exclude,后面是否得小心翼翼地确保不要用到 exclude 掉的特性
  2. 补丁是打包到静态文件的,如果我配置 targets 为 chrome: 62, ie: 9,那意味着 chrome 62 也得载入 ie 9 相关的补丁,这也是一份冗余
  3. 我们是基于 core-js 打的补丁,所以只会包含 ecmascript 规范里的内容,其他比如说 dom 里的补丁,就不在此列,应该如何处理?

手动引入

传统的手动打补丁方案虽然低效,但直观有用。有些非常在乎性能的场景,比如我们公司的部分无线 H5 业务,他们宁可牺牲效率也要追求性能。所以他们的补丁方案是手动引入 core-js/modules 下的文件,缺啥加啥就好。

注意:

  1. core-js 目前用的是 v2 版本,不是 v3-beta
  2. 补丁用的是 core-js/modules,而不是 core-js/library。为啥?二者又有啥区别呢?

在线补丁,比如:polyfill.io

前面的手动引入解决的是特性列表的问题,有了特性列表,要做到按需下载,就需要用到在线的补丁服务了。目前最流行的应该就是 polyfill.io,提供的是 cdn 服务,有些站点在用,例如 https://spectrum.chat/。另外,polyfill.io 还开源了 polyfill-service 供我们自己搭建使用。

使用上,比如:

<script src="https://polyfill.io/v3/polyfill.min.js?features=default%2CPromise"></script>

然后在 Chrome@71 下的输出是:

/* Disable minification (remove `.min` from URL path) for more info */

啥都没有,因为 Promsie 特性 Chrome@71 已经支持了。

未来

关于补丁方案的未来,我觉得按需特性探测 + 在线补丁才是终极方案。

按需特性探测保证特性的最小集;在线补丁做按需下载。

按需特性探测可以用 @babel/preset-env 配上 targets 以及试验阶段的 useBuiltIns: usage,保障特性集的最小化。之所以说是未来,因为 JavaScript 的动态性,语法探测不太可能探测出所有特性,但上了 TypeScript 之后可能会好一些。另外,要注意一个前提是 node_modules 也需要走 babel 编译,不然 node_modules 下用到的特性会探测不出来。

在线补丁可以用类似前面介绍的 https://polyfill.io/ 提供的方案,让浏览器只下载必要的补丁,通常大公司用的话会部署一份到自己的 cdn 上。(阿里好像有团队部署了,但一时间想不起地址了。)

FAQ

组件应该包含补丁吗?比如 dva 里用了 Promise,是否应该把 Promise 打在 dva 的产出里?

不应该。 比如项目了依赖了 a 和 b,a 和 b 都包含 Promise 的补丁,就会有冗余。所以组件不应该包含补丁,补丁应该由项目决定。

组件不包含补丁?那需要处理啥?

通常不需要做特殊处理,但是有些语言特性的实现会需要引入额外的 helper 方法。

比如:

console.log({ ...a });

编译后是:

function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i] != null ? arguments[i] : {}; var ownKeys = Object.keys(source); if (typeof Object.getOwnPropertySymbols === 'function') { ownKeys = ownKeys.concat(Object.getOwnPropertySymbols(source).filter(function (sym) { return Object.getOwnPropertyDescriptor(source, sym).enumerable; })); } ownKeys.forEach(function (key) { _defineProperty(target, key, source[key]); }); } return target; }

function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }

console.log(_objectSpread({}, a));

然后我们会有很多文件,每个文件都引入一遍 helper 方法,会有很多冗余。所以我们通常会使用 @babel/plugin-transform-runtime 来复用这些 helper 方法。

.babelrc 里配置:

{
  "plugins": [
    "@babel/transform-runtime"
  ]
}

编译后是:

var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
var _objectSpread2 = _interopRequireDefault(require("@babel/runtime/helpers/objectSpread"));

console.log((0, _objectSpread2.default)({}, a));

所以,组件编译只要确保没有冗余的 helper 方法就好了。

core-js/library or core-js/modules?

core-js 提供了两种补丁方式。

  1. core-js/library,通过 helper 方法的方式提供
  2. core-js/module,通过覆盖全局变量的方式提供

举个例子,

import '@babel/polyfill';
Promise.resolve('foo');

.babelrc 配:

{
  "presets": [
    ["@babel/env", {
    	"useBuiltIns": "entry",
      "targets": {
        "ie": 9
      }
    }]
  ]
}

编译结果是:

require("core-js/modules/es6.promise");
require("core-js/modules/es7.promise.finally");
// 此处省略数十个其他补丁...

Promise.resolve('foo');

然后把文件内容换成:

// import '@babel/polyfill';
Promise.resolve('foo');

.babelrc 配:

{
  "plugins": [
    ["@babel/transform-runtime", {
      "corejs": 2
    }]
  ]
}

编译结果是:

var _interopRequireDefault = require("@babel/runtime-corejs2/helpers/interopRequireDefault");
var _promise = _interopRequireDefault(require("@babel/runtime-corejs2/core-js/promise"));

_promise.default.resolve('foo');

然后 @babel/runtime-corejs2/core-js/promise 的内容是:

module.exports = require("core-js/library/fn/promise");

目前推荐是用 core-js/modules,因为 node_modules 不走 babel 编译,所以 core-js/library 的方式无法为依赖库提供补丁。

非 core-js 里的特性,如何打补丁?

手动引入,比如 Intl.js、URL 等。但是得小心有些规范后续加入 ecmascript 之后可能的冗余,比如 URL

参考

@ql434

This comment has been minimized.

Copy link

ql434 commented Jan 29, 2019

mark

@lovemegowin lovemegowin referenced this issue Jan 29, 2019

Open

read list #4

@leftstick

This comment has been minimized.

Copy link

leftstick commented Jan 29, 2019

脸上写满了认真,赞

@shaozj

This comment has been minimized.

Copy link

shaozj commented Jan 29, 2019

如何兼顾开发效率和性能,是每个开发需要思考以及持续优化的地方

@guozimo

This comment has been minimized.

Copy link

guozimo commented Jan 29, 2019

真棒 云谦师兄

@maxmeng93

This comment has been minimized.

Copy link

maxmeng93 commented Jan 29, 2019

👍

@snoopy1412

This comment has been minimized.

Copy link

snoopy1412 commented Jan 29, 2019

mark cdn那个有点意思,。,

@Lstoryc

This comment has been minimized.

Copy link

Lstoryc commented Jan 29, 2019

mark

@thjiang

This comment has been minimized.

Copy link

thjiang commented Jan 29, 2019

目前看useBuiltIns是不是用usage更合适一点?

@we125182

This comment has been minimized.

Copy link

we125182 commented Jan 30, 2019

受益良多!

@jamieYou

This comment has been minimized.

Copy link

jamieYou commented Jan 30, 2019

polyfill.io 这块想试一下,但是发现微信浏览器下访问 https://polyfill.io/v3/polyfill.js,代码特别多。
而用了另外一个 cdn https://cdn.polyfill.io/v2/polyfill.min.js,却显示识别不了浏览器的名字和版本,导致没有代码。应该是这个库不支持识别微信浏览器吧

@sleagon

This comment has been minimized.

Copy link

sleagon commented Jan 31, 2019

@sorrycc @jamieYou 你说的服务应该是(CBU)这边做的auto polyfill服务,用了他们的library,上层实现不太一样,主要是从性能和可靠性上有些考量。已经在多个BU落地了哈~~~ 具体链接是:https://polyfill.alicdn.com/polyfill.min.js?features=Promise 现在覆盖了阿里系的绝大部分webview,以及chrome/safari/firefox/IE/EDGE这些,至于微信的webview还不支持,因为这一块需求不是很强烈,就没有去做,一旦有诉求我们会去加进来的~~ 花名:亦昼

@sorrycc

This comment has been minimized.

Copy link
Owner Author

sorrycc commented Feb 1, 2019

@sleagon 一个项目用到哪些 features 是怎么收集的?

@sleagon

This comment has been minimized.

Copy link

sleagon commented Feb 1, 2019

@sleagon 一个项目用到哪些 features 是怎么收集的?

我们没有收集开发者用到了什么features,这个暂时还没想到什么好的方案来自动采集,因为一方面现在不少页面是以区块为维度开发的,自动采集不太现实,另一方面跟你说的那样,node_modules里的polyfill容易被漏掉。目前还是靠开发者手动去添加features列表,我们也提供了类似polyfill.alicdn.com/modern/polyfill.min.js 这样的默认带了es6/es7的比较全的集合,以及只带了基本的polyfill的链接。

@laozhu

This comment has been minimized.

Copy link

laozhu commented Feb 1, 2019

什么时候 umijs 实现啊,期待ing

@jsw0528

This comment has been minimized.

Copy link

jsw0528 commented Feb 16, 2019

维护个 polyfill.io 服务的成本也不小,又想追求性能,又想提效,可以折中下,根据业务场景分个类,允许少量冗余,这样就不会出现 chrome 下也加载给 ie 打得补丁了,也不用挨个打补丁

@sleagon

This comment has been minimized.

Copy link

sleagon commented Feb 18, 2019

维护个 polyfill.io 服务的成本也不小,又想追求性能,又想提效,可以折中下,根据业务场景分个类,允许少量冗余,这样就不会出现 chrome 下也加载给 ie 打得补丁了,也不用挨个打补丁

对的,这也是我们搞类似polyfill.alicdn.com/default/polyfill.min.js和polyfill.alicdn.com/modern/polyfill.min.js这类链接的原因,一个提供了基本支持,一个提供了es6/es7之类的,默认集合,免得每个人都去配一串链接

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment