不容错过的 Babel 知识点汇总

445 阅读5分钟

image.png

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.

Babel 是一个 JavaScript 编译器,简单来说把 JavaScript 中 es2015/2016/2017/2030 的新语法转化为 es5,让低端运行环境(如浏览器和 node )能够认识并执行。

Babel 能够做什么:

  • 语法转换
  • 通过 Polyfill 方式在目标环境中添加缺失的特性( @babel/polyfill 模块)
  • 源码转换( codemods )

基础概念

@babel/core

Babel 的核心功能包含在 @babel/core 模块中,比如常见的 transform、parse。不安装 @babel/core,无法使用 babel 进行编译。

@babel/cli

babel 提供的命令行工具,主要是提供 babel 这个命令。

@babel/node

直接在 node 环境中,运行 ES6 的代码。

babylon

Babel 的解析器

babel-traverse

用于对 AST 的遍历,维护了整棵树的状态,并且负责替换、移除和添加节点。

babel-types

用于 AST 节点的 Lodash 式工具库, 它包含了构造、验证以及变换 AST 节点的方法,对编写处理 AST 逻辑非常有用。

babel-generator

Babel 的代码生成器,它读取 AST 并将其转换为代码和源码映射(sourcemaps)


初始化项目,安装 npm install --save-dev @babel/core @babel/cli

src/index.js

const fn = () => {
  console.log('hello world');
};

package.json

"scripts": {
  "compiler": "babel src --out-dir lib --watch"
},

使用 npm run compiler 执行编译。

同级目录下,生成 lib 文件夹,但输出的内容却没有变化。

这是因为现在没有配置任何插件,编译前后的代码是完全一样的。如果想要 Babel 做一些实际的工作,就需要为其添加插件(plugin)。

插件 plugin

Babel 的插件分为两种: 语法插件和转换插件。

语法插件:只允许 babel 解析(parse) 特定类型的语法(不是转换)

使用

在项目目录下新建 .babelrc 配置文件。

解析箭头函数 npm install @babel/plugin-transform-arrow-functions -D

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

再次执行 npm run complier,可以看到箭头函数已经被编译。

预设 preset

有了插件,为什么还需要预设?

如果你想将新的 JS 特性全部都转换成低版本,需要使用对应的 plugin 。如果一个个配置的话,会非常繁琐,因为你可能需要配置几十个插件。通过使用或创建一个 preset 即可轻松使用一组插件,去简化这个配置。

@babel/preset-env:主要作用是对我们所使用的并且目标浏览器中缺失的功能进行代码转换和加载,在不进行任何配置的情况下,@babel/preset-env 所包含的插件将支持所有最新的JS特性。将其转换成ES5代码。

又比如 React 中使用到的 @babel/preset-react

{
  "plugins":[
    "@babel/plugin-syntax-jsx",
    "@babel/plugin-transform-react-jsx",
    "@babel/plugin-transform-react-display-name"
  ]
}

但如果使用 preset 就简单很多了。

{
  "presets":["@babel/preset-react"]
}

如果 presets 和 plugins 同时存在,会先执行 plugins 的配置,再执行 presets 的配置。plugin 会从第一个开始顺序执行,preset 是反向的。

// .babelrc 文件
{
  "presets": [
    [
      "@babel/preset-env"
    ]
  ],
  "plugins": [
    ["@babel/plugin-proposal-decorators", { "legacy": true }],
    ["@babel/plugin-proposal-class-properties", { "loose": true }],
    "@babel/plugin-transform-runtime"
  ]
}
  1. @babel/plugin-proposal-decorators
  2. @babel/plugin-proposal-class-properties
  3. @babel/plugin-transform-runtime
  4. @babel/preset-env

@babel/polyfill 垫片

垫片就是垫平不同浏览器环境的差异,让大家都一样,@babel/polyfill 模块可以模拟完整的 ES5 环境,让新的内置函数、实例方法等在低版本浏览器中也可以使用。

src/index.js

const arr = [1, 2, 3].includes(3);

const p = new Promise((resolve, reject) => {
  resolve(1000);
});

npm run complier

lib/index.js

"use strict";

var arr = [1, 2, 3].includes(3);
var p = new Promise(function (resolve, reject) {
  resolve(1000);
});

V7.4.0 版本开始,@babel/polyfill 已经被废弃

Babel 编译的三个阶段

Babel 的编译过程和大多数其他语言的编译器相似,可以分为三个阶段:

  • 解析(Parsing):将代码字符串解析成抽象语法树。
  • 转换(Transformation):对抽象语法树进行转换操作。
  • 生成(Code Generation): 根据变换后的抽象语法树再生成代码字符串。

image.png

解析(Parsing)

将源代码抽象出来,变成 AST(抽象语法树)Abstract Syntax Tree。

抽象语法树是源代码的抽象语法结构的树状表示,树上的每个节点都表示源代码中的一种结构,之所以说是抽象的,是因为抽象语法树并不会表示出真实语法出现的每一个细节。

AST explorer

image.png

AST 是怎么来的?

一般来说,Parse 阶段可以细分为两个阶段:「词法分析」(Lexical Analysis, LA)和「语法分析」(Syntactic Analysis, SA)。

在线分词工具

词法分析

分词:将整个代码字符串分割成语法单元数组,语法单元直白点说就是代码中的最小单元,不能再被分割。分词说白了就是简单粗暴地对字符串一个个遍历。

它接收一段源代码,然后执行一段 tokenize 函数,把代码分割成被称为Tokens 的东西。Tokens 是一个数组,由一些代码的碎片组成,比如数字、标点符号、运算符号等等等等,例如这样:

image.png

这里可以浅浅模仿一下

function tokenizer(input) {
  const tokens = [];
  const punctuators = [',', '.', '(', ')', '=', ';'];

  let current = 0;
  while (current < input.length) {

    let char = input[current];

    if (punctuators.indexOf(char) !== -1) {

      tokens.push({
        type: 'Punctuator',
        value: char,
      });
      current++;
      continue;
    }
    // 检查空格,连续的空格放到一起
    let WHITESPACE = /\s/;
    if (WHITESPACE.test(char)) {
      current++;
      continue;
    }

    // 标识符是字母、$、_开始的
    if (/[a-zA-Z\$\_]/.test(char)) {
      let value = '';

      while (/[a-zA-Z0-9\$\_]/.test(char)) {
        value += char;
        char = input[++current];
      }
      tokens.push({ type: 'Identifier', value });
      continue;
    }

    // 数字从0-9开始,不止一位
    const NUMBERS = /[0-9]/;
    if (NUMBERS.test(char)) {
      let value = '';
      while (NUMBERS.test(char)) {
        value += char;
        char = input[++current];
      }
      tokens.push({ type: 'Numeric', value });
      continue;
    }

    // 处理字符串
    if (char === '"') {
      let value = '';
      char = input[++current];

      while (char !== '"') {
        value += char;
        char = input[++current];
      }

      char = input[++current];

      tokens.push({ type: 'String', value });

      continue;
    }
    // 最后遇到不认识到字符就抛个异常出来
    throw new TypeError('Unexpected charactor: ' + char);
  }

  return tokens;
}

const input = `console.log("hello");`

console.log(tokenizer(input));

语法分析:建立分析语法单元之间的关系

「词法分析」之后,代码就已经变成了一个 Tokens 数组了,现在需要通过「语法分析」把 Tokens 转化为上面提到过的 AST。

语义分析则是将得到的词汇进行一个立体的组合,确定词语之间的关系。考虑到编程语言的各种从属关系的复杂性,语义分析的过程又是在遍历得到的语法单元组,相对而言就会变得更复杂。

简单来说语法分析是对语句和表达式识别,这是个递归过程,在解析中,Babel 会在解析每个语句和表达式的过程中设置一个暂存器,用来暂存当前读取到的语法单元,如果解析失败,就会返回之前的暂存点,再按照另一种方式进行解析,如果解析成功,则将暂存点销毁,不断重复以上操作,直到最后生成对应的语法树。

转换(Transformation)

主要用到了插件(plugin)和 预设(presets)。

这一步做的事情也很简单,就是操作 AST。可以看到 AST 中有很多相似的元素,它们都有一个 type 属性,这样的元素被称作「节点」。一个节点通常含有若干属性,可以用于描述 AST 的部分信息。比如这是一个最常见的 Identifier 节点:

{
  type: 'Identifier',
  name: 'add'
}

表示这是一个标识符。所以,操作 AST 也就是操作其中的节点,可以增删改这些节点,从而转换成实际需要的 AST。

Babel 对于 AST 的遍历是深度优先遍历,对于 AST 上的每一个分支 Babel 都会先向下遍历走到尽头,然后再向上遍历退出刚遍历过的节点,然后寻找下一个分支。

image.png

从 declarations 里开始遍历:

  1. 声明了一个变量,并且知道了它的内部属性(id、init),然后我们再以此访问每一个属性以及它们的子节点。
  2. id 是一个 Idenrifier,有一个 name 属性表示变量名。
  3. 之后是 init,init 也有好几个内部属性。

Babel 会维护一个称作 Visitor 的对象,这个对象定义了用于 AST 中获取具体节点的方法。Babel 遍历 AST 其实会经过两次节点:遍历的时候和退出的时候。

var visitor = {
  Identifier: {
    enter() {
      console.log('Identifier enter');
    },
    exit() {
      console.log('Identifier exit');
    }
  }
};

生成(Code Generation)

经过上面两个阶段,需要转译的代码已经经过转换,生成新的 AST 了,最后一个阶段理所应当就是根据这个 AST 来输出代码。

Babel 是通过babel-generator来完成的。当然,也是深度优先遍历。

class Generator extends Printer {
  constructor(ast, opts = {}, code) {
    const format = normalizeOptions(code, opts);
    const map = opts.sourceMaps ? new SourceMap(opts, code) : null;
    super(format, map);
    this.ast = ast;
  }
  ast: Object;
  generate() {
    returnsuper.generate(this.ast);
  }
}

自定义插件

一种方式是将自己写的插件发布到 npm 仓库中去,然后本地安装,然后在 Babel 配置文件中配置插件名称就好了:

npm install @babel/plugin-myPlugin

.babelrc

{
  "plugins": ["@babel/plugin-myPlugin"]
}

另外一种方式就是不发布,直接将写好的插件放在项目中,然后在 babel 配置文件中通过访问相对路径的方式来加载插件:

.babelrc

{
  "plugins": ["./plugins/plugin-myPlugin"]
}

编写插件

插件实际上就是在处理 AST 抽象语法树,所以编写插件只需要做到下面三点:

  • 确认我们要修改的节点类型
  • 找到 AST 中需要修改的属性
  • 将 AST 中需要修改的属性用新生成的属性对象替换
export default function({ types: t }) {
  // plugin contents
}

返回一个对象,visitor 属性是这个插件的主要访问者。

export default function({ types: t }) {
  return {
    visitor: {
      // visitor contents
    }
  };
};

visitor 中的每个函数接收 2 个参数:path 和 state

export default function({ types: t }) {
  return {
    visitor: {
      CallExpression(path, state) {}
    }
  };
};

示例插件

简单的插件,把定义变量名为 a 的换成 b。在写插件前你需要明确转换前后的 AST 长什么样子,就说你总得有一个对照吧。

image.png

image.png

找到差别,然后就到了用代码来解决问题的时候了

function aTob({ types: t }) {
  return {
    visitor: {
      VariableDeclarator(path, state) {
        if (path.node.id.name == 'a') {
          path.node.id = t.identifier('b')
        }
      }
    }
  }
}

我们要把 id 属性是 a 的替换成 b 。但是不能直接 path.node.id.name = 'b' 。如果操作的是Object,就没问题,但是这里是 AST 语法树,所以想改变某个值,就是用对应的 AST 来替换,现在我们用新的标识符来替换这个属性。

node aTob.js

import * as babel from '@babel/core';
const c = `var a = 2022`;

function aTob({ types: t }) {
  return {
    visitor: {
      VariableDeclarator(path, state) {
        if (path.node.id.name == 'a') {
          path.node.id = t.identifier('b')
        }
      }
    }
  }
}

const { code } = babel.transform(c, {
  plugins: [
    aTob
  ]
})

console.log(code);
// var b = 2022;

自定义预设(Presets)

预设就是已有插件的组合,主要就是为了避免使用者配置过多的插件,通过预设把插件收敛起来。当你提前知道了自己需要什么插件的时候。就是把 Babel 配置中的 plugins 配置放到 presets 中了,实质上还是在配置 Plugins,只是写 Presets 的人帮我们配置好了。

import { declare } from "@babel/helper-plugin-utils";
import pluginA from "myPluginA";
import pluginB from "myPluginB"
export default declare((api, opts) => {
  const pragma = opts.pragma;
  return {
    plugins: [
      [
        pluginA,
        { pragma }//插件传参
      ],
      pluginB
    ]
  };
});

参考文章:

不容错过的 Babel7 知识

前端工程师的自我修养-关于 Babel 那些事儿