Polyfill 方案的過去、如今和將來

做者:sorryccjavascript

日期:2019-01-29 01:31:04html

原文地址: github.com/sorrycc/blo…前端

blog: gitissue.com/issues/5c4f…java

任何一個小知識點,深挖下去,也是很是有意思的。node

什麼是補丁?

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.git

咱們但願瀏覽器提供一些特性,可是沒有,而後咱們本身寫一段代碼來實現他,那這段代碼就是補丁。es6

好比 IE11 不支持 Promise,而咱們又須要在項目裏用到,寫了這樣的代碼:github

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

這時在 IE 下運行就會報錯了,ajax

img

而後在此以前加上補丁,chrome

<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>
複製代碼

刷新瀏覽器,就能夠正常運行了,

img

過去

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>
複製代碼

而後就 es六、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 服務,有些站點在用,例如 spectrum.chat/。另外,polyfil… 還開源了 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 下用到的特性會探測不出來。

在線補丁能夠用相似前面介紹的 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

參考

關注公衆號,發現更多精彩內容。

相關文章
相關標籤/搜索