Skip to content

Commit

Permalink
add "Polyfill 方案过去、现在和未来"
Browse files Browse the repository at this point in the history
  • Loading branch information
sorrycc committed Jan 29, 2019
1 parent 3d646f4 commit f9c1795
Show file tree
Hide file tree
Showing 2 changed files with 291 additions and 1 deletion.
288 changes: 288 additions & 0 deletions 2019/2019-01-28-Polyfill 方案的过去、现在和未来.md
@@ -0,0 +1,288 @@
# Polyfill 方案过去、现在和未来

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

## 什么是补丁?

> 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](https://caniuse.com/#feat=promises),而我们又需要在项目里用到,写了这样的代码:

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

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

![](https://camo.githubusercontent.com/348c3453fc18218b9a60e6eb67821c83e1ec8bdf/68747470733a2f2f67772e616c697061796f626a656374732e636f6d2f7a6f732f726d73706f7274616c2f6b766d4e755778747758636e764f574f6c6f4d752e706e67)

然后在此之前加上补丁,

```html
<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>
```

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

![](https://camo.githubusercontent.com/d8aae6399751c7cc33c634397162e0d3dfd3f31e/68747470733a2f2f67772e616c697061796f626a656374732e636f6d2f7a6f732f726d73706f7274616c2f6c6b7a4e70546878667a7565506a51736f586a792e706e67)

## 过去

### shim + sham

如果你是一个 3 年陈 + 的前端,应该会有听说过 shim、sham、[es5-shim](https://github.com/es-shims/es5-shim)[es6-shim](https://github.com/es-shims/es6-shim) 等等现在看起来很古老的补丁方式。

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

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

### babel-polyfill.js

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

```html
<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`

```js
import '@babel/polyfill';
```

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

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

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

替换后的内容,比如:

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

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

> 是不是很好用?
😄

> 有什么问题?
🤔

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

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

### 手动引入

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

注意:

1. core-js 目前用的是 v2 版本,不是 v3-beta
2. 补丁用的是 core-js/modules,而不是 [core-js/library](https://github.com/zloirock/core-js/tree/v2/library)。为啥?二者又有啥区别呢?

### 在线补丁,比如:polyfill.io

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

使用上,比如:

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

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

```js
/* 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 方法。

比如:

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

编译后是:

```js
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](https://babeljs.io/docs/en/next/babel-plugin-transform-runtime.html) 来复用这些 helper 方法。

`.babelrc` 里配置:

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

编译后是:

```js
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`,通过覆盖全局变量的方式提供

举个例子,

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

`.babelrc` 配:

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

编译结果是:

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

Promise.resolve('foo');
```

然后把文件内容换成:

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

`.babelrc` 配:

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

编译结果是:

```js
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` 的内容是:

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

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

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

手动引入,比如 [Intl.js](https://github.com/andyearnshaw/Intl.js/)、URL 等。但是得小心有些规范后续加入 ecmascript 之后可能的冗余,比如 [URL](https://github.com/zloirock/core-js/pull/454)

## 参考

* <http://2ality.com/2011/12/shim-vs-polyfill.html>
* https://babeljs.io/docs/en/next/babel-preset-env.html
* https://github.com/zloirock/core-js/tree/v2

4 changes: 3 additions & 1 deletion README.md
@@ -1,2 +1,4 @@
# Blog
# blog

personal blog repository.

0 comments on commit f9c1795

Please sign in to comment.