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

babel能不能分析代码然后按需polyfill ? #5

Open
yinxin630 opened this issue Dec 6, 2018 · 2 comments
Open

babel能不能分析代码然后按需polyfill ? #5

yinxin630 opened this issue Dec 6, 2018 · 2 comments

Comments

@yinxin630
Copy link
Owner

yinxin630 commented Dec 6, 2018

先聊下 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

https://babeljs.io/docs/en/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

https://babeljs.io/docs/en/babel-preset-env

@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 不支持的所有内容, 包括 SetfindIndex(), 它并不会去分析源码用到的哪些内容

尝试修改 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 已经支持 SetfindIndex() 了, 因此 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 的讨论可以看看 microsoft/TypeScript#3101

TypeScript 可以用 --lib 参数指定要依赖的库, 搭配 ts-polyfill 可以对依赖的库进行 polyfill, 但是指定依赖时不能详细到某个方法, 只能 ESNext.Array

如果非要只 polyfill SetfindIndex 呢?

可以手动引入 core-js 中相应的实现, 譬如:

import 'core-js/modules/es6.set.js';
import 'core-js/modules/es6.array.find-index.js';

不推荐这种做法, 除非追求最小的 polyfill 大小, 你必须清楚的知道项目中用到了哪些内容. 但在实际项目中, 尤其多人开发的项目, 通常很难去控制

@yinxin630 yinxin630 changed the title babel能不能自动分析代码, 只对用到的内容引入polyfill ? babel能不能分析代码然后按需polyfill ? Dec 6, 2018
@hopepdm
Copy link

hopepdm commented Dec 7, 2018

有这个功夫,感觉不如自己写一个内部polyfill

@marsprince
Copy link

marsprince commented Dec 7, 2018

我觉得按浏览器是比较科学的,按需无法控制node_modules里的第三方代码

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

No branches or pull requests

3 participants