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指南 #107

Open
willson-wang opened this issue Sep 3, 2022 · 1 comment
Open

babel polyfill指南 #107

willson-wang opened this issue Sep 3, 2022 · 1 comment

Comments

@willson-wang
Copy link
Owner

willson-wang commented Sep 3, 2022

语雀地址

目录

背景

在公司内部对项目进行构建大小优化的时候,发现构建产物,包括core-js与core-js-pure两份core-js相关的代码,所以想去尝试能不能只保留一份core-js

另外在使用一些新的api时,比如[].at(index),发现项目内并没有对array.at方法进行polyfill,这有点奇怪,毕竟项目的内的babel配置如下所示,按道理应该会有对应的polyfill,但是实际上并没有

module.exports = {
  presets: [
    ['@babel/preset-typescript'],
    ['@babel/preset-react'],
    [
      '@babel/preset-env',
      {
        debug: false,
        useBuiltIns: 'usage',
        corejs: {
          version: 3,
          proposals: true
        }
      },
    ],
  ],
  plugins: [
    [
      '@babel/plugin-transform-runtime',
      {
        corejs: false,
        helpers: true,
        regenerator: true,
      },
    ],
  ],
};

为了解决上面的问题,及更近一步了解polyfill,于是有了如下实践

core-js介绍

core-js目前主流的polyfill库,babel内部默认的polyfill库

core-js目前有两个主流版本在使用2.x与3.x

3.x与2.x的主要区别

  • 3.x支持一些最新的提案api,而2.x不支持最新的一些提案api
  • 3.x相比2.x有更合理的命令方式
    • 稳定的方法命名为es.xxx
    • 提案的方法命名为esnext.xxx
    • 而在2.x使用es5、es6、es7这样的命名方式
  • 3.x支持多种包结构
    • core-js 提供非纯的polyfill api
    • core-js-pure 提供纯的polyfill api
    • core-js-compact 提供core-js每个版本支持的api及每个api兼容情况,供babel这样的公司查询使用
    • core-js-builder 提供一个core-js自定义打包器,允许定义自定义的core-js

常见polyfill入口

// 包含所有的Es 与 web api垫片
import "core-js";

// 只包含稳定的ES and web 标准api垫片
import "core-js/stable";

// 只包含稳定的ES api垫片
import "core-js/es";


// 包含所有Set相关api的垫片,包括提案中的api
import "core-js/features/set";

// 包含所有Set相关api的垫片,不包括提案中的api
import "core-js/stable/set";

// 只包含Es Set相关api的垫片
import "core-js/es/set";

// 与上面的Set含义一样,只不过是无污染的形式导入
import Set from "core-js-pure/features/set";
import Set from "core-js-pure/stable/set";
import Set from "core-js-pure/es/set";

// 仅仅polyfill某个方法
import "core-js/features/set/intersection";
import "core-js/stable/queue-microtask";
import "core-js/es/array/from";

// 仅仅包含某个提案
import "core-js/proposals/reflect-metadata";

// 包含state2及以上的提案垫片
import "core-js/stage/2";

其实不论是在项目中还是npm中,一般都不会直接使用core-js来进行polyfill,而是会使用babel来进行polyfill,原因是我们写的代码,如果需要运行在低版本的浏览器上,不仅需要对api进行polyfll,而且还需要对相关的es6+语法转换成es5语法,使用babel就可以把这两件事一起做了

babel polyfill

babel polyfill在陆续的演变中,提供了两种polyfill的方式,分别是@babel/preset-env与@babel/plugin-transform-runtime,二者都提供了polyfill的能力,但是提供的方式略有不同

原理:babel将code => ast => 遍历ast => 碰到对应的api则引入core-js对应的api or 直接引入整个core-js,如下所示

require("core-js/modules/es.array.find-index.js");
OR
require("core-js");
OR
var _at = _interopRequireDefault(require("@babel/runtime-corejs3/core-js/instance/at"));

@babel/preset-env

@babel/preset-env与polyfill相关的参数如下所示

  • target 法语与api兼容的最终终端目标
  • useBuiltIns 是否开启polyfill功能
  • corejs core-js相关配置
    • version 允许设置成3.1、3.21等值
    • proposals 是否允许使用提案语法
  • shippedproposals 是否允许使用稳定的提案语法

需要注意参数就是 useBuiltIns: 'entry' | 'usage' | false;

  • entry 代表直接引入整个core-js包
  • usage 代表代码内使用了哪些api,就引入对应api的polyfill
  • false代表不进行polyfill

考虑到项目大小,一般推荐使用 useBuiltIns: 'usage'

@babel/plugin-transform-runtime

为什么@babel/preset-env已经提供了polyfill,@babel/plugin-transform-runtime还需要提供polyfill,这不是增加使用难度吗?

原因是:@babel/preset-env仅提供非纯方式引入的polyfill,在项目使用场景没有问题,但是对于npm包场面,则可能会有问题,因为npm包一般是第三方提供的,为了尽可能的减少引入的npm对项目产生影响,使用无污染的方式导入polyfill更合理,所以最终演变成了@babel/plugin-transform-runtime提供无污染的polyfill方式(关于为什么不在@babel/preset-env直接做无污染的方式,猜测可能是不同的成员开发的)

// 有污染的方式,会直接在arrary原型上添加findIndex方法
require("core-js/modules/es.array.find-index.js");

// 无污染方式,不会在arrary原型上添加findIndex方法
var _at = _interopRequireDefault(require("@babel/runtime-corejs3/core-js/instance/at"));

@babel/plugin-transform-runtime相关参数如下所示

  • corejs
    • version 只允许设置2、3,这里与@babel/preset-env有差异
    • propsals

babel新的polyfill方式

babel在polyfill方面,一直在致力以更优及更小的方式帮助项目or npm包导入polyfill,所以babel团队针对目前的polyfill方式,又提出了一种新的解决方案
之前babel polyfill存在两个问题

  • babel 提供polyfill的方式有两种,为什么不能使用一种方式,降低使用成本,比如都使用@babel/preset-env或者@babel/plugin-transform-runtime
  • 另外babel默认只支持core-js这一个polyfill库,为什么不支持其它的polyfill库

所以babel团队成员提供了一个新的polyfill插件,通过该插件支持@babel/preset-env@babel/plugin-transform-runtime包含的polyfill方式

那么为什么不在@babel/preset-env或者@babel/plugin-transform-runtime基础上改造呢?原因就是这两个都是独立的包,改造起来成本都大,且都会有侵入性,所以干脆重新开一个仓库维护,并重写了polyfill的内部实现,更多详情可以参考RFC: Rethink polyfilling story

@babel/preset-env7.12.17版本接入新的babel-polyfills包
@babel/plugin-transform-runtime7.13.0接入新的babel-polyfills包

// @babel/plugin-transform-runtime
createCorejsPlgin(pluginCorejs3, {
  method: "usage-pure",
  version: 3,
  proposals,
  [pluginsCompat]: {
    useBabelRuntime: modulePath,
    ext: corejsExt
  }
}
// @babel/preset-env
const pluginOptions = {
  method: `${useBuiltIns}-global`,
  version: corejs ? corejs.toString() : undefined,
  targets: polyfillTargets,
  include,
  exclude,
  proposals,
  shippedProposals,
  debug
};

[pluginCoreJS3, pluginOptions]

babel-plugin-polifill-corejs3原理

然后我们来看下,新的polyfill方式是如何来实现的,以babel-plugin-polyfill-corejs3@0.2.4为例

三种注入core-js的方式

entry-global

注入全局polyfill

Input code Output code
import "core-js"; import "core-js/modules/es7.array.flat-map.js";
import "core-js/modules/es6.array.sort.js";
import "core-js/modules/es7.string.trim-right.js";
import "core-js/modules/web.timers.js";
...

对应@babel/preset-env 全局polyfill场景

usage-global

按需注入polyfill

Input code Output code
foo.flatMap(x => [x, x+1]);
bar.trimLeft(); arr.includes(2);
import "core-js/modules/es.array.flat-map.js"; import "core-js/modules/es.array.unscopables.flat-map.js";
import "core-js/modules/es.string.trim-start.js";
foo.flatMap(x => [x, x + 1]); bar.trimLeft();
arr.includes(2);

对应@babel/preset-env 按需polyfill场景

usage-pure

以非全局污染的方式按需导入polyfill

Input code Output code
foo.flatMap(x => [x, x+1]);
bar.trimLeft(); arr.includes(2);
import _flatMapInstanceProperty from "core-js-pure/stable/instance/flat-map.js";
import _trimLeftInstanceProperty from "core-js-pure/stable/instance/trim-left.js";
_flatMapInstanceProperty(foo).call(foo, x => [x, x + 1]);
_trimLeftInstanceProperty(bar).call(bar); arr.includes(2);

对应@babel/plugin-transform-runtime polyfill 按需无污染场景

polyfill原理

  1. babel生成ast
  2. 遍历ast
  3. 根据对应的ast,获取使用的api
  4. 然后判断api是否符合polyfill规则
  5. 如果符合,则添加对应的corejs垫片
    1. 如果是uages,则添加import core-js/modules/es.array.find-index.js
    2. 如果是pure,则添加 import _findIndex from '@babel/runtime-corejs3/core-js/instance/find-index'
  6. 如果不符合则不添加对应的垫片

关键点:怎么知道代码内使用的某个api是否符合polyfill规则

根据传入的corejs版本号及core-js-compat 包内提供的get-modules-list-for-target-version.js

modules-by-versions.json 包含每个core-js版本支持的polyfill api,这样做的原因是ecma规范是不断变化的,那么api也会不断的变化状态,比如从state0-state4,在比如新增or删除一个api,所以core-js也是不断变化的,所以每个core-js版本支持的api也是不同的

{
  "3.0": [
    "es.symbol",
    "es.symbol.description",
    "es.symbol.async-iterator",
    "es.symbol.has-instance",
    "es.symbol.is-concat-spreadable",
    ...
  ],
  "3.1": [
    "es.string.match-all",
    "es.symbol.match-all",
    "esnext.symbol.replace-all"
  ],
  "3.2": [
    "es.promise.all-settled",
    "esnext.array.is-template-object",
    "esnext.map.update-or-insert",
    "esnext.symbol.async-dispose"
  ],
  ...
  "3.16": [
    "esnext.array.filter-reject",
    "esnext.array.group-by",
    "esnext.typed-array.filter-reject",
    "esnext.typed-array.group-by"
  ],
  "3.17": [
    "es.array.at",
    "es.object.has-own",
    "es.string.at-alternative",
    "es.typed-array.at"
  ]
}

modules.json 包含core-js最新版本支持的所有api

[
  "es.symbol",
  "es.symbol.description",
  "es.symbol.async-iterator",
  "es.symbol.has-instance",
  "es.symbol.is-concat-spreadable",
  "es.symbol.iterator",
  "es.symbol.match",
  "es.symbol.match-all",
  ...
]
const modulesByVersions = require('./modules-by-versions');
const modules = require('./modules');

module.exports = function (raw) {
  // 判断传入的额corejs版本号是否符合npm版本号规范
  const corejs = semver(raw);
  if (corejs.major !== 3) {
    throw RangeError('This version of `core-js-compat` works only with `core-js@3`.');
  }
  const result = [];
  // modulesByVersions提供了core-js每个版本提供的polyfill api
  for (const version of Object.keys(modulesByVersions)) {
    // 将小于传入core-js版本号的api传入result数组
    if (compare(version, '<=', corejs)) {
      result.push(...modulesByVersions[version]);
    }
  }
  // modules包含core-js最新支持的所有api,这里的目的是从modules中过滤掉,不包含在result中的api
  return intersection(result, modules);
};
// 根据传入的corejs版本号,获取当前core-js版本支持的api
const available = new Set(getModulesListForTargetVersion(version));

filterPolyfills(name) {
  // 通过available过滤api,如果api不存在,说明api不在当前的传入的corejs支持版本内,不支持当前api polyfill
  if (!available.has(name)) return false;
  
  // 判断proposals是否为true,如果为ture表示polyfill支持提案语法,所以支持当前 api polyfill
  if (proposals) return true;

  // 判断shippedProposals是否为true,且存在corejs3ShippedProposalsList内,则支持api polyfill
  if (shippedProposals && corejs3ShippedProposalsList.has(name)) {
    return true;
  }

  // 否则,判断api是否是esnext开头,如果是esnext开头,则不支持当前api polyfill,否则则支持当前api polyfill
  return !name.startsWith("esnext.");
},

注意proposalsshippedProposals的区别是,proposals代表所有提案, shippedProposals代表进入第四个阶段的提案

所以从这里看,如果是使用@babel/preset-envpolyfill 应该这样设置

项目支持所有api的polyfill

const pkg = require('core-js/package.json');

module.exports = {
  presets: [
    [
      '@babel/preset-env',
      {
        useBuiltIns: 'usage',
        corejs: {
          version: pkg.version,
          proposals: true
        }
      },
    ],
  ],
};

项目不支持提案api的polyfill

const pkg = require('core-js/package.json');

module.exports = {
  presets: [
    [
      '@babel/preset-env',
      {
        useBuiltIns: 'usage',
        corejs: {
          version: pkg.version,
          proposals: false
        }
      },
    ],
  ],
};

所以从这里看,如果是使用@babel/plugin-transform-runtimepolyfill 应该这样设置

npm包支持所有api的polyfill

module.exports = {
  plugins: [
    [
      '@babel/plugin-transform-runtime',
      {
        corejs: {
          version: 3,
          proposals: true
        },
        helpers: true,
        regenerator: true,
      },
    ],
  ],
};

npm包不支持提案api的polyfill

module.exports = {
  plugins: [
    [
      '@babel/plugin-transform-runtime',
      {
        corejs: {
          version: 3,
          proposals: true
        },
        helpers: true,
        regenerator: true,
      },
    ],
  ],
};

注意这里@babel/preset-env@babel/plugin-transform-runtime传入的corejs参数有两个差异

  • @babel/plugin-transform-runtime的corejs参数,不支持传入3.x这样带小版本号的数字
  • 因为第一点的不同,导致proposals二者插件之间的表象不一致

@babel/plugin-transform-runtime传入corejs小版本号会抛错

if (![false, 2, 3].includes(corejsVersion)) {
  throw new Error(`The \`core-js\` version must be false, 2 or 3, but got ${JSON.stringify(rawVersion)}.`);
}

@babel/preset-env@babel/plugin-transform-runtimeproposals: true表象不一致

// 输入内容
const getArr = (index) => [5, 12, 8, 130, 44].at(index);
const getIndex = (index) => [5, 12, 8, 130, 44].findIndex(index);

export {
  getArr,
  getIndex,
};

// '@babel/preset-env', version: 3, proposals: true 输出

require("core-js/modules/es.array.find-index.js");

var getArr = function getArr(index) {
  return [5, 12, 8, 130, 44].at(index);
};

var getIndex = function getIndex(index) {
  return [5, 12, 8, 130, 44].findIndex(index);
};

// '@babel/plugin-transform-runtime', version: 3, proposals: true 输出

var _at = _interopRequireDefault(require("@babel/runtime-corejs3/core-js/instance/at"));

var _findIndex = _interopRequireDefault(require("@babel/runtime-corejs3/core-js/instance/find-index"));

var getArr = function getArr(index) {
  var _context;

  return (0, _at.default)(_context = [5, 12, 8, 130, 44]).call(_context, index);
};

var getIndex = function getIndex(index) {
  var _context2;

  return (0, _findIndex.default)(_context2 = [5, 12, 8, 130, 44]).call(_context2, index);
};

从babel输出结果可以看到,二者传入的都是corejs: { version: 3, proposals: true} 为什么得到的结果却是不同的,@babel/preset-env没有polyfill到arrary.at方法,而@babel/plugin-transform-runtime确polyfill到了array.at方法,原因是什么呢?

先看babel-plugin-polyfill-corejs3插件内的usageGlobal实现
image.png

image.png

image.png

准确的过滤出esnext.array.at方法,而esnext.array.at是在core-js 3.8版本内提供的,所以core-js3.0版本内是存在该api,所以最终的polyfill不包含array.at方法

在看babel-plugin-polyfill-corejs3内的usagePure实现

image.png

image.png

image.png

image.png

最终进行匹配的是esnext.string.at api, 而不是esnext.arrary.at api,而esnext.string.at恰好包含在corejs 3.0支持的api内,所以@babel/plugin-transform-runtime 场景下arrary.at polyfill成功了

结论:

  • proposals: true@babel/preset-env@babel/plugin-transform-runtime下表现可能是不一致的,需要看具体的api

core-js与core-js-pure共存问题

core-js是有污染的方式导入垫片,而core-js-pure是无污染的方式导入垫片;所以如果在项目使用中,出现了无污染方式进行polyfill的包,那么最终构建产物就会包含这两份,如下图所示
image.png

对于这个问题有两个思路

  1. 构建项目的时候给webpack设置别名的方式,让core-js-pure or core-js保留一个,但是目前这种方式可能会有问题,相关issues Do we need both of core-js and core-js-pure in the bundle? 以及Is there a way to share code between app and libs
  2. 对于npm包不使用无污染的方式导入垫片,这样就不会出现core-js-pure

对于内部公司内部npm包在进行构建的时候,是否一定要无污染的polyfill?目前认为是不需要的
原因就是,公司内部的项目本身会做兼容性要求,所以会进行polyfill,是可控的,所以如果引入的npm包又是纯的polyfill,那么项目在构建的产物里面最终会包含core-js 以及 core-js-pure两个包,而这两个包又有一定的大小,所以是自己项目的npm包,不推荐进行polyfill or 使用非纯的方式进行polyfill

推荐配置

鉴于上面@babel/preset-env@babel/plugin-transform-runtime关于proposals: true表现不一致,为了尽可能的降低理解成本,推荐直接使用babel-plugin-polyfill-corejs3插件,而关闭@babel/preset-env@babel/plugin-transform-runtime polyfill的能力

如果还未升级到babel最新版本,建议升级到babel最新版本,以便支持新的polyfill方式

公司内部npm包

const pkg = require('core-js/package.json');

module.exports = {
  presets: [
    ['@babel/preset-typescript'],
    ['@babel/preset-react'],
    [
      '@babel/preset-env',
      {
        debug: false,
        useBuiltIns: false,
      },
    ],
  ],
  plugins: [
    [
      '@babel/plugin-transform-runtime',
      {
        corejs: false,
        helpers: true,
        regenerator: true,
      },
    ],
    [
      "polyfill-corejs3", 
      { 
        "method": "usage-pure", 
        "version": pkg.version,
        "proposals": true
      }
    ]
  ],
};

web项目

const pkg = require('core-js/package.json');

module.exports = {
  presets: [
    ['@babel/preset-typescript'],
    ['@babel/preset-react'],
    [
      '@babel/preset-env',
      {
        debug: false,
        useBuiltIns: false,
      },
    ],
  ],
  plugins: [
    [
      '@babel/plugin-transform-runtime',
      {
        corejs: false,
        helpers: true,
        regenerator: true,
      },
    ],
    [
      "polyfill-corejs3", 
      { 
        "method": "usage-global", 
        "version": pkg.version,
        "proposals": true
      }
    ]
  ],
};

总结

回到最开始的两个问题:
在公司内部对项目进行构建大小优化的时候,发现构建产物,包括core-js与core-js-pure两份core-js相关的代码,所以想去尝试能不能只保留一份core-js
解决方案:对于公司内部的npm包,可以直接使用有污染的方式进行polyfill or 不进行polyfill由项目内统一处理,对于第三方的npm包,则可以尝试使用webpack alias来处理

另外在使用一些新的api时,比如[].at(index),发现项目内并没有对array.at方法进行polyfill,这有点奇怪,毕竟项目的内的babel配置如下所示,按道理应该会有对应的polyfill,但是实际上并没有
解决方案:保证项目的core-js版本是最新的,同时确保传入的corejs参数版本号是最新的,且proposals设置为true

如果碰到相关的api最终没有被polyfill,推荐按以下步骤进行排查

  1. 确定项目使用core-js与core-js-compact版本 yarn list core-js core-js-compat
  2. 确定使用哪种方式进行polyfill,比如@babel/preset-envor @babel/plugin-transform-runtime
  3. 确认传入的corejs参数版本号及proposals参数
  4. core-js-compact/modules-by-versions.json内查询使用的api在哪个corejs版本内
    1. 如果不在,则需要升级babel polyfill相关插件版本
    2. 如果在,则确认是否开启proposals语法
  5. 如果上面还不能简单排查出来,则推荐使用断点的方式进行排查
@CommanderXL
Copy link

点赞,总结的挺好的。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
WEB综合
Awaiting triage
开发工具
Awaiting triage
Development

No branches or pull requests

2 participants