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 ,而我們又需要在專案裡用到,寫了這樣的程式碼:
<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-shim 和 es6-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
,並設定 useBuiltIns
和 targets
,
{ "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 之類的都不會再打包引入。
是不是很好用?
:smile:
有什麼問題?
細細想想,其實還有不少問題,
- 特性列表是按瀏覽器整理的,那怎麼知道哪些特性我用了,哪些沒有用到,沒有用到的部分也引入了是不是也是冗餘?
@babel/preset-env
有提供 exclude 的配置,如果我配置了 exclude,後面是否得小心翼翼地確保不要用到 exclude 掉的特性 - 補丁是打包到靜態檔案的,如果我配置 targets 為
chrome: 62, ie: 9
,那意味著 chrome 62 也得載入 ie 9 相關的補丁,這也是一份冗餘 - 我們是基於 core-js 打的補丁,所以只會包含 ecmascript 規範裡的內容,其他比如說 dom 裡的補丁,就不在此列,應該如何處理?
手動引入
傳統的手動打補丁方案雖然低效,但直觀有用。有些非常在乎效能的場景,比如我們公司的部分無線 H5 業務,他們寧可犧牲效率也要追求效能。所以他們的補丁方案是手動引入 core-js/modules 下的檔案,缺啥加啥就好。
注意:
- core-js 目前用的是 v2 版本,不是 v3-beta
- 補丁用的是 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 提供了兩種補丁方式。
core-js/library 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 。