22

最近在做一次Babel6升级Babel7的操作,把升级的过程和关于babel的配置进行一次总结。

1 为什么讲Babel配置

Babel 是一个工具链,主要用于将 ECMAScript 2015+ 版本的代码转换为向后兼容的 JavaScript 语法,以便能够运行在当前和旧版本的浏览器或其他环境中。

其实目前前端开发,各种项目模版,你也不需要关心babel的配置,随便拉下来一个就能运行,但是要做定制化的处理还是要把babel搞懂。

@babel/cli是Babel的命令行工具,我们一般用不到,因为我们通常都是用babel-loader,里边使用的是@babel/core的api形式,我们只需要关心Babel的配置,如果有需要在编译阶段对代码进行处理 也可以写自己的插件,但是大部分场景是需要我们把Babel的配置搞清楚。

2 Babel的配置文件

Babel6的阶段 最常用的是.babelrc,但是现在Babel7支持了更多格式:

const RELATIVE_CONFIG_FILENAMES = [".babelrc", ".babelrc.js", ".babelrc.cjs", ".babelrc.mjs", ".babelrc.json"];
package.json files with a "babel" key。

配置文件的格式如下:

{
    "presets": [
      [
        "@babel/preset-env",
        {
          "modules": "commonjs"
        }
      ]
    ],
    "plugins": [
      [
        "@babel/plugin-transform-runtime",
        {
          "corejs": 3
        }
      ],
      "@babel/plugin-syntax-dynamic-import",
    ]
  }
}

更详细介绍参见Babel Config

2.1 pluginspreset

配置文件中主要有两个配置pluginspreset,@babel/core本身对代码不做任何的转化,但是提供了对代码的解析,各种插件在解析过程中可以进行代码的转换,比如处理箭头函数的插件@babel/plugin-transform-arrow-functions等等,所以比如针对ES6语法的解析就需要很多插件,preset预设就是配置的子集,预设的一套配置,可以根据参数动态的返回配置。

2.2 执行顺序

顺序问题很重要,比如某一个插件是添加'use strict', 一个插件是删除'use strict',如果想要删除成功,就要保证执行顺序。
在一个配置里面

  • 插件在 presets 前运行。
  • 插件顺序从前往后排列。
  • preset 顺序是颠倒的(从后往前)。

所以在preset中的插件,肯定比外层的插件要后执行。

2.3 传参数

pluginspreset的配置是数组的形式,如果不需要传参数,最基本的就是字符串名称,如果需要传参数,把它写成数组的形式,数组第一项是字符串名称,第二项是要传的参数对象。

3 Babel的升级

3.1 废弃的preset

@babel/preset-env已经完全可以替换

  • babel-preset-es2015
  • babel-preset-es2016
  • babel-preset-es2017
  • babel-preset-latest

所有stage的preset在Babel v7.0.0-beta.55版本都已经被废弃了,
stage-x:指处于某一阶段的js语言提案

  • Stage 0 - 设想(Strawman):只是一个想法,可能有 Babel插件。
  • Stage 1 - 建议(Proposal):这是值得跟进的。
  • Stage 2 - 草案(Draft):初始规范。
  • Stage 3 - 候选(Candidate):完成规范并在浏览器上初步实现。
  • Stage 4 - 完成(Finished):将添加到下一个年度版本发布中。

最开始stage的出现是为了方便开发人员,每个阶段的插件与TC39和社区相互作用,同步更新,用户可以直接引用对应stage支持的语法特性。关于废弃的原因 总结下来是:

  • 1 对用户太黑盒了,当提案发生重大变化和废弃时,stage内部的插件就会变化,用户可能会出现未编译的语法。
  • 2 当用户想要支持某种语法时,不知道在某一个stage里,所以最好是让用户自己去添加插件,或者你只需要指定浏览器的兼容性,preset中动态的添加对应插件。
  • 3 第三点举了个例子,很多人都把装饰器特性叫做ES7,其实这只是阶段0的实验性建议,可能永远不会成为JS的一部分。不要将其称为“ES7”,我们要时刻提醒开发者babel是怎么工作的。

3.1 废弃的polyfill

先说下已经有了Babel为什么还要polyfill,Babel默认只转换新的JavaScript句法(syntax),而不转换新的API,比如 Iterator、Generator、Set、Maps、Proxy、Reflect、Symbol、Promise等全局对象,以及一些定义在全局对象上的方法(比如Object.assign)都不会转码。举个栗子,ES6在Array对象上新增了Array.from方法。babel就不会转码这个方法。所以之前我们都需要引入polyfill。

但是从Babel 7.4.0开始,不推荐使用此软件包,而直接包括core-js/stable(包括regenerator-runtime/runtimepolyfill ECMAScript功能)和(需要使用转译的生成器函数)。

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

但是最优的方式也不是直接这样引入,后面讲@babel/preset-env的使用时会有更好的方式。

3.3 babel-upgrade

关于升级,官方提供了工具 babel-upgrade 总结关键点如下:

  • 1 node版本8以上 这个应该都不是问题了。
  • 2 npx babel-upgrade --write --install,两个参数,--write会把更新的配置写入babel的配置文件中,package.json中也会更新依赖,但是发现没有的依赖没有新增,所以我在更新的时候把配置中依赖的npm包,在package.json都check了一遍。--install是会进行一次安装操作。

4 @babel/preset-env

@babel/preset-env是Babel推荐的最智能的预设,在使用了 babel-upgrade 升级之后你就可以看到配置中会有这个预设,因为设个预设集成了常用插件和polyfill能力,可以根据用户指定的环境寻找对应的插件。

下面对它的关键配置项做说明。

4.1 targets

string | Array<string> | { [string]: string },默认为{}

描述您为项目支持/目标的环境。

这可以是与浏览器列表兼容的查询:

`{
  "targets": "> 0.25%, not dead"
}` 

或支持最低环境版本的对象:

`{
  "targets": {
    "chrome": "58",
    "ie": "11"
  }
}` 

实施例的环境中:chromeoperaedgefirefoxsafariieiosandroidnodeelectron

如果未指定目标,则旁注@babel/preset-env将默认转换所有ECMAScript 2015+代码,所以不建议。

4.2 useBuiltIns

"usage"| "entry"| false,默认为false

此选项决定@babel/preset-env如何处理polyfill的引入。

前面将废弃polyfill时 讲到了polyfill现在分为两个npm包,是这样引入

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

但是问题是全量引入,增加包体积,所以useBuiltIns选项就是对其进行优化。

当取值"entry"时,@babel/preset-env 会把全量引入替换为目标环境特定需要的模块。

当目标浏览器是 chrome 72 时,上面的内容将被 @babel/preset-env 转换为

require("core-js/modules/es.array.reduce");
require("core-js/modules/es.array.reduce-right");
require("core-js/modules/es.array.unscopables.flat");
require("core-js/modules/es.array.unscopables.flat-map");
require("core-js/modules/es.math.hypot");
require("core-js/modules/es.object.from-entries");
require("core-js/modules/web.immediate");

当取值"usage"时,我们无需手动引入polyfill文件,@babel/preset-env 在每个文件的开头引入目标环境不支持、仅在当前文件中使用的 polyfills。

例如,

const set = new Set([1, 2, 3]);
[1, 2, 3].includes(2);

当目标环境是老的浏览器例如 ie 11,将转换为

import "core-js/modules/es.array.includes";
import "core-js/modules/es.array.iterator";
import "core-js/modules/es.object.to-string";
import "core-js/modules/es.set";

const set = new Set([1, 2, 3]);
[1, 2, 3].includes(2);

当目标是 chrome 72 时不需要导入,因为这个环境不需要 polyfills:

const set = new Set([1, 2, 3]);
[1, 2, 3].includes(2);

4.3 core-js

core-js就是Javascript标准库的polyfill,@babel/preset-env的polyfill就依赖于它,所以我们需要指定使用的core-js的版本,目前最新版本是3。
默认情况下,仅注入稳定ECMAScript功能的polyfill,如果想使用一些提案的语法,可以有三种选择:

  • 使用useBuiltIns: "entry"时,可以直接导入建议填充工具import "core-js/proposals/string-replace-all"
  • 使用useBuiltIns: "usage"时,您有两种不同的选择:

    • shippedProposals选项设置为true。这将启用已经在浏览器中发布一段时间的投标的polyfill和transforms。
    • 使用corejs: { version: 3, proposals: true }。这样可以对所支持的每个提案进行填充core-js

4.4 exclude

我觉得这个选择有用,因为@babel/preset-env中内置的插件,我们无法在其后执行,比如里面内置的"@babel/plugin-transform-modules-commonjs"插件会默认的在所有的模块上都添加use strict 严格模式, 虽然有babel-plugin-remove-use-strict用于移除use strict 但是由于执行顺序的问题,还是无法移除。
第二个问题就是内置插件无法传参数的问题。
所以我想到的方法是先exclude排除掉这个插件,然后在外层再添加 这样就可以改变执行顺序同时也可以自定义传参数。

5 @babel/plugin-transform-runtime

已经有了polyfill,这个包的作用是什么?主要分两类:

  • 1 减少代码体积,Babel的编译会在每一个模块都添加一些行内的代码垫片,例如await_asyncToGeneratorasyncGeneratorStep,使用了它之后会把这些方法通过@babel/runtime/helpers中的模块进行替换。

例如代码

async function a () {
  await new Promise(function(resolve, reject) {
    resolve(1)
  })
} 

没使用之前,编译结果

require("regenerator-runtime/runtime");

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); }); }; }


function _a() {
  _a = _asyncToGenerator( /*#__PURE__*/regeneratorRuntime.mark(function _callee() {
    return regeneratorRuntime.wrap(function _callee$(_context) {
      while (1) {
        switch (_context.prev = _context.next) {
          case 0:
            _context.next = 2;
            return new Promise(function (resolve, reject) {
              resolve(1);
            });

          case 2:
          case "end":
            return _context.stop();
        }
      }
    }, _callee);
  }));
  return _a.apply(this, arguments);
}

使用之后


var _regenerator = _interopRequireDefault(require("@babel/runtime-corejs3/regenerator"));

var _promise = _interopRequireDefault(require("@babel/runtime-corejs3/core-js-stable/promise"));

require("regenerator-runtime/runtime");

var _asyncToGenerator2 = _interopRequireDefault(require("@babel/runtime-corejs3/helpers/asyncToGenerator"));
  • 2 局部引入 不影响全局变量

@babel/preset-env中引入的polyfill都是直接引入的core-js下的模块,它的问题会污染全局变量,比如

"foobar".includes("foo");

编译后的polyfill是给String.prototype添加了includes方法,所以会影响全局的String对象。

require("core-js/modules/es.string.includes");

而使用了@babel/plugin-transform-runtime后的编译结果

var _includes = _interopRequireDefault(require("@babel/runtime-corejs3/core-js-stable/instance/includes"));

(0, _includes.default)(_context = "foobar").call(_context, "foo");

会把代码中用的到的方法进行包装,而不会对全局变量产生影响。

最后是 @babel/plugin-transform-runtime的配置项,关键的是指定 core-js的版本。

corejs: 2仅支持全局变量(例如Promise)和静态属性(例如Array.from),corejs: 3还支持实例属性(例如[].includes)。

默认情况下,@babel/plugin-transform-runtime不填充提案。如果您使用corejs: 3,则可以通过使用proposals: true选项启用此功能。

需要安装对应的运行时依赖:
npm install --save @babel/runtime-corejs3

最后 你可以基于以上知识已经创建了符合自己团队开发的preset。

如果觉得有收获请关注微信公众号 前端良文 每周都会分享前端开发中的干货知识点。


传播正能量
238 声望30 粉丝