Skip to content
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 · 28 comments
Open

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

sorrycc opened this issue Jan 29, 2019 · 28 comments

Comments

@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
Copy link

ql434 commented Jan 29, 2019

mark

@leftstick
Copy link

脸上写满了认真,赞

@shaozj
Copy link

shaozj commented Jan 29, 2019

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

@guozimo
Copy link

guozimo commented Jan 29, 2019

真棒 云谦师兄

@maxmeng93
Copy link

👍

@snoopy1412
Copy link

mark cdn那个有点意思,。,

@lstoryc
Copy link

lstoryc commented Jan 29, 2019

mark

@thjiang
Copy link

thjiang commented Jan 29, 2019

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

@we125182
Copy link

受益良多!

@jamieYou
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
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
Copy link
Owner Author

sorrycc commented Feb 1, 2019

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

@sleagon
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
Copy link

laozhu commented Feb 1, 2019

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

@antife-yinyue
Copy link

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

@sleagon
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之类的,默认集合,免得每个人都去配一串链接

@sl1673495
Copy link

赞 present-env + useBuiltIns: 'usage' 应该是不错的选择, 最近正好也在看这个。
babel7的配置与优化

@HaveF
Copy link

HaveF commented Mar 19, 2019

@sleagon ali的这个polyfill现在算是公开的服务吗? 还是只是你们内部使用的? 谢谢

@edokeh
Copy link

edokeh commented Mar 21, 2019

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

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

更新下 corejs@3 的方式

  • core-js/features/set 通过覆盖全局变量的方式提供
  • import Set from "core-js-pure/features/set"; 通过 helper 方式提示,不污染全局

@sorrycc sorrycc pinned this issue Mar 21, 2019
@sleagon
Copy link

sleagon commented Mar 25, 2019

@HaveF 目前没做限制,不过目前更多地还是针对阿里系和几个大的浏览器做了匹配,你基本可以按照目前开源社区的用法来用都是OK的,后面也有考虑直接放出去用。

@sl1673495 现在babel这种usage的标识支持的能力还是有限的,如果你用了Usage,是不会注入类似fetch这种浏览器api性质的polyfill的,它只会处理es标准的polyfill,还是需要你去手动处理其他的polyfill的。

@hhking
Copy link

hhking commented Apr 1, 2019

赞!看了好几遍

@hnsylitao
Copy link

polyfill.io 服务端的成本太大,如果用第三方提供的服务,不敢保证。

其实 @babel/preset-env + useBuiltins: entry + targets 感觉已经解决很多问题了。

当然如果成本允许 polyfill.io 也挺好

@DoubleBlock
Copy link

好文 收藏先

@ImJoeHs
Copy link

ImJoeHs commented Aug 6, 2019

性列表是按浏览器整理的,那怎么知道哪些特性我用了,哪些没有用到,没有用到的部分也引入了是不是也是冗余?@babel/preset-env 有提供 exclude 的配置,如果我配置了 exclude,后面是否得小心翼翼地确保不要用到 exclude 掉的特性

有没有好用的 lint 工具,来禁止使用 无法被 babel transform / 配置了 exclude 的语法呢?

@liSong5713
Copy link

赞 云谦 大佬

@CommanderXL
Copy link

文章里面有提到关于node_modules下面的package也需要走编译的流程,这块的内容是如何实践的?通过 webpack plugin 进行 babel-loader 的匹配和加入的工作么?

@liaoliaojun
Copy link

mark

1 similar comment
@TZZack
Copy link

TZZack commented Nov 1, 2022

mark

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests