你不知道的core-js、polyfill和babel-polyfill

1,777 阅读6分钟

前言:在前端项目开发和调试的过程中,总是在不经意间瞥见这个"core-js",终于有一天忍不住查找资料一探究竟...

开篇问题: core-js和babel在前端工程化中是如何紧密集成在一起的?

core-js是什么?

core-js是一个js标准库,一个以实现polyfill/垫片为核心目的的现代化前端项目的"标准套件". core-js 是一个由 Lerna 搭建的 Monorepo 风格的项目,在它的 packages 中,我们能看到五个相关包: core-js,core-js-pure,core-js-compact,core-js-builder,core-js-bundle.

每个包都有什么作用?

core-js包:实现基础polyfill功能,也是整个core-js的核心。
比如我们可以按照如下代码引入全局 polyfills:
import 'core-js';

或者按照:

import 'core-js/features/array/from';

的方式,按需在业务项目的入口引入某些 polyfills。

core-js-pure提供了不污染全局变量的能力:

比如我们可以按照:

import _from from 'core-js-pure/features/array/from';
import _flat from 'core-js-pure/features/array/flat';

的方式来独立导出命名空间,避免污染全局变量。

core-js-compact 维护了按照browserslist规范的垫片需求数据,我们可以按需获取符合我们需要的polyfill需求集合,比如以下代码:

const {
  list, // array of required modules
  targets, // object with targets for each module
} = require('core-js-compat')({
  targets: '> 2.5%'
});

可以筛选出全球使用份额大于2.5%的浏览器,包括chrome、safari等需要支持的polyfill能力

core-js-builder可以利用core-js-compact,基于webpack,按需打包core-js代码。比如:

require('core-js-builder')({
  targets: '> 0.5%',
  filename: './my-core-js-bundle.js',
}).then(code => {}).catch(error => {});

将会把符合需求的 core-js 垫片打包到my-core-js-bundle.js文件当中。整个流程可以用代码演示为:

require('./packages/core-js-builder')({ filename: './packages/core-js-bundle/index.js' }).then(done).catch(error => {
  // eslint-disable-next-line no-console
  console.error(error);
  process.exit(1);
});

可以看得出来,core-js的各个包各有各的作用,可以单独用也可以结合一起用。

实际上,core-js在前端工程领域有着广泛的作用,比如: core-js-compact 可以被 Babel 生态使用,由 Babel 分析出根据环境需要按需加载的垫片; core-js-builder 可以被 Node.js 服务使用,构建出不同场景的垫片包。

如何复用一个 Polyfill 实现

Array.prototype.every 是一个常见且常用的数组原型上的方法。该方法用于测试一个数组内所有元素是否都能通过某个指定函数的测试,并最终返回一个布尔值来表示测试是否通过。它的浏览器兼容性如下图所示:

1.png

手动实现

我们可以根据mdn上的every的特性自己简单实现一个polyfill,大概的思路是遍历数组,让数组的每一项执行下回调方法,返回一个值表明是否通过测试。下面是 MDN 的一个实现:


if (!Array.prototype.every) {
  Array.prototype.every = function(callbackfn, thisArg) {
    'use strict';
    var T, k;
    if (this == null) {
      throw new TypeError('this is null or not defined');
    }
    var O = Object(this);
    var len = O.length >>> 0;
    if (typeof callbackfn !== 'function') {
      throw new TypeError();
    }
    if (arguments.length > 1) {
      T = thisArg;
    }
    k = 0;
    while (k < len) {
      var kValue;
      if (k in O) {
        kValue = O[k];
        var testResult = callbackfn.call(T, kValue, k, O);
        if (!testResult) {
          return false;
        }
      }
      k++;
    }
    return true;
  };
}

工程化实现

需要core-js-pure和core-js分别引用一个基础的polyfill方法(上面只是举例)。如果可以污染全局变量例如Array.prototype.every的,就使用core-js,否则使用core-js-pure. 实际上就是一个导出方式的区分。

这里看下core-js中Array.prototype.every 的 polyfill的核心实现,在./packages/core-js/modules/es.array.every.js中,源码如下:

1.png

最终调用了基础polyfill方法$every,

return $every(this, callbackfn, arguments.length > 1 ? arguments[1] : undefined);

这里的this指的是调用every方法的数组实例对象,最后一个参数指的是callbackfn中的上下文语境this。第一个参数

{target: 'Array', proto: true}

的意思是说core-js需要在数组Array的原型上复写every方法

对应$every源码

1.png

2.png

核心是通过闭包和魔法常量来实现的,此外利用了拥有this绑定能力的bind, 如下,

var bind = require('../internals/function-bind-context');
var boundFunction = bind(callbackfn, that);

重点是core-js和core-js-pure实现了对原型方法的不同导出(是否污染全局变量every)。core-js的源码如下:

1.png

core-pure-js的源码如下:

1.png

2.png

而core-js-pure的核心功能是通过在构建阶段复写core-js的核心逻辑来实现的。

polyfill是什么?

简单来说,plolyfill就是利用社区上提供的一段代码,让我们在不兼容某些新特性的浏览器上也能使用新特性。

babel-polyfill是什么?

最佳的polyfill方案:侵入性最小,业务影响最低,工程化程度最高。

存在的问题

如果粗暴的使用babel-polyfill将一次性把全量的polyfill导入到项目中,增加额外的内存消耗,增加污染全局变量的风险。

解决方案

于是, babel-polyfill结合@babel/preset-env+useBuiltins(value为entry)+preset-env targets方案诞生了

@babel/preset-env 定义了 Babel 所需预设插件,同时由 Babel 根据 preset-env targets 配置的支持环境,自动按需加载 polyfills,

,使用方式如下:

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

上述代码的意思是说需要在工程入口文件 import '@babel/polyfill' 来实现对指定target的polyfill的注入。会被编译成如下形式:

会被编译为:

import "core-js/XXXX/XXXX";
import "core-js/XXXX/XXXXX";

实际上,babel-polyfill是融合了core-js和regenerator-runtime做的一个实现,底层还是core-js,让core-js插上了babel的翅膀,更好用了.

潜在的问题

如果在某个业务代码中并没有用到配置环境注入的polyfill,不是让代码更加臃肿吗?虽然环境需要,但业务上并不一定用得到啊!这就出现了另一个解决方案 @babel/preset-env + useBuiltins(usage) + preset-env targets

解决方案

useBuiltins的value被设置成usage, 则可以根据代码情况分析AST/抽象语法树来实现更细颗粒度的按需引入, 整个过程是一个静态编译的过程。

另一种便捷的实现方式

上面说的babel-polyfill方案本质上是在构建/打包阶段注入polyfill来实现的,还有一种实现是“在线动态打补丁”,这种方案以 Polyfill.io 为代表,它提供了 CDN 服务,使用者可以按照所需环境,生成打包链接

1.png

polyfill.io/v3/polyfill… polyfills bundle:

<script src="https://polyfill.io/v3/polyfill.min.js?features=es2015"></script>

在高版本浏览器上,可能会返回空内容,因为该浏览器已经支持了 ES2015 特性。如果在低版本浏览器上,将会得到真实的 polyfills bundle

开篇问题的解答

babel和core-js是通过babel-polyfill而紧密集成在一起的(因为babel-polyfill底层是融合了core-js来实现的)。

总结

从工程化的角度来说,最佳的polyfill设计应该实现按需注入,因为这样打包的bundle体积最小,性能最优。 这就需要在用户环境和业务代码两个层面都需要用到则注入进来/打补丁。

希望对你有所启发或帮助!