基于实践探寻babel7最佳配置方案

1,167 阅读14分钟

《圣经·创世纪》第11章中说,人类最早的时候都住在一个地方,讲一种语言。他们决定造一座通天的塔,所有的人住在里面,人类再也不会分散。上帝不同意,他将人类拆散到世界各地,让人类讲不同的语言,从此难于沟通。 因此,"巴别塔"就成了混乱和语言不通的代名词,是《圣经》中最广为人知的故事之一。

本文适用版本

  "devDependencies": {
    "@babel/core": "^7.15.8",
    "@babel/plugin-transform-runtime": "^7.15.8",
    "@babel/preset-env": "^7.15.8",
    "babel-loader": "^8.2.3",
    "webpack": "^5.59.1",
    "webpack-cli": "^4.9.1"
  },
  "dependencies": {
    "@babel/runtime": "^7.15.4",
    "@babel/runtime-corejs2": "^7.15.4",
    "@babel/runtime-corejs3": "^7.15.4",
    "core-js": "^3.18.3"
  }

什么是 babel

babel 用于转换 ECMAScript 2015+ 为向后兼容的 JavaScript 版本:

  • 转换语法(通过 @babel/preset-env 里集合的插件,会用到 helper 函数)
  • 目标环境中缺少的 Polyfill 功能(通过第三方 polyfill,例如 core-js
  • 源代码转换(codemods

babel 运行方式

babel 总共分为三个阶段:

  1. 解析(parser):通过 babel-parser(babylon) 解析成 AST
  2. 转换(transform):All the plugins/presetsAST 转换
  3. 生成(generator):最后通过 babel-generator 生成目标代码+sourcemap

babel 本身不具有任何转换功能,因此当我们不配置任何插件时,经过 babel 的代码和输入是相同的。

babel原理

本文侧重探讨 babel 生态各包的作用机制,故略去原理分析。

Plugin

babel 转换的功能都分解到一个个 plugin 里面,在 transform 阶段,会应用 plugins 来完成 AST 的转换:

  1. 把不支持的语法转成目标环境支持的语法来实现相同功能
  2. 把不支持的 api 自动引入对应的 polyfill

plugin 分为三类:

  1. @babel/plugin-transform-xx:转换插件,语法转换(会自动启用对应的语法插件)。
  2. @babel/plugin-proposal-xx:转换插件,指代那些对 ES Proposal(即还未被 ECMA-262 正式发布的特性)进行转换的插件,一旦正式发布后,名称就会被重名为 @babel/plugin-transform-xx
  3. @babel/plugin-syntax-xx:语法插件,不需要单独配置,会被转换插件依赖,用于语法解析。

plugins 会从前到后顺序执行,前一个 plugin 的处理结果,将作为下一个 plugin 的输入。

Preset

preset 即一组官方推荐的预设插件的集合,可以理解为插件套餐,如:

  • @babel/preset-env for compiling ES2015+ syntax
  • @babel/preset-typescript for TypeScript
  • @babel/preset-react for React
  • @babel/preset-flow for Flow

Preset 会从后往前执行(因为作者认为大部分开发者会把 presets 写成 ["es2015", "stage-0"]stage-xJavascript 语法的一些提案,那这部分可能依赖了 ES6 的语法,解析的时候得先解析这部分到 ES6,再把 ES6 解析成 ES5

Plugin 会运行在 Preset 之前。

browserslist

browserslist是在不同的前端工具之间共用目标浏览器和 node 版本的配置工具。

eg:package.json:

  "browserslist": [
    "last 1 version",
    "> 1%",
    "maintained node versions",
    "not dead"
  ]

core-js

core-js是完全模块化的 javascript 标准库。 包含 ECMA-262 至今为止大部分特性的 polyfill,如 promises、symbols、collections、iterators、typed arrays 等。目前在用的版本是 core-js@2core-js@3,其中不推荐使用 v2,因为 v3 支持更多特性的 polyfill

core-js 同时提供 3 个包:

  1. core-js:最常用的版本,引入整个 core-js 或部分特性,就会把所有或对应的 polyfill,直接扩展到代码运行的全局环境中(修改原型等方式),业务代码可直接使用最新的 ES 写法。

    import 'core-js'; //全部引入
    import 'core-js/features/array/flat'; //针对性引入(feature命名空间)
    [1, [2, 3], [4, [5]]].flat(2); // => [1, 2, 3, 4, 5]
    

    另外,通过查看core-js 源码,很清晰的得到:引入 core-js 即引入 core-js/featurescore-js/features 里引入了全部的 modules,包括 es,esnext(proposal,stage),web;另 core-js/stable 包括 es+web,故若部分特性引入则用 features 命名空间就行。

  2. core-js-pure:类似一种工具函数,不会注入到全局环境,所以整体引入无效。在使用的时候需要单独引入并使用对应 polyfillmodule 方法,不能直接使用最新 ES 的写法。

    import flat from 'core-js-pure/features/array/flat';
    flat([1, [2, 3], [4, [5]]], 2); // => [1, 2, 3, 4, 5]
    

    另外,通过查看core-js-pure 源码,与 core-js 源码仅 internalsmodules 两个文件夹有区别,即在模块导出的时候做了一些处理,形成了类似工具函数的调用方式。

    很明显在实际业务代码中,直接引用 core-js-pure 还需要格外编码变的复杂,后面介绍通过与 @babel/runtime 集成简化使用。

  3. core-js-bundle:编译打包好的版本,包含全部的 polyfill 特性,适合在浏览器里面通过 script 直接加载。

core-js 需安装在 dependencies 依赖里,并且通常情况不单独使用,要与 babel 集成。

regenerator-runtime

regenerator-runtime 模块来自 facebookregenerator 模块。生成器函数、asyncawait 函数经 babel 编译后,regenerator-runtime 模块用于提供功能实现。

@babel/core

babel 的编译核心包,是语法转换的主要工具。通过 plugins 执行后得到 result 对象包括 { code, map, ast } 等,其中 code 即为编译后的代码。所谓 babel 版本多少就是指这个包的版本多少。

@babel/core 在业务项目中,基本不会用到。都是其他编译插件需要依赖他来进行编译。故可以在这些插件的 package.json 里看到:

  "peerDependencies": {
    "@babel/core": "^7.0.0-0"
  },

npm7 以下,peerDependencies 不会自动安装,从 npm7 开始,会被默认安装,所以保险起见,业务项目最好也手动安装到 devDependencies

babel-loader

webpack 中使用 babel 加载需要编译的文件,注意 excludeinclude 的使用。

@babel/polyfill

babel 7.4 版本已废弃,因为他仅仅依赖了 core-jsregenerator-runtime,安装这两个就可以了。并且它会无差别地引入所有的 polyfill,这是不符合未来浏览器等运行环境的,按需引入更适合实际需求。

@babel/preset-env

babel7 废弃了 preset-20xx,preset-stage-xpreset 包,替换为 @babel/preset-env@babel/preset-env 是一个智能预设,集合了一系列常用插件,会根据 browserslistcompat-table 等设置的目标环境,自动将代码中的新特性转换成目标浏览器支持的代码(仅转换语法)。

通过 package.json,就能知道它集合了哪些 plugins:

  "dependencies": {
    "@babel/compat-data": "^7.15.0",
    "@babel/helper-compilation-targets": "^7.15.4",
    "@babel/helper-plugin-utils": "^7.14.5",
    "@babel/helper-validator-option": "^7.14.5",
    "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.15.4",
    "@babel/plugin-proposal-async-generator-functions": "^7.15.8",
    "@babel/plugin-proposal-class-properties": "^7.14.5",
    "@babel/plugin-proposal-class-static-block": "^7.15.4",
    "@babel/plugin-proposal-dynamic-import": "^7.14.5",
    "@babel/plugin-proposal-export-namespace-from": "^7.14.5",
    "@babel/plugin-proposal-json-strings": "^7.14.5",
    "@babel/plugin-proposal-logical-assignment-operators": "^7.14.5",
    "@babel/plugin-proposal-nullish-coalescing-operator": "^7.14.5",
    "@babel/plugin-proposal-numeric-separator": "^7.14.5",
    "@babel/plugin-proposal-object-rest-spread": "^7.15.6",
    "@babel/plugin-proposal-optional-catch-binding": "^7.14.5",
    "@babel/plugin-proposal-optional-chaining": "^7.14.5",
    "@babel/plugin-proposal-private-methods": "^7.14.5",
    "@babel/plugin-proposal-private-property-in-object": "^7.15.4",
    "@babel/plugin-proposal-unicode-property-regex": "^7.14.5",
    "@babel/plugin-syntax-async-generators": "^7.8.4",
    "@babel/plugin-syntax-class-properties": "^7.12.13",
    "@babel/plugin-syntax-class-static-block": "^7.14.5",
    "@babel/plugin-syntax-dynamic-import": "^7.8.3",
    "@babel/plugin-syntax-export-namespace-from": "^7.8.3",
    "@babel/plugin-syntax-json-strings": "^7.8.3",
    "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4",
    "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3",
    "@babel/plugin-syntax-numeric-separator": "^7.10.4",
    "@babel/plugin-syntax-object-rest-spread": "^7.8.3",
    "@babel/plugin-syntax-optional-catch-binding": "^7.8.3",
    "@babel/plugin-syntax-optional-chaining": "^7.8.3",
    "@babel/plugin-syntax-private-property-in-object": "^7.14.5",
    "@babel/plugin-syntax-top-level-await": "^7.14.5",
    "@babel/plugin-transform-arrow-functions": "^7.14.5",
    "@babel/plugin-transform-async-to-generator": "^7.14.5",
    "@babel/plugin-transform-block-scoped-functions": "^7.14.5",
    "@babel/plugin-transform-block-scoping": "^7.15.3",
    "@babel/plugin-transform-classes": "^7.15.4",
    "@babel/plugin-transform-computed-properties": "^7.14.5",
    "@babel/plugin-transform-destructuring": "^7.14.7",
    "@babel/plugin-transform-dotall-regex": "^7.14.5",
    "@babel/plugin-transform-duplicate-keys": "^7.14.5",
    "@babel/plugin-transform-exponentiation-operator": "^7.14.5",
    "@babel/plugin-transform-for-of": "^7.15.4",
    "@babel/plugin-transform-function-name": "^7.14.5",
    "@babel/plugin-transform-literals": "^7.14.5",
    "@babel/plugin-transform-member-expression-literals": "^7.14.5",
    "@babel/plugin-transform-modules-amd": "^7.14.5",
    "@babel/plugin-transform-modules-commonjs": "^7.15.4",
    "@babel/plugin-transform-modules-systemjs": "^7.15.4",
    "@babel/plugin-transform-modules-umd": "^7.14.5",
    "@babel/plugin-transform-named-capturing-groups-regex": "^7.14.9",
    "@babel/plugin-transform-new-target": "^7.14.5",
    "@babel/plugin-transform-object-super": "^7.14.5",
    "@babel/plugin-transform-parameters": "^7.15.4",
    "@babel/plugin-transform-property-literals": "^7.14.5",
    "@babel/plugin-transform-regenerator": "^7.14.5",
    "@babel/plugin-transform-reserved-words": "^7.14.5",
    "@babel/plugin-transform-shorthand-properties": "^7.14.5",
    "@babel/plugin-transform-spread": "^7.15.8",
    "@babel/plugin-transform-sticky-regex": "^7.14.5",
    "@babel/plugin-transform-template-literals": "^7.14.5",
    "@babel/plugin-transform-typeof-symbol": "^7.14.5",
    "@babel/plugin-transform-unicode-escapes": "^7.14.5",
    "@babel/plugin-transform-unicode-regex": "^7.14.5",
    "@babel/preset-modules": "^0.1.4",
    "@babel/types": "^7.15.6",
    "babel-plugin-polyfill-corejs2": "^0.2.2",
    "babel-plugin-polyfill-corejs3": "^0.2.5",
    "babel-plugin-polyfill-regenerator": "^0.2.2",
    "core-js-compat": "^3.16.0",
    "semver": "^6.3.0"
  },

proposal plugin 的提案进度在不断更新,故保持 preset-env 的更新,在业务项目中也是需要定期关注。除此之外,如果我们用到某一个新的还在 proposal 阶段的 ES 特性,并且 preset-env 未集成该 plugin,就得自己单独配置 plugins 了。

默认的 @babel/preset-env 是无法转换新的 API,比如 Iterator、Generator、Set、Maps、Proxy、Reflect、Symbol、Promise 等全局对象,以及一些定义在全局对象上的方法(比如 Object.assign )都不会转码。需要添加 core-jsregenerator-runtime 支持。

需要注意的是,在 @babel/preset-env 7.15.8 版本依赖插件 @babel/plugin-transform-regenerator,该插件用于编译 async 和生成器。因为他依赖 regenerator-transform->@babel/runtime->regenerator-runtime,本质还是 regenerator-runtime 提供编译能力。故不需要格外安装 regenerator-runtime

preset-env 配置介绍

module.exports = {
  presets: [
    [
      '@babel/env',
      {
        targets: {
          // 目标运行环境,优先级大于 browserslist
          browsers: ['> 1%', 'last 2 versions', 'not dead'],
        },
        debug: true, // 编译时候的 console
        modules: false, // 取值可以是 amd, umd, systemjs, commonjs 和 false,为 false 时可以用于 webpack 做 tree shaking
        useBuiltIns: 'usage', // usage-按需引入 entry-入口引入(代码里需手动引入core-js) false-不引入(defaults),即关闭polyfill
        corejs: 3, // 2-corejs@2(defaults) ,3-corejs@3
      },
    ],
  ],
};

需手动安装依赖 corejs@3(dependencies),已该配置为默认配置,主要关注不同 useBuiltIns 的配置情况:

entry

如果 useBuiltIns 取值为 entry,需要在代码中手动引入 polyfill 包,实例代码:

import 'core-js'; //手动引入
import 'regenerator-runtime/runtime'; //手动引入

const c = [5, 6, 7].includes(2);
const d = async () => {
  const e = await a;
  console.log(e);
};

其中引入 regenerator-runtime/runtime,是为了添加 regenerator-runtimepolyfill,否则会出现 regeneratorRuntime is not defined 的问题。

regeneratorRuntime is not defined

根据上面的配置,webpack 打包结果有 18000 多行,如果改变 targets 的配置:

targets: 'last 1 Chrome versions',

重新打包则有 6500 多行,说明在 entry 模式下,代码中对 core-jsimport 会根据 targets 的配置,替换为 core-js 最底层的 modules 引用,并且会全部打包进来

  • 优点: 所有 polyfill 都被引入,则无需关心具体某个特性的 polyfill,直接写代码就行。

  • 缺点:生成的代码包体积太大,很多特性都不需要,对前端性能必然有影响。可以在代码单独引用 core-js 的某一部分:

    import 'core-js/es/promise';
    import 'core-js/es/array';
    

    这样可以减少部分无用代码,但对 FEES 特性十分熟悉,依旧操作难度大。

usage

usage 会根据每个文件里面用到的 ES 特性+target 配置自动引入对应的 polyfill,如果 targets 的最低环境不支持某个 es 特性,则这个 es 特性的 core-js 的对应 module 会被注入。

实例代码:

index.js:

import 'test.js';
const c = [5, 6, 7].includes(2);
const d = async () => {
  const e = await a;
  console.log(e);
};

test.js:

const f = async () => {
  const g = await a;
  console.log(g);
};

webpack 打包出来的代码一共有 3700 多行,列出其中 index.jstest.js 模块代码分析:

// 省略部分代码
/***/ "./index.js":
/*!******************!*\
  !*** ./index.js ***!
  \******************/
/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {

"use strict";
__webpack_require__.r(__webpack_exports__);
/* harmony import */ var regenerator_runtime_runtime_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! regenerator-runtime/runtime.js */ "./node_modules/regenerator-runtime/runtime.js");
/* harmony import */ var regenerator_runtime_runtime_js__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(regenerator_runtime_runtime_js__WEBPACK_IMPORTED_MODULE_0__);
/* harmony import */ var core_js_modules_es_array_includes_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! core-js/modules/es.array.includes.js */ "./node_modules/core-js/modules/es.array.includes.js");
/* harmony import */ var core_js_modules_es_array_includes_js__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(core_js_modules_es_array_includes_js__WEBPACK_IMPORTED_MODULE_1__);
/* harmony import */ var core_js_modules_es_object_to_string_js__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! core-js/modules/es.object.to-string.js */ "./node_modules/core-js/modules/es.object.to-string.js");
/* harmony import */ var core_js_modules_es_object_to_string_js__WEBPACK_IMPORTED_MODULE_2___default = /*#__PURE__*/__webpack_require__.n(core_js_modules_es_object_to_string_js__WEBPACK_IMPORTED_MODULE_2__);
/* harmony import */ var core_js_modules_es_promise_js__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! core-js/modules/es.promise.js */ "./node_modules/core-js/modules/es.promise.js");
/* harmony import */ var core_js_modules_es_promise_js__WEBPACK_IMPORTED_MODULE_3___default = /*#__PURE__*/__webpack_require__.n(core_js_modules_es_promise_js__WEBPACK_IMPORTED_MODULE_3__);
/* harmony import */ var _test_js__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(/*! ./test.js */ "./test.js");


function asyncGeneratorStep(gen, resolve, reject, _next, _throw, key, arg) { try { var info = gen[key](arg); var value = info.value; } catch (error) { reject(error); return; } if (info.done) { resolve(value); } else { Promise.resolve(value).then(_next, _throw); } }

function _asyncToGenerator(fn) { return function () { var self = this, args = arguments; return new Promise(function (resolve, reject) { var gen = fn.apply(self, args); function _next(value) { asyncGeneratorStep(gen, resolve, reject, _next, _throw, "next", value); } function _throw(err) { asyncGeneratorStep(gen, resolve, reject, _next, _throw, "throw", err); } _next(undefined); }); }; }





var c = [5, 6, 7].includes(2);

var d = /*#__PURE__*/function () {
  var _ref = _asyncToGenerator( /*#__PURE__*/regeneratorRuntime.mark(function _callee() {
    var e;
    return regeneratorRuntime.wrap(function _callee$(_context) {
      while (1) {
        switch (_context.prev = _context.next) {
          case 0:
            _context.next = 2;
            return a;

          case 2:
            e = _context.sent;
            console.log(e);

          case 4:
          case "end":
            return _context.stop();
        }
      }
    }, _callee);
  }));

  return function d() {
    return _ref.apply(this, arguments);
  };
}();

/***/ }),

/***/ "./test.js":
/*!*****************!*\
  !*** ./test.js ***!
  \*****************/
/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {

"use strict";
__webpack_require__.r(__webpack_exports__);
/* harmony import */ var core_js_modules_es_object_to_string_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! core-js/modules/es.object.to-string.js */ "./node_modules/core-js/modules/es.object.to-string.js");
/* harmony import */ var core_js_modules_es_object_to_string_js__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(core_js_modules_es_object_to_string_js__WEBPACK_IMPORTED_MODULE_0__);
/* harmony import */ var core_js_modules_es_promise_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! core-js/modules/es.promise.js */ "./node_modules/core-js/modules/es.promise.js");
/* harmony import */ var core_js_modules_es_promise_js__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(core_js_modules_es_promise_js__WEBPACK_IMPORTED_MODULE_1__);
/* harmony import */ var regenerator_runtime_runtime_js__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! regenerator-runtime/runtime.js */ "./node_modules/regenerator-runtime/runtime.js");
/* harmony import */ var regenerator_runtime_runtime_js__WEBPACK_IMPORTED_MODULE_2___default = /*#__PURE__*/__webpack_require__.n(regenerator_runtime_runtime_js__WEBPACK_IMPORTED_MODULE_2__);




function asyncGeneratorStep(gen, resolve, reject, _next, _throw, key, arg) { try { var info = gen[key](arg); var value = info.value; } catch (error) { reject(error); return; } if (info.done) { resolve(value); } else { Promise.resolve(value).then(_next, _throw); } }

function _asyncToGenerator(fn) { return function () { var self = this, args = arguments; return new Promise(function (resolve, reject) { var gen = fn.apply(self, args); function _next(value) { asyncGeneratorStep(gen, resolve, reject, _next, _throw, "next", value); } function _throw(err) { asyncGeneratorStep(gen, resolve, reject, _next, _throw, "throw", err); } _next(undefined); }); }; }

var f = /*#__PURE__*/function () {
  var _ref = _asyncToGenerator( /*#__PURE__*/regeneratorRuntime.mark(function _callee() {
    var g;
    return regeneratorRuntime.wrap(function _callee$(_context) {
      while (1) {
        switch (_context.prev = _context.next) {
          case 0:
            _context.next = 2;
            return a;

          case 2:
            g = _context.sent;
            console.log(g);

          case 4:
          case "end":
            return _context.stop();
        }
      }
    }, _callee);
  }));

  return function f() {
    return _ref.apply(this, arguments);
  };
}();

/***/ }),
// 省略部分代码

从代码中可以看出:

  1. webpack 打包后,每个模块会根据该模块用到的特性(如本例中 includes)引入对应的 module 扩展了全局变量。
  2. asyncGeneratorStep_asyncToGenerator 之类的 helper 函数,会在每个模块里都会定义一次。

同样改变 targets 的配置:

targets: 'last 1 Chrome versions',

webpack 打包结果:

(self['webpackChunkbabel_demo'] = self['webpackChunkbabel_demo'] || []).push([
  ['main'],
  {
    /***/ './index.js':
      /*!******************!*\
  !*** ./index.js ***!
  \******************/
      /***/ () => {
        const c = [5, 6, 7].includes(2);

        const d = async () => {
          const e = await a;
          console.log(e);
        };

        /***/
      },
  },
  /******/ (__webpack_require__) => {
    // webpackRuntimeModules
    /******/ var __webpack_exec__ = (moduleId) => __webpack_require__((__webpack_require__.s = moduleId));
    /******/ var __webpack_exports__ = __webpack_exec__('./index.js');
    /******/
  },
]);
//# sourceMappingURL=main.js.map

一共才 24 行,说明该 targetes 的特性是浏览器直接支持的,无需添加 polyfill

false

false 即为不引入 corejspolyfill,只会做语法转换。实例代码:

const c = [5, 6, 7].includes(2);
const f = [1, 2, 3];
const g = [...f, 5, 6, 7];
const d = async () => {
  const e = await a;
  console.log(e);
};

webpack 打包结果:

(self['webpackChunkbabel_demo'] = self['webpackChunkbabel_demo'] || []).push([
  ['main'],
  {
    /***/ './index.js':
      /*!******************!*\
  !*** ./index.js ***!
  \******************/
      /***/ () => {
        function asyncGeneratorStep(gen, resolve, reject, _next, _throw, key, arg) {
          try {
            var info = gen[key](arg);
            var value = info.value;
          } catch (error) {
            reject(error);
            return;
          }
          if (info.done) {
            resolve(value);
          } else {
            Promise.resolve(value).then(_next, _throw);
          }
        }

        function _asyncToGenerator(fn) {
          return function () {
            var self = this,
              args = arguments;
            return new Promise(function (resolve, reject) {
              var gen = fn.apply(self, args);
              function _next(value) {
                asyncGeneratorStep(gen, resolve, reject, _next, _throw, 'next', value);
              }
              function _throw(err) {
                asyncGeneratorStep(gen, resolve, reject, _next, _throw, 'throw', err);
              }
              _next(undefined);
            });
          };
        }

        var c = [5, 6, 7].includes(2);
        var f = [1, 2, 3];
        var g = [].concat(f, [5, 6, 7]);

        var d = /*#__PURE__*/ (function () {
          var _ref = _asyncToGenerator(
            /*#__PURE__*/ regeneratorRuntime.mark(function _callee() {
              var e;
              return regeneratorRuntime.wrap(function _callee$(_context) {
                while (1) {
                  switch ((_context.prev = _context.next)) {
                    case 0:
                      _context.next = 2;
                      return a;

                    case 2:
                      e = _context.sent;
                      console.log(e);

                    case 4:
                    case 'end':
                      return _context.stop();
                  }
                }
              }, _callee);
            })
          );

          return function d() {
            return _ref.apply(this, arguments);
          };
        })();

        /***/
      },
  },
  /******/ (__webpack_require__) => {
    // webpackRuntimeModules
    /******/ var __webpack_exec__ = (moduleId) => __webpack_require__((__webpack_require__.s = moduleId));
    /******/ var __webpack_exports__ = __webpack_exec__('./index.js');
    /******/
  },
]);

可知仅仅做了语法转换,includes 特性对应的 polyfill 未加入。

@babel/runtime 系列

@babel/runtime 系列包含以下三种:

  1. @babel/runtime
  2. @babel/runtime-corejs2@babel/runtime + core-js@2
  3. @babel/runtime-corejs3@babel/runtime + core-js-pure@3

@babel/runtime 提供 runtime helpersregenerator-runtime,即只做语法转换,没有新 api 的实现,需要安装到 dependencies 并被 @babel/plugin-transform-runtime 依赖使用。

@babel/plugin-transform-runtime

@babel/plugin-transform-runtime插件主要通过使用@babel/runtime 系列内部的模块来代替重复的 helpers、对全局空间有污染的 core-jsregenerator-runtime 相关变量:

  1. Babel 编译过程中各模块内重复产生的 helper 方法进行重新聚合(全部指向 @babel/runtime/helpers 这个 module 当中的辅助函数),以达到减少打包体积的目的.
  2. 避免全局补丁污染,对每个模块内提供"沙箱"式的补丁。

几个例子

准备以下测试文件:

index.js:

import 'test.js';
const c = [5, 6, 7].includes(2);
const d = async () => {
  const e = await a;
  console.log(e);
};

test.js:

const f = async () => {
  const g = await a;
  console.log(g);
};

例一

添加插件,同时关闭 preset-envpolyfill 影响:

babel.config.js:

module.exports = {
  presets: [
    [
      '@babel/preset-env',
      {
        targets: {
          // 目标运行环境,优先级大于 browserslist
          browsers: ['> 1%', 'last 2 versions', 'not dead'],
        },
        useBuiltIns: false,
        // corejs:3,
        debug: true,
      },
    ],
  ],
  plugins: [
    [
      '@babel/plugin-transform-runtime',
      {
        corejs: 3, //为false就安装 npm i @babel/runtime, 为2就安装@babel/runtime-corejs2,为3就安装@babel/runtime-corejs3
      },
    ],
  ],
};

默认情况下 transform-runtime 是不启用对 core-jspolyfill 处理,那时需手动安装需手动安装 @babel/runtime,本例需手动安装依赖 @babel/runtime-corejs3webpack 打包结果(保留部分代码):

//省略部分代码
/***/ "./index.js":
/*!******************!*\
  !*** ./index.js ***!
  \******************/
/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {

"use strict";
__webpack_require__.r(__webpack_exports__);
/* harmony import */ var _babel_runtime_corejs3_helpers_asyncToGenerator__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! @babel/runtime-corejs3/helpers/asyncToGenerator */ "./node_modules/@babel/runtime-corejs3/helpers/esm/asyncToGenerator.js");
/* harmony import */ var _babel_runtime_corejs3_regenerator__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! @babel/runtime-corejs3/regenerator */ "./node_modules/@babel/runtime-corejs3/regenerator/index.js");
/* harmony import */ var _babel_runtime_corejs3_regenerator__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(_babel_runtime_corejs3_regenerator__WEBPACK_IMPORTED_MODULE_1__);
/* harmony import */ var _babel_runtime_corejs3_core_js_stable_instance_includes__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! @babel/runtime-corejs3/core-js-stable/instance/includes */ "./node_modules/@babel/runtime-corejs3/core-js-stable/instance/includes.js");
/* harmony import */ var _babel_runtime_corejs3_core_js_stable_instance_includes__WEBPACK_IMPORTED_MODULE_2___default = /*#__PURE__*/__webpack_require__.n(_babel_runtime_corejs3_core_js_stable_instance_includes__WEBPACK_IMPORTED_MODULE_2__);
/* harmony import */ var _test_js__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! ./test.js */ "./test.js");


var _context;





var c = _babel_runtime_corejs3_core_js_stable_instance_includes__WEBPACK_IMPORTED_MODULE_2___default()(_context = [5, 6, 7]).call(_context, 2);

var d = /*#__PURE__*/function () {
  var _ref = (0,_babel_runtime_corejs3_helpers_asyncToGenerator__WEBPACK_IMPORTED_MODULE_0__["default"])( /*#__PURE__*/_babel_runtime_corejs3_regenerator__WEBPACK_IMPORTED_MODULE_1___default().mark(function _callee() {
    var e;
    return _babel_runtime_corejs3_regenerator__WEBPACK_IMPORTED_MODULE_1___default().wrap(function _callee$(_context2) {
      while (1) {
        switch (_context2.prev = _context2.next) {
          case 0:
            _context2.next = 2;
            return a;

          case 2:
            e = _context2.sent;
            console.log(e);

          case 4:
          case "end":
            return _context2.stop();
        }
      }
    }, _callee);
  }));

  return function d() {
    return _ref.apply(this, arguments);
  };
}();

/***/ }),

/***/ "./test.js":
/*!*****************!*\
  !*** ./test.js ***!
  \*****************/
/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {

"use strict";
__webpack_require__.r(__webpack_exports__);
/* harmony import */ var _babel_runtime_corejs3_helpers_asyncToGenerator__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! @babel/runtime-corejs3/helpers/asyncToGenerator */ "./node_modules/@babel/runtime-corejs3/helpers/esm/asyncToGenerator.js");
/* harmony import */ var _babel_runtime_corejs3_regenerator__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! @babel/runtime-corejs3/regenerator */ "./node_modules/@babel/runtime-corejs3/regenerator/index.js");
/* harmony import */ var _babel_runtime_corejs3_regenerator__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(_babel_runtime_corejs3_regenerator__WEBPACK_IMPORTED_MODULE_1__);



var f = /*#__PURE__*/function () {
  var _ref = (0,_babel_runtime_corejs3_helpers_asyncToGenerator__WEBPACK_IMPORTED_MODULE_0__["default"])( /*#__PURE__*/_babel_runtime_corejs3_regenerator__WEBPACK_IMPORTED_MODULE_1___default().mark(function _callee() {
    var g;
    return _babel_runtime_corejs3_regenerator__WEBPACK_IMPORTED_MODULE_1___default().wrap(function _callee$(_context) {
      while (1) {
        switch (_context.prev = _context.next) {
          case 0:
            _context.next = 2;
            return a;

          case 2:
            g = _context.sent;
            console.log(g);

          case 4:
          case "end":
            return _context.stop();
        }
      }
    }, _callee);
  }));

  return function f() {
    return _ref.apply(this, arguments);
  };
}();

/***/ }),
//省略部分代码

从代码中可以看出:

  1. 重复的 helper 函数转换成公共的、单独的依赖(./node_modules/@babel/runtime-corejs3/helpers/esm/asyncToGenerator.js)引入,不在重复定义;
  2. 创建一个沙盒环境,能将这些特性对应的全局变量转换为对 core-jsregenerator-runtime 非全局变量版本的引用(如 includes 就被转换)。

例二

将例一里的 targets 改为:targets: 'last 1 Chrome versions',其他不变,再次 webpack 打包结果(保留部分代码):

//省略部分代码
/***/ "./index.js":
/*!******************!*\
  !*** ./index.js ***!
  \******************/
/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {

"use strict";
__webpack_require__.r(__webpack_exports__);
/* harmony import */ var _babel_runtime_corejs3_core_js_stable_instance_includes__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! @babel/runtime-corejs3/core-js-stable/instance/includes */ "./node_modules/@babel/runtime-corejs3/core-js-stable/instance/includes.js");
/* harmony import */ var _babel_runtime_corejs3_core_js_stable_instance_includes__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(_babel_runtime_corejs3_core_js_stable_instance_includes__WEBPACK_IMPORTED_MODULE_0__);
/* harmony import */ var _test_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./test.js */ "./test.js");
/* harmony import */ var _test_js__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(_test_js__WEBPACK_IMPORTED_MODULE_1__);
var _context;




const c = _babel_runtime_corejs3_core_js_stable_instance_includes__WEBPACK_IMPORTED_MODULE_0___default()(_context = [5, 6, 7]).call(_context, 2);

const d = async () => {
  const e = await a;
  console.log(e);
};

/***/ }),

/***/ "./test.js":
/*!*****************!*\
  !*** ./test.js ***!
  \*****************/
/***/ (() => {

const f = async () => {
  const g = await a;
  console.log(g);
};

/***/ }),
//省略部分代码

从代码中可以看出:

通过 preset-envtarget 配置,语法转换在高版本代码量减少,但 corejspolyfill 未减少。原因是 transform-runtimepolyfill 对目标环境是不做判断的,只要它识别到代码里有用到新的 ES 特性,就会进行替换。

例三

同时开启 preset-envtransform-runtimepolyfill 功能:

babel.config.js:

module.exports = {
  presets: [
    [
      '@babel/preset-env',
      {
        targets: {
          browsers: ['> 1%', 'last 2 versions', 'not dead'],
        },
        useBuiltIns: 'usage',
        corejs: 3,
        debug: true,
      },
    ],
  ],
  plugins: [
    [
      '@babel/plugin-transform-runtime',
      {
        corejs: 3,
      },
    ],
  ],
};

index.js:

Promise.resolve().finally();

webpack 打包结果(保留部分代码):

//省略部分代码
/***/ "./index.js":
/*!******************!*\
  !*** ./index.js ***!
  \******************/
/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {

"use strict";
__webpack_require__.r(__webpack_exports__);
/* harmony import */ var _babel_runtime_corejs3_core_js_stable_promise__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! @babel/runtime-corejs3/core-js-stable/promise */ "./node_modules/@babel/runtime-corejs3/core-js-stable/promise.js");
/* harmony import */ var _babel_runtime_corejs3_core_js_stable_promise__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(_babel_runtime_corejs3_core_js_stable_promise__WEBPACK_IMPORTED_MODULE_0__);
/* harmony import */ var core_js_modules_es_object_to_string_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! core-js/modules/es.object.to-string.js */ "./node_modules/core-js/modules/es.object.to-string.js");
/* harmony import */ var core_js_modules_es_object_to_string_js__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(core_js_modules_es_object_to_string_js__WEBPACK_IMPORTED_MODULE_1__);
/* harmony import */ var core_js_modules_es_promise_js__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! core-js/modules/es.promise.js */ "./node_modules/core-js/modules/es.promise.js");
/* harmony import */ var core_js_modules_es_promise_js__WEBPACK_IMPORTED_MODULE_2___default = /*#__PURE__*/__webpack_require__.n(core_js_modules_es_promise_js__WEBPACK_IMPORTED_MODULE_2__);
/* harmony import */ var core_js_modules_es_promise_finally_js__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! core-js/modules/es.promise.finally.js */ "./node_modules/core-js/modules/es.promise.finally.js");
/* harmony import */ var core_js_modules_es_promise_finally_js__WEBPACK_IMPORTED_MODULE_3___default = /*#__PURE__*/__webpack_require__.n(core_js_modules_es_promise_finally_js__WEBPACK_IMPORTED_MODULE_3__);





_babel_runtime_corejs3_core_js_stable_promise__WEBPACK_IMPORTED_MODULE_0___default().resolve().finally();

/***/ }),
//省略部分代码

从代码中可以看出:

出现了 preset-envpolyfilltransform-runtimepolyfill 并存的现象。所以两种 polyfill 不能同时启用。

由以上 3 个例子和 usage 的例子得出结论:

  1. plugin-transform-runtimepreset-env 提供的 polyfill 适用的场景是完全不同,前者适合开发库,后者适合开发 application
  2. plugin-transform-runtimepreset-envpolyfill 不能同时启用
  3. plugin-transform-runtimepolyfill 不判断目标运行环境,因为 plugin 执行在 preset 之前

babel 8

在未来的 babel8babel-polyfills会解决上述问题,支持按需自定义配置一个 polyfill provider,并将 transform-runtime 做的事情内置到 @babel/preset-env,并且 plugin 也可以使用 targets

babel7 最佳实践

使用 @babel/preset-env 的 polyfill

npm 安装:

npm i @babel/core @babel/preset-env @babel/plugin-transform-runtime -D
npm i core-js@3

babel.config.js 配置:

module.exports = {
  presets: [
    [
      '@babel/env',
      {
        targets: {
          browsers: ['> 1%', 'last 2 versions', 'not dead'],
        },
        useBuiltIns: 'usage',
        corejs: 3,
      },
    ],
  ],
  plugins: [
    [
      '@babel/plugin-transform-runtime',
      {
        corejs: false,
        regenerator: false,
      },
    ],
  ],
};

该方案会污染模块内全局,但会根据 target 来一定程度上减少 polyfill,进而减小体积,适合业务项目。

使用 @babel/plugin-transform-runtime 的 polyfill

npm 安装:

npm i @babel/core @babel/preset-env @babel/plugin-transform-runtime -D
npm i @babel/runtime-corejs3

babel.config.js 配置:

module.exports = {
  presets: [
    [
      '@babel/env',
      {
        targets: {
          browsers: ['> 1%', 'last 2 versions', 'not dead'],
        },
        useBuiltIns: false,
      },
    ],
  ],
  plugins: [
    [
      '@babel/plugin-transform-runtime',
      {
        corejs: 3,
      },
    ],
  ],
};

该方案不会污染全局,但是这个 transform-runtime 没法利用 target 因此使 polyfill 全量引入导致体积增大,适合开发库。

参考