MrErHu / blog

Star 就是最大的鼓励 👏👏👏
MIT License
605 stars 40 forks source link

Babel Polyfill 常见问题总结 #46

Open MrErHu opened 1 year ago

MrErHu commented 1 year ago

前言

作为前端工程化工具,无论是Babel还是Webpack,在前端工程化中都扮演非常重要的角色。但是这类工具除了在项目初始创建时会频繁接触,到了后期的功能开发和维护中却又鲜有涉及,加之这类工具可配置属性多,随着工具更新配置项又会经常变化,因此对我个人而言,一直掌握的并不是很好。本次在升级组件库中遇到了一系列问题,借此机会记录问题的解决。

对于Babel涉及以下相关的问题:

Babel是什么?

按照Babel官方的说法:

Babel is a JavaScript compiler

Babel is a toolchain that is mainly used to convert ECMAScript 2015+ code into a backwards compatible version of JavaScript in current and older browsers or environments. Here are the main things Babel can do for you:

  • Transform syntax
  • Polyfill features that are missing in your target environment (through @babel/polyfill)
  • Source code transformations (codemods)
  • And more!

如官方所说,Babel是JavaScript编译器,作为工具链主要用来将ECMAScript2015+的代码兼容为能运行在当前和旧版本的浏览器或其他环境中的代码。其实在上面的官方介绍中就表示了Babel所提供的两大功能:

Babel作为开箱即用工具链,在不做任何配置的情况下,Babel并不会做任何处理,而Babel所需要处理的任何工作都需要借助插件(plugin)完成。例如当我们在.babelrc中配置:

{
    "plugins": ["@babel/plugin-transform-arrow-functions"]
}

时,Babel便可以用来编译箭头函数:

// before compile
const fn = () => console.log('a')
// after compile
var fn = function fn() {
  return console.log('a');
};

当我们在项目中想使用Babel支持众多特性和语法,一条条插件配置过于繁重,因此Babel提供了Preset(预设),其本质就是一系列Babel插件的集合,例如:

@babel/preset-env是什么?

关于@babel/preset-env官方介绍:

@babel/preset-env is a smart preset that allows you to use the latest JavaScript without needing to micromanage which syntax transforms (and optionally, browser polyfills) are needed by your target environment(s).

@babel/preset-env允许我们在不需要微观管理的情况下,根据目标浏览器环境,进行语法转换(polyfill)从而使用最新的JavaScript。

在 babel@6 版本中,一般使用的是stage,例如:babel/preset-stage-0,对stage其实只会语法转化,对应的polyfill对应的API则交给 babel-plugin-transform-runtime 或者 babel-polyfill 来实现。

在 babel@7版本,废弃了stage,转而引入了@babel/preset-env,@babel/preset-env不仅提供了语法转化,同时也提供了polyfill的能力。

target

target参数表明我们项目需要适配到的环境,比如可以声明适配到的浏览器版本(例如IE11),这样 babel 会根据浏览器的支持情况自动引入所需要的 polyfill。

@babel/preset-env实际上依赖了类似于:browserslist、compat-table、electron-to-chromium这类第三方库数据,@babel/preset-env利用这些数据并按照我们配置target,获得我们目标浏览器所支持的JavaScript语法和浏览器特性,从而对应这些JavaScript语法和浏览器特性所需要的Babel插件和Polyfills。

当@babel/preset-env的target参数为空时,默认指向的是项目层级的browserslistrc配置。

一般来说,项目层级的浏览器支持配置可以通过 .browserslistrc 文件来设定目标浏览器,例如:

// .browserslistrc
> 0.25%
not dead

表示目标是包括浏览器市场份额超过0.25%且忽略没有安全更新的浏览器(如 IE10 和 BlackBerry)的用户所需的Polyfills 和代码转换。如果未设置,则默认使用browserslist配置源。browserslist的默认配置为:

> 0.5%, last 2 versions, Firefox ESR, not dead

需要注意的是,@babel/preset-env目前不支持stage-x阶段的插件,需要单独引入相应的插件。

corejs

按照core-js官方的介绍

Modular standard library for JavaScript. Includes polyfills for ECMAScript up to 2021: promises, symbols, collections, iterators, typed arrays, many other features, ECMAScript proposals, some cross-platform WHATWG / W3C features and proposals like URL. You can load only required features or use it without global namespace pollution.

core-js是JavaScript的模块化标准库,包含到ECMAScript 2021的polyfill,例如:promises、symbols、collections、iterators 等特性及提案。

目前core-js分为v2和v3两个大的版本,v3版本并不向后兼容v2版本,目前推荐使用core-js@3的主要原因在于:

@babel/preset-env的corejs属性仅当配置 useBuiltIns: usage 或 useBuiltIns: entry 时才对应生效,corejs对应的属性值为 core-js 所支持的版本,从而 决定 @babel/preset-env 如何注入polyfill。

useBuiltIns

useBuiltIns作为@babel/preset-env的配置项,支持一下三个值:

useBuiltIns: false时,@babel/preset-env不会引入polyfill,需要你在项目中主动引入:

import 'core-js/stable';
import 'regenerator-runtime/runtime';

通过这种方式,会把所有的polyfill全部引入,造成包体积庞大。

useBuiltIns: entry时,我们需要在项目入口中主动引入polyfill库,babel 根据 targets 替换成浏览器不兼容的所有 polyfill。

import "core-js";

const ps = new Promise.resolve();

会被编译成:

"use strict";

require("core-js/modules/es.symbol");
require("core-js/modules/es.symbol.description");
require("core-js/modules/es.symbol.async-iterator");
// ......省略
require("core-js/modules/web.url-search-params");
var ps = new Promise.resolve();

useBuiltIns: usage时,无序引入任何的polyfill库,babel 根据 targets ,以及项目代码中用到的 API 实现按需添加,例如:

Promise.resolve();

会被编译成

"use strict";

require("core-js/modules/es.object.to-string");
require("core-js/modules/es.promise");
var ps = new Promise.resolve();

此时@babel/preset-env则会按需引入polyfill。

需要注意的是,无论使用的哪一种useBuiltIns,preset-env 注入的 polyfill 是会污染全局的。

@babel/plugin-transform-runtime

@babel/plugin-transform-runtime的官方文档介绍如下:

A plugin that enables the re-use of Babel's injected helper code to save on codesize.

  • Automatically requires @babel/runtime/regenerator when you use generators/async functions (toggleable with the regenerator option).
  • Can use core-js for helpers if necessary instead of assuming it will be polyfilled by the user (toggleable with the corejs option)
  • Automatically removes the inline Babel helpers and uses the module @babel/runtime/helpers instead (toggleable with the helpers option).

@babel/plugin-transform-runtime的功能可以总结为三点:

自动处理generators/async函数

对于不支持的generators/async的浏览器,我们必须使用polyfill兼容处理。例如我们通过@babel/preset-env配置useBuiltIns为usage:

"use strict";

function _typeof(obj) { "@babel/helpers - typeof"; return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (obj) { return typeof obj; } : function (obj) { return obj && "function" == typeof Symbol && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }, _typeof(obj); }
require("core-js/modules/es.object.to-string");
require("core-js/modules/es.promise");
require("regenerator-runtime/runtime");
function _regeneratorRuntime() {//....}
function asyncGeneratorStep(gen, resolve, reject, _next, _throw, key, arg) { // ...... }
function _asyncToGenerator(fn) { //...... }
var fn = /*#__PURE__*/function () {
  var _ref = _asyncToGenerator( /*#__PURE__*/_regeneratorRuntime().mark(function _callee() {
    return _regeneratorRuntime().wrap(function _callee$(_context) {
      while (1) switch (_context.prev = _context.next) {
        case 0:
          console.log('Hello World');
        case 1:
        case "end":
          return _context.stop();
      }
    }, _callee);
  }));
  return function fn() {
    return _ref.apply(this, arguments);
  };
}();

但是这种方式会造成全局环境污染,利用@babel/plugin-transform-runtime配置regenerator属性则可以避免该情况。

"use strict";

var _interopRequireDefault = require("@babel/runtime-corejs3/helpers/interopRequireDefault");
var _regenerator = _interopRequireDefault(require("@babel/runtime-corejs3/regenerator"));
require("regenerator-runtime/runtime");
var _asyncToGenerator2 = _interopRequireDefault(require("@babel/runtime-corejs3/helpers/asyncToGenerator"));
var fn = /*#__PURE__*/function () {
  var _ref = (0, _asyncToGenerator2["default"])( /*#__PURE__*/_regenerator["default"].mark(function _callee() {
    return _regenerator["default"].wrap(function _callee$(_context) {
      while (1) switch (_context.prev = _context.next) {
        case 0:
          console.log('Hello World');
        case 1:
        case "end":
          return _context.stop();
      }
    }, _callee);
  }));
  return function fn() {
    return _ref.apply(this, arguments);
  };
}();

避免polyfill污染全局变量

@babel/plugin-transform-runtime插件的另外一个目的是构建代码的沙盒环境。我们之前提到的,引入 core-js 或者 @babel/polyfill都会污染全局环境(对于@babel/preset-env而言,无论使用的哪一种useBuiltIns),如果是应用开发不会造成额外的影响,但如果你的代码目的是发布给其他人使用的库,这就会造成其他的问题。

例如Promise的polyfill会被编译成:

"use strict";

require("core-js/modules/es.symbol");
require("core-js/modules/es.symbol.description");
require("core-js/modules/es.symbol.async-iterator");
// ......省略
require("core-js/modules/web.url-search-params");
var ps = new Promise.resolve();

而引入 @babel/plugin-transform-runtime 插件后,则会被编译成:

import _Promise from "@babel/runtime-corejs3/core-js-stable/promise";
const ps = new _Promise.resolve();

相比于添加全局的实例或者修改原型,而是通过的统一模块(@babel/runtime-corejs3)引入并替换,避免了对全局变量及其原型的污染,更符合类库或者工具库的定义。

如果@babel/plugin-transform-runtime 和 @babel/preset-env 共同使用,且@babel/plugin-transform-runtime 开启corejs,@babel/preset-env开启 useBuiltIns,实际效果会是怎么阳?事实上,polyfill 将会采用不污染全局的,且 @babel/preset-env 的 targets 设置将会失效。但是 @babel/plugin-transform-runtime 也并不是没有缺点,因为其导致了targets的失效,因此无法带来打包体积的优势。

自动移除Babel的helpers

Babel使用非常小的助手函数(helper)实现常见的函数,例如:_extend。默认情况下,helper会被添加到所需的每个文件中。这种逻辑会造成打包体积的膨胀。

例如:

class Person {}

会被编译为:

 "use strict";

function _typeof(obj) { // ...... }
function _defineProperties(target, props) { // ......}
function _createClass(Constructor, protoProps, staticProps) { // ...... }
function _toPropertyKey(arg) { // ...... }
function _toPrimitive(input, hint) { // ...... }
function _classCallCheck(instance, Constructor) { // ...... }
var Person = /*#__PURE__*/_createClass(function Person() {
  _classCallCheck(this, Person);
});

Babel为Class创造了_classCallCheck作为辅助函数(helpers),但是项目中存在多个文件,Babel就会为每个文件创建单独的辅助函数,这无疑会大大增加打包体积。这就是@babel/plugin-transform-runtime出现的主要原因,所有的helper将会引用@babel/runtime模块从而避免编译输出的内容的重复。 同样上面的内容,引入 @babel/plugin-transform-runtime,上面的类会被转译为:

"use strict";

var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
var _createClass2 = _interopRequireDefault(require("@babel/runtime/helpers/createClass"));
var _classCallCheck2 = _interopRequireDefault(require("@babel/runtime/helpers/classCallCheck"));
var Person = /*#__PURE__*/(0, _createClass2.default)(function Person() {
  (0, _classCallCheck2.default)(this, Person);
});

因此@babel/plugin-transform-runtime插件能够复用Babel的注入helper代码从而节省资源体积。

@babel/plugin-transform-runtime关于polyfill的副作用

@babel/plugin-transform-runtime 插件实现的 polyfill 不会污染全局环境,但是采用 @babel/plugin-transform-runtime后, @babel/preset-env 中的 targets 将会失效,这会导致最终包的体积变大。

应用项目和Library中该如何分别配置polyfill

应用项目

useBuiltIns推荐设置设置为entry,将最低环境不支持的所有 polyfill 都引入到入口文件(即使你在你的业务代码中并未使用),这是一种兼顾最终打包体积和稳妥的方式,因为我们很难保证引用的三方包有处理好polyfill这些问题。除非充分保证你的三方依赖 polyfill处理得当,那么也可以把 useBuiltIns 设置为 usage。

建议配置如下:

{
  "presets": [
    [
      "@babel/preset-env",
      {
        "useBuiltIns": "entry",
        "corejs": {
          "version": 3,
          "proposals": true
        }
      }
    ]
  ],
  "plugins": [
    [
      "@babel/plugin-transform-runtime",
      {
        "corejs": false
      }
    ]
  ]
}

并在项目开头处引入:

import 'core-js/stable';
import 'regenerator-runtime/runtime';

Library

Library与应用项目不同在会被发布给第三方使用,因而无法确定使用环境,因而要求整个环境必须是不会污染全局环境的沙盒。因此必须借助babel/plugin-transform-runtime插件,议开启 corejs,polyfill 由 @babel/plugin-transform-runtime 引入。@babel/preset-env 关闭 useBuiltIns。

建议配置如下:

{
  "presets": [
    [
      "@babel/preset-env",
    ]
  ],
  "plugins": [
    [
      "@babel/plugin-transform-runtime",
      {
        "corejs": {
          "version": 3,
          "proposals": true
        }
      }
    ]
  ]
}
h476564406 commented 1 year ago

已经收到。