原文地址: github.com/yinxin630/b…
技术交流群: fiora.suisuijiang.com/
先聊下 babel 与 polyfill
ES2015 标准已经发布三年了, 在项目中我们会写 ES2015(或者更高版本) 的代码, 但是代码最终运行的环境(浏览器)通常是不可控的, 因此需要将 ES2015 编译为低版本代码, 来保证所有目标环境可运行
babel 就是用来将高版本编译为低版本的工具, 在不配置额外插件的情况下, babel 仅仅是将 ES2015 的语法(例如for of
)转换, 而 ES2015 新增的类/方法(例如Set
或者 [1, 2].findIndex()
)会保持原样
这时候就需要 polyfill 了, 需要在项目入口文件最开头引入@babel/polyfill
. 但是在项目中, 通常仅用到了有限的 polyfill 内容, 而最新版的@babel/polyfill
包体积有 81.2k(gzipped 27.7k) 大小
那么可不可以只 polyfill 代码中用到的内容呢?
假设有如下源码:
const set = new Set(); // ES6 Set
set.add(1);
set.add(2);
set.add(3);
const arr = [1, 2, 3]; // ES6 for..of
for (const a of arr) {
console.log(a);
}
console.log(arr.findIndex(x => x === 2)); // ES6 Array.prototype.findIndex
复制代码
接下来试试不同的 polyfill 方案
@babel/plugin-transform-runtime
首先是使用 transfrom-runtime 这个插件, 它可以仅对代码中用到的类/静态方法进行 polyfill, 但是对于原型链上新增的方法无效
NOTE: Instance methods such as "foobar".includes("foo") will not work since that would require modification of existing built-ins (you can use @babel/polyfill for that).
添加 babel 配置
// babel 配置
{
"presets": [
[
"@babel/preset-env",
]
],
"plugins": [
[
"@babel/plugin-transform-runtime",
{
"corejs": 2,
"helpers": true,
"regenerator": true,
"useESModules": false
}
]
]
}
复制代码
编译后:
"use strict";
var _interopRequireDefault = require("@babel/runtime-corejs2/helpers/interopRequireDefault");
var _set = _interopRequireDefault(require("@babel/runtime-corejs2/core-js/set"));
var set = new _set.default();
set.add(1);
set.add(2);
set.add(3);
var arr = [1, 2, 3];
for (var _i = 0; _i < arr.length; _i++) {
var a = arr[_i];
console.log(a);
}
console.log(arr.findIndex(function (x) {
return x === 2;
}));
复制代码
编译后的代码仅仅引入了 Set 实现, 但是 findIndex()
没有 polyfill 如果你确定不会使用任何原型链上新增的方法, 那么 @babel/plugin-transform-runtime
会是一个不错的选择
@babel/preset-env + useBuiltIns
@babel/preset-env
支持你配置目标环境, 它的 useBuiltIns
选项, 有三个可选值 "usage" | "entry" | false(默认值)
This option adds direct references to the core-js module as bare imports. Thus core-js will be resolved relative to the file itself and needs to be accessible. You may need to specify core-js@2 as a top level dependency in your application if there isn't a core-js dependency or there are multiple versions.
useBuiltIns: 'entry'
该选项需要在项目中引入 @babel/polyfill
, babel 会自动将 @babel/polyfill
分解为更小的、仅目标环境需要的 polyfill 引用
NOTE: Only use require("@babel/polyfill"); once in your whole app. Multiple imports or requires of @babel/polyfill will throw an error since it can cause global collisions and other issues that are hard to trace. We recommend creating a single entry file that only contains the require statement.
首先要在源码第一行添加 polyfill 引用
import '@babel/polyfill'
复制代码
修改 babel 配置
// babel 配置
{
"presets": [
[
"@babel/preset-env",
{
"targets": "Chrome 40",
"useBuiltIns": "entry"
}
]
]
}
复制代码
编译后:
"use strict";
require("core-js/modules/es6.array.copy-within");
require("core-js/modules/es6.array.fill");
require("core-js/modules/es6.array.find");
require("core-js/modules/es6.array.find-index");
require("core-js/modules/es6.array.from");
require("core-js/modules/es7.array.includes");
require("core-js/modules/es6.array.of");
require("core-js/modules/es6.array.sort");
require("core-js/modules/es6.array.species");
require("core-js/modules/es6.date.to-primitive");
require("core-js/modules/es6.function.has-instance");
require("core-js/modules/es6.map");
require("core-js/modules/es6.number.constructor");
require("core-js/modules/es6.object.assign");
require("core-js/modules/es7.object.define-getter");
require("core-js/modules/es7.object.define-setter");
require("core-js/modules/es7.object.entries");
require("core-js/modules/es6.object.freeze");
require("core-js/modules/es6.object.get-own-property-descriptor");
require("core-js/modules/es7.object.get-own-property-descriptors");
require("core-js/modules/es6.object.get-prototype-of");
require("core-js/modules/es7.object.lookup-getter");
require("core-js/modules/es7.object.lookup-setter");
require("core-js/modules/es6.object.prevent-extensions");
require("core-js/modules/es6.object.is-frozen");
require("core-js/modules/es6.object.is-sealed");
require("core-js/modules/es6.object.is-extensible");
require("core-js/modules/es6.object.seal");
require("core-js/modules/es7.object.values");
require("core-js/modules/es6.promise");
require("core-js/modules/es7.promise.finally");
require("core-js/modules/es6.reflect.apply");
require("core-js/modules/es6.reflect.construct");
require("core-js/modules/es6.reflect.define-property");
require("core-js/modules/es6.reflect.delete-property");
require("core-js/modules/es6.reflect.get");
require("core-js/modules/es6.reflect.get-own-property-descriptor");
require("core-js/modules/es6.reflect.get-prototype-of");
require("core-js/modules/es6.reflect.has");
require("core-js/modules/es6.reflect.is-extensible");
require("core-js/modules/es6.reflect.own-keys");
require("core-js/modules/es6.reflect.prevent-extensions");
require("core-js/modules/es6.reflect.set");
require("core-js/modules/es6.reflect.set-prototype-of");
require("core-js/modules/es6.regexp.constructor");
require("core-js/modules/es6.regexp.flags");
require("core-js/modules/es6.regexp.match");
require("core-js/modules/es6.regexp.replace");
require("core-js/modules/es6.regexp.split");
require("core-js/modules/es6.regexp.search");
require("core-js/modules/es6.regexp.to-string");
require("core-js/modules/es6.set");
require("core-js/modules/es6.symbol");
require("core-js/modules/es7.symbol.async-iterator");
require("core-js/modules/es6.string.code-point-at");
require("core-js/modules/es6.string.ends-with");
require("core-js/modules/es6.string.from-code-point");
require("core-js/modules/es6.string.includes");
require("core-js/modules/es7.string.pad-start");
require("core-js/modules/es7.string.pad-end");
require("core-js/modules/es6.string.raw");
require("core-js/modules/es6.string.repeat");
require("core-js/modules/es6.string.starts-with");
require("core-js/modules/es6.typed.array-buffer");
require("core-js/modules/es6.typed.int8-array");
require("core-js/modules/es6.typed.uint8-array");
require("core-js/modules/es6.typed.uint8-clamped-array");
require("core-js/modules/es6.typed.int16-array");
require("core-js/modules/es6.typed.uint16-array");
require("core-js/modules/es6.typed.int32-array");
require("core-js/modules/es6.typed.uint32-array");
require("core-js/modules/es6.typed.float32-array");
require("core-js/modules/es6.typed.float64-array");
require("core-js/modules/es6.weak-map");
require("core-js/modules/es6.weak-set");
require("core-js/modules/web.timers");
require("core-js/modules/web.immediate");
require("core-js/modules/web.dom.iterable");
require("regenerator-runtime/runtime");
var set = new Set();
set.add(1);
set.add(2);
set.add(3);
var arr = [1, 2, 3];
for (var _i = 0; _i < arr.length; _i++) {
var a = arr[_i];
console.log(a);
}
console.log(arr.findIndex(function (x) {
return x === 2;
}));
复制代码
编译后的代码自动引入的 Chrome 40 不支持的所有内容, 包括 Set
和 findIndex()
, 它并不会去分析源码用到的哪些内容
尝试修改 targets 为 Chrome 60, 编译后:
"use strict";
require("core-js/modules/es6.array.sort");
require("core-js/modules/es7.object.define-getter");
require("core-js/modules/es7.object.define-setter");
require("core-js/modules/es7.object.lookup-getter");
require("core-js/modules/es7.object.lookup-setter");
require("core-js/modules/es7.promise.finally");
require("core-js/modules/es7.symbol.async-iterator");
require("core-js/modules/web.timers");
require("core-js/modules/web.immediate");
require("core-js/modules/web.dom.iterable");
const set = new Set();
set.add(1);
set.add(2);
set.add(3);
const arr = [1, 2, 3];
for (const a of arr) {
console.log(a);
}
console.log(arr.findIndex(x => x === 2));
复制代码
由于 Chrome 60 已经支持 Set
和 findIndex()
了, 因此 polyfill 的内容并不包括它俩
useBuiltIns: 'usage'
该选项目前还是实验性的, 我们来试试它打包后是怎样的
首先要去掉源码中的 import '@babel/polyfill'
修改 babel 配置:
{
"presets": [
[
"@babel/preset-env",
{
"targets": "Chrome 40",
"useBuiltIns": "usage"
}
]
]
}
复制代码
编译后:
"use strict";
require("core-js/modules/es6.array.find-index");
require("core-js/modules/web.dom.iterable");
require("core-js/modules/es6.set");
var set = new Set();
set.add(1);
set.add(2);
set.add(3);
var arr = [1, 2, 3];
for (var _i = 0; _i < arr.length; _i++) {
var a = arr[_i];
console.log(a);
}
console.log(arr.findIndex(function (x) {
return x === 2;
}));
复制代码
哇, 看起来这似乎就是我需要的! 但它是分析到我使用了 Array.protoptype.findIndex()
才添加 polyfill 的吗? 来做个试验看看
修改源码:
String.prototype.findIndex = function() {}
const str = '';
str.findIndex(); // 调用 String.prototype.findIndex
复制代码
这次我没有去调用 Array 原型链的 findIndex, 而且调用了自己实现的 String 原型链的 findIndex
编译后:
"use strict";
require("core-js/modules/es6.array.find-index");
String.prototype.findIndex = function () {};
var str = '';
str.findIndex();
复制代码
?原来它是直接匹配的方法名, 添加了同名的 polyfill
useBuiltIns 结论
useBuiltIns: 'entry'
是按目标环境去 polyfill 的, 不关心代码中是否使用, 可以保证在目标环境一定可用
useBuiltIns: 'usage'
目前还是实验性的配置, 它会分析代码调用, 但是对于原型链上的方法仅仅按照方法名去匹配, 可以得到更小的 polyfill 体积. 但是它不会去分析代码依赖的 npm 包的内容, 如果某个 npm 包是需要一些 polyfill 的, 那这些 polyfill 并不会被打包进去
为什么原型链上的方法不能根据是否用到, 然后按需去 polyfill 呢?
主要是因为 JavaScript 动态类型的特性, 有些变量/实例的类型是运行时才能确定的, 而 babel 仅仅是对代码的静态编译, 因此它并不能确定 findIndex()
到底是不是 Array.protoptype.findIndex()
, 例如:
fetch('/api')
.then(res => res.json())
.then(data => data.findIndex)
复制代码
data 的类型由运行时接口返回的内容决定, 所以 babel 不能实现原型链方法按需 polyfill
TypeScript 具备静态类型, 可以按需 polyfill 吗?
结论是不能! 关于 polyfill 的讨论可以看看 github.com/Microsoft/T…
TypeScript 可以用 --lib
参数指定要依赖的库, 搭配 ts-polyfill
可以对依赖的库进行 polyfill, 但是指定依赖时不能详细到某个方法, 只能 ESNext.Array
如果非要只 polyfill Set
和 findIndex
呢?
可以手动引入 core-js
中相应的实现, 譬如:
import 'core-js/modules/es6.set.js';
import 'core-js/modules/es6.array.find-index.js';
复制代码
不推荐这种做法, 除非追求最小的 polyfill 大小, 你必须清楚的知道项目中用到了哪些内容. 但在实际项目中, 尤其多人开发的项目, 通常很难去控制