「完全理解」如何配置项目中的 Babel

image.png

Babel,已经是每个项目都必不可少的依赖了。不过大多数同学可能并没有自己配置 Babel 的经验和机会

其实 Babel 配置并没有很难,只要了解了配置文件中的几个参数和所需依赖的作用,基本就等于完全掌握了 Babel 的使用

一、启动 Babel


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

简单说就是源码通过 Babel 处理后会得到向后兼容的代码。我们可以按照这个逻辑写出最简单的 Babel 用例

// 安装 babel 核心库
yarn add @babel/core
const babel = require('@babel/core')

babel.transform(`
        const fn=()=>console.log(1);
        fn();
    `, {}, (err, result) => {
    console.log(result.code);
});

使用 Node.js 执行这个 js 文件之后得到的输出如下

const fn = () => console.log(1);
fn();

可以发现待转化的源码没有任何变化。因为 @babel/core 的作用只是用来解析源码、把 js 代码分析成 ast,方便各个插件分析语法进行相应处理

而且我们实际使用过程中也不会直接这么用,一般会基于构建工具做一层封装,例如使用 webpack 时的 babel-loader

为了后续调用 babel 时更加直观,我们使用相应的 cli 库 @babel/cli

yarn add @bable/cli

之后随便新建个 js 文件

// src/index.js

const fn = () => console.log(1);
fn();

然后在根目录里执行命令即可

./node_modules/.bin/babel src --out-dir lib

参数的含义是把 src 里的代码用 @babel/core 处理并输出到 lib 文件夹中,所以执行成功我们可以看到输出后的代码

当然这样的执行方式太丑陋了,我们选择在 package.json 里添加 scripts

{
  ...
  "scripts": {
      "babel":"babel src --out-dir lib"
   },
  ...
}
yarn babel

由于只是换了一种执行方式,所以输出的 js 当然还是和之前一样,不会有任何转换

// lib/index.js

const fn = () => console.log(1);
fn();

二、@babel/preset-env


2.1 Plugins 插件

@babel/core 我们已经测试过,不会转化任何源码。所以为了能正确转化,我们必须使用对应的 plugins

Babel 推崇功能的单一性,就是每个插件的功能尽可能的单一。比如想使用 ES6 的箭头函数,需要对应的转化插件

yarn add @babel/plugin-transform-arrow-functions

再执行 babel

yarn babel --plugins=@babel/plugin-transform-arrow-functions

终于得到了转化后的代码

// lib/index.js

const fn = function () {
  return console.log(1);
};

fn();

不过这么写太丑了,我们直接在根目录新建一个 babel.config.js

// babel.config.js

module.exports = {
  plugins: [
    "@babel/plugin-transform-arrow-functions"
  ]
};

执行命令,Babel 会自动找到根目录中的 config 文件并使用。得到的转化文件和之前一样

yarn babel

2.2 Presets 预设

转化箭头函数需要一个 Plugin,而 const 需要另外的。我们不可能一个个的设置所有的 Plugin

这时候就需要 Presets,可以简单理解为它是一堆 Plugin 的组合。常见的 Preset 如下:(前两个已弃用,了解一下即可)

2.2.1 @babel/preset-stage-xxx

@babel/preset-stage-xxx 是 ES 在不同阶段语法提案的转码规则而产生的预设,随着被批准为 ES 新版本的组成部分而进行相应的改变(例如 ES6/ES2015)

提案分为以下几个阶段:

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

2.2.2 @babel/preset-es2015

preset-es2015 是仅包含 ES6 功能的 Babel 预设

实际上在 Babel7 出来后上面提到的这些预设 stage-x,preset-es2015 都可以废弃了,因为 @babel/preset-env 出来一统江湖了

2.2.3 @babel/preset-env

前面两个预设是从 ES 标准的维度来确定转码规则的,而 @babel/preset-env 是根据浏览器的不同版本中缺失的功能确定代码转换规则的,在配置的时候我们只用配置需要支持的浏览器版本就好了,@babel/preset-env 会根据目标浏览器生成对应的插件列表然后进行编译:

// babel.config.js

module.exports = {
 presets: [
   ["@babel/preset-env", {
     targets: {
       browsers: ["last 10 versions", "ie >= 9"]
     }
   }],
 ],
  // plugins: ["@babel/plugin-transform-arrow-functions"]
};

执行命令之后,会得到箭头函数和 const 都被转化过的代码

//lib/index.js

"use strict";

var fn = function fn() {
  return console.log(1);
};

fn();

2.2.4 @babel/preset-react

用来转化 jsx https://babeljs.io/docs/en/babel-preset-react

2.2.5 @babel/preset-typescript

用来转化 ts https://babeljs.io/docs/en/babel-preset-typescript

2.3 执行顺序

插件的排列顺序很重要。

如果两个转换插件都将处理源码的某个代码片段时,转化将根据 Plugins 或 Presets 的排列顺序依次执行

  • Plugins 在 Presets 前运行
  • Plugin 顺序从前往后排列
  • Preset 顺序是颠倒的(从后往前)

三、@babel/polyfill

看起来我们的 babel 配置好像已经比较完善了。我们在源码上再加两行代码并转化试试

// src/index.js

const fn = () => console.log(1);
fn();
+ const pro = new Promise()
+ const isIncludes = [1, 2, 3].includes(2)
//lib/index.js

"use strict";

var fn = function fn() {
  return console.log(1);
};

fn();
var pro = new Promise();
var isIncludes = [1, 2, 3].includes(2);

可以看到 Promise 和 includes 均没有被转化

有理由怀疑可能是 targets 设置过大,不过实际上就算调整到 ie 6,新加的两行代码也不会被转化

3.1 syntax 与 api

这是由于 Babel 把 Javascript 语法 分为 syntax 和 api

syntax 句法

类似箭头函数、let、const、class 等在 JavaScript 运行时无法重写的部分,就是 syntax 句法

api 方法

类似 Promise、includes 等可以通过函数重新覆盖的语法都可以归类为 api 方法。而且方法本身还分为两类

  • 方法 - Promise,Object.entries 等可以直接调用的
  • 实例方法 - [1, 2, 3].includes 等绑定在实例上的

而之前我们的源码部分只转化了 syntax 部分。 api 部分则需要借助另外的库

3.2 core-js 与 regenerator-runtime/runtime

转化 api 的思路很简单。在全局上下文下添加一个同名 api 即可

这些 api 都在 @babel/polyfill 中,所以先安装

yarn add @babel/polyfill

修改配置如下并转化源码(配置后续再说明)

// babel.config.js

module.exports = {
  "presets": [
    ["@babel/preset-env", {
+      useBuiltIns: "usage",
+      corejs: 3,
      targets: {
        browsers: ["last 10 versions", "ie >= 9"]
      }
    }],
  ],
  // plugins: ["@babel/plugin-transform-arrow-functions"]
};

可以发现我们的代码已经引入了 promise 和 includes 相关的垫片了

// lib/index.js

"use strict";

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

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

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

// src/index.js
var fn = function fn() {
  return console.log(1);
};

fn();
var pro = new Promise();
var isIncludes = [1, 2, 3].includes(2);

如果使用 await/async 语法

// src/index.js

const asyncFn = async () => { }

会发现在转化句法的同时还引入了 regenerator-runtime 库

// dist/index.js

"use strict";

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

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

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

// src/index.js
var asyncFn = /*#__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:
          case "end":
            return _context.stop();
        }
      }
    }, _callee);
  }));

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

其实 @babel/polyfill 本质就是集成了这两个库

core-js

core-js 是用于 JavaScript 的组合式标准化库,它包含 es5 (e.g: object.freeze), es6 的 promise,symbols, collections, iterators, typed arrays, es7+提案等等的 polyfills 实现。也就是说,它几乎包含了所有 JavaScript 最新标准的垫片

// 比如,只不过需要单个引用
require('core-js/array/reduce');
require('core-js/object/values');

regenerator-runtime/runtime

它是来自于 facebook 的一个库,链接。主要就是实现了 generator/yeild, async/await。
所以 babel-runtime 是单纯的实现了 core-js 和 regenerator 引入和导出,比如这里是 filter 函数的定义,做了一个中转并处理了 esModule 的兼容。

3.3 Options

// babel.config.js

module.exports = {
  "presets": [
    ["@babel/preset-env", {
      useBuiltIns: "usage",
      corejs: 3,
      targets: {
        browsers: ["last 10 versions", "ie >= 9"]
      }
    }],
  ],
  // plugins: ["@babel/plugin-transform-arrow-functions"]
};

关于 preset-env 的几个常用配置如下

corejs

指定 core-js 库的版本。除非是有历史遗留的项目,否则使用官方推荐的就行,目前是 3

useBuiltIns

最重要的一个配置项,一共有 3 个值 false、usage 和 entry

  • false - 默认值。不转化 api,只转化 syntax
  • usage - 转化源码里用到的 api
  • entry - 转化所有的 api

看起来好像 usage 吊打 entry,其实不然。

真实开发中,我们通常会引用各种第三方库,并且为了编译速度,一般都配置了规则,不会让 babel 处理这些库。

但是第三方库质量良莠不齐,保不齐哪个库里的代码就没做好兼容性转化。一旦特定浏览器运行到这部分代码,轻则报 error,重则页面白屏

所以要是对第三方库没有完善的管理机制,还是使用 entry 更保险

使用 entry 时必须先手动引入 core-js 和 regenerator-runtime/runtime(在以前引入 @babel/polyfill 这个集成库即可,最新版 babel 弃用了这个设定,必须直接引入两个库)

// src/index.js

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

const fn = () => console.log(1);
fn();
const pro = new Promise()
const isIncludes = [1, 2, 3].includes(2)
// dist/index.js

"use strict";

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

require("core-js/modules/es.symbol.description");
// ... 省略 500+ 行 require
require('regenerator-runtime/runtime');

var fn = function fn() {
  return console.log(1);
};

fn();
var pro = new Promise();
var isIncludes = [1, 2, 3].includes(2);

targets

当然直接引入也有很大缺点,由于需要转化的 api 过多,会极大增加打包后的代码体积。因此一定要配合 targets 参数使用,这样 babel 只会转化指定浏览器版本所需要的 api

modules

引入 api 的方式,默认值为 auto。设置为 false 的话 api 引入方式会变成 import

其他参数见官方文档:https://babeljs.io/docs/en/babel-preset-env#shippedproposals

四、@babel/runtime


现在想象我们开发的不是一个普通项目,而是一个工具库。

有一天,一个同学引用了我们的工具库,本来这个同学的项目经过改写原 api,每次调用 promise 方法时都会发送一个埋点

可是在使用我们的工具库之后,由于工具库里全局引入了 core-js,导致各个 api 都被 core-js 里提供的方法全局覆盖以至于污染了。因此埋点方法彻底失效了!

显然这是不合理的,因此对于类库、工具库中的 api 转化,我们可以选择全局覆盖之外的另一种方法:api 替换

先安装所需要的相关依赖

yarn add @babel/runtime @babel/plugin-transform-runtime @babel/runtime-corejs3

再修改配置(修改的是 plugins 而不是 presets)

// babel.config.js

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

源码

// src/index.js

const fn = () => console.log(1);
fn();
const pro = new Promise()
const isIncludes = [1, 2, 3].includes(2)

转化后代码

// dis/index.js

import _includesInstanceProperty from "@babel/runtime-corejs3/core-js-stable/instance/includes";
import _Promise from "@babel/runtime-corejs3/core-js-stable/promise";

var _context;

// src/index.js
const fn = () => console.log(1);

fn();
const pro = new _Promise();

const isIncludes = _includesInstanceProperty(_context = [1, 2, 3]).call(_context, 2);

这样需要转化的 api 使用的就是 @babel/runtime-corejs3 提供的方法,且不会污染全局 api

当然转化后的代码还有几个其他问题,我们依次来解决

4.1 syntax

源码并没有转化句法,因此还是需要使用 @babel/preset-env 来转化句法。(记得设置 useBuiltIns 为 false,以保证不使用 polyfill 来转化 api,不过不设置的情况下也会优先使用 runtime)

// babel.config.js

module.exports = {
  "presets": [
    ["@babel/preset-env", {
      useBuiltIns: false,
      targets: {
        browsers: ["last 10 versions", "ie >= 9"]
      }
    }],
  ],
  plugins: [
    [
      '@babel/plugin-transform-runtime',
      {
        corejs: 3,
      },
    ]
  ]
};

转化后代码

// dist/index.js

"use strict";

var _interopRequireDefault = require("@babel/runtime-corejs3/helpers/interopRequireDefault");

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

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

var _context;

// src/index.js
var fn = function fn() {
  return console.log(1);
};

fn();
var pro = new _promise["default"]();
var isIncludes = (0, _includes["default"])(_context = [1, 2, 3]).call(_context, 2);

可以看到 syntax 也完全转化了

4.2 target

有个坑点是 @babel/plugin-transform-runtime 目前不支持设置 target,也就意味着所有能被转化的 api 都会被转化。不过好像有人正在帮官方增加这个配置

4.3 实例方法

在之前我们提到过 api 有两种,一种是像 Promise 这样可以直接使用的,一种是挂载在实例上的。在 corejs 2 时期,runtime 是无法转化实例方法的。

因为 JavaScript 实在太灵活了,以至于编译阶段 Babel 根本无法分析实例到底是什么

举个极端例子

// src/index.js

function IncludesMock() { }

IncludesMock.prototype.includes = function (val) {
    return 'faker includes: ' + val
}

const randomVal = Math.random(1)
// 只有在运行时才知道 dontKnow 到底是 Array 实例 or IncludesMock 实例
const dontKnow = randomVal <= 0.5 ? [1, 2, 3] : new IncludesMock()

console.log(randomVal);
console.log(dontKnow.includes(1));

不过 corejs 3 的现在已经可以处理这个问题了

// dist/index.js

"use strict";

var _interopRequireDefault = require("@babel/runtime-corejs3/helpers/interopRequireDefault");

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

function includesMock() {}

includesMock.prototype.includes = function (val) {
  return 'faker includes: ' + val;
};

var randomVal = Math.random(1);
var dontKnow = randomVal <= 0.5 ? [1, 2, 3] : new includesMock();
console.log(randomVal);
console.log((0, _includes["default"])(dontKnow).call(dontKnow, 1));

使用 Node.js 直接执行 lib/index.js 的两种输出

0.19043928592202097
true

0.7307718322750261
faker includes: 1

推测应该是只要获取到 'includes' 关键字就会触发转化的逻辑

4.4 特例

再变态一点

// src/index.js

function IncludesMock() { }

IncludesMock.prototype.includes = function (val) {
    return 'faker includes: ' + val
}
IncludesMock.prototype.pop = function () {
    return 'faker pop'
}

const randomVal = Math.random(1)
const dontKnow = randomVal <= 0.5 ? [1, 2, 3] : new IncludesMock()

const randomVal2 = Math.random(1)
const randomApi = randomVal2 <= 0.5 ? 'includes' : 'pop'

console.log(randomVal);
console.log(dontKnow[randomApi](1));
// dist/index.js

"use strict";

function IncludesMock() {}

IncludesMock.prototype.includes = function (val) {
  return 'faker includes: ' + val;
};

IncludesMock.prototype.pop = function () {
  return 'faker pop';
};

var randomVal = Math.random(1);
var dontKnow = randomVal <= 0.5 ? [1, 2, 3] : new IncludesMock();
var randomVal2 = Math.random(1);
var randomApi = randomVal2 <= 0.5 ? 'includes' : 'pop';
console.log(randomVal);
console.log(dontKnow[randomApi](1));

可以发现 runtime 已经无能为力了,动态语言就是这么无法预测。不过正常人应该不会这么写代码,所以提醒自己注意,使用 api 的时候不要太非主流即可

其他参数见官方文档:https://babeljs.io/docs/en/babel-plugin-transform-runtime#docsNav

到这里,你因该已经完全了解了该如何配置 Babel,可喜可贺~

参考:

阅读 142

推荐阅读