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 常见问题总结 #46

Open
MrErHu opened this issue Jan 24, 2023 · 1 comment
Open

Babel Polyfill 常见问题总结 #46

MrErHu opened this issue Jan 24, 2023 · 1 comment

Comments

@MrErHu
Copy link
Owner

MrErHu commented Jan 24, 2023

前言

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

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

  • Babel在项目中起到了什么作用?
  • Babel的preset是什么?@babel/preset-env起到了什么作用?@babel/preset-env为什么要配置corejs参数?
  • @babel/plugin-transform-runtime起到了什么作用?
  • Library和Application中该如何分别配置polyfill

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所提供的两大功能:

  • Transform syntax
  • Browser feature

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-flow
  • @babel/preset-react
  • @babel/preset-typescript

@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的主要原因在于:

  • [email protected]的版本已经被冻结,所有的新特性只会添加到3.0的分支中
  • [email protected]增加了proposals配置项,对处在提案阶段的api提供支持,但是因为提案阶段并不稳定,在正式加入标准之前,可能会有大的改动,因此需要谨慎使用
  • [email protected]增加了对一些web标准的支持,比如URL 和 URLSearchParams
  • [email protected]尽可能支持模块化,可以按需引入,不污染全局环境

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

useBuiltIns

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

  • false
  • entry
  • usage

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函数
  • 避免polyfill污染全局变量
  • 自动移除Babel的helpers

自动处理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
Copy link

h476564406 commented Jan 24, 2023 via email

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

2 participants