# Babel 基础

# Babel 是什么

官网 (opens new window)

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

Babel就是js的编译器 Babel是一个工具集,可以用来将ES2015+的代码转换为向后兼容的浏览器可运行的代码

# Babel 能做些什么

  • 语法转换
  • 提供当前环境可运行的代码垫片
  • 源代码的转换

# Babel 的工作原理

# 抽象语法树(AST)

首先我们编写的代码在编译阶段解析成抽象语法树(AST),然后经过一系列的遍历和转换,然后再将转换后的抽象语法树生成为常规的js代码,具体的过程如下图

babel-001

代码解析成AST的目的就是方便计算机更好地理解我们的代码,比如一个add 函数

function add(x, y) {
    return x + y;
}

add(1, 2);

将代码解析成抽象语法树(在线工具 (opens new window)),表示成JSON的形式

{
  "type": "Program",
  "start": 0,
  "end": 53,
  "body": [
    {
      "type": "FunctionDeclaration",
      "start": 0,
      "end": 40,
      "id": {
        "type": "Identifier",
        "start": 9,
        "end": 12,
        "name": "add"
      },
      "expression": false,
      "generator": false,
      "async": false,
      "params": [
        {
          "type": "Identifier",
          "start": 13,
          "end": 14,
          "name": "x"
        },
        {
          "type": "Identifier",
          "start": 16,
          "end": 17,
          "name": "y"
        }
      ],
      "body": {
        "type": "BlockStatement",
        "start": 19,
        "end": 40,
        "body": [
          {
            "type": "ReturnStatement",
            "start": 25,
            "end": 38,
            "argument": {
              "type": "BinaryExpression",
              "start": 32,
              "end": 37,
              "left": {
                "type": "Identifier",
                "start": 32,
                "end": 33,
                "name": "x"
              },
              "operator": "+",
              "right": {
                "type": "Identifier",
                "start": 36,
                "end": 37,
                "name": "y"
              }
            }
          }
        ]
      }
    },
    {
      "type": "ExpressionStatement",
      "start": 42,
      "end": 52,
      "expression": {
        "type": "CallExpression",
        "start": 42,
        "end": 51,
        "callee": {
          "type": "Identifier",
          "start": 42,
          "end": 45,
          "name": "add"
        },
        "arguments": [
          {
            "type": "Literal",
            "start": 46,
            "end": 47,
            "value": 1,
            "raw": "1"
          },
          {
            "type": "Literal",
            "start": 49,
            "end": 50,
            "value": 2,
            "raw": "2"
          }
        ],
        "optional": false
      }
    }
  ],
  "sourceType": "module"
}

这里你会发现抽象语法树中不同层级有着相似的结构,比如:

{
    "type": "Program",
    "start": 0,
    "end": 52,
    "body": [...]
}
{
    "type": "FunctionDeclaration",
    "start": 0,
    "end": 40,
    "id": {...},
    "body": {...}
}
{
    "type": "BlockStatement",
    "start": 19,
    "end": 40,
    "body": [...]
}

像这样的结构叫做节点(Node)。一个AST是由多个或单个这样的节点组成,节点内部可以有多个这样的子节点,构成一颗语法树,这样就可以描述用于静态分析的程序语法。

节点中的type字段表示节点的类型,比如上述AST中的 "Program"、"FunctionDeclaration"、 "ExpressionStatement"等等, 当然每种节点类型会有一些附加的属性用于进一步描述该节点类型

再举一个例子,如下的代码:

function abs(number) {
  if (number >= 0) {  // test
    return number;  // consequent
  } else {
    return -number; // alternate
  }
}

被解析成 AST 后对应的结构图

babel-002

# 工作原理

babel-001

上图已经描述了Babel的工作流程,有三个主要处理步骤分别是: 解析(parse)转换(transform)生成(generate)

  • 解析 将代码解析成抽象语法树(AST),每个js引擎(比如Chrome浏览器中的V8引擎)都有自己的AST解析器,而Babel是通过Babylon(现改为@babel/parser (opens new window) Github (opens new window))实现的。在解析过程中有两个阶段:词法分析语法分析,词法分析阶段把字符串形式的代码转换为令牌(tokens)流,令牌类似于AST中节点;而语法分析阶段则会把一个令牌流转换成 AST的形式,同时这个阶段会把令牌中的信息转换成AST的表述结构

  • 转换 在这个阶段,Babel接受得到AST并通过babel-traverse对其进行深度优先遍历,在此过程中对节点进行添加更新移除操作。这部分也是Babel插件介入工作的部分

  • 生成 将经过转换的AST通过babel-generator再转换成js代码,过程就是深度优先遍历整个AST,然后构建可以表示转换后代码的字符串,同时这个阶段还会生成Source Map

这部分的详细过程可以参考:babel手册 (opens new window)

Code -> Tokens -> AST -> transform -> Code(sourcemap)

# Babel 工具集

# babylon (opens new window)

“Babylon 是 Babel的解析器。最初是从Acorn项目fork出来的。Acorn非常快,易于使用,并且针对非标准特性(以及那些未来的标准特性) 设计了一个基于插件的架构。”。这里直接引用了手册里的说明,可以说Babylon定义了把代码解析成AST的一套规范。

eg:

import * as babylon from "babylon";
const code = `function square(n) {
  return n * n;
}`;

babylon.parse(code);
// Node {
//   type: "File",
//   start: 0,
//   end: 38,
//   loc: SourceLocation {...},
//   program: Node {...},
//   comments: [],
//   tokens: [...]
// }

实际上该项目已经被移动至@babel/parser (opens new window)

@babel/parser的介绍:传送门 (opens new window)

# babel-traverse (opens new window)

babel-traverse用于维护操作AST的状态,定义了更新、添加和移除节点的操作方法。之前也说到,path参数里面的属性和方法都是在babel-traverse里面定义的。这里还是引用一个例子,将babel-traverse和Babylon一起使用来遍历和更新节点

import * as babylon from "babylon";
import traverse from "babel-traverse";

const code = `function square(n) {
  return n * n;
}`;

const ast = babylon.parse(code);

traverse(ast, {
  enter(path) {
    if (
      path.node.type === "Identifier" &&
      path.node.name === "n"
    ) {
      path.node.name = "x";
    }
  }
});

# babel-types

babel-types是一个强大的用于处理AST节点的工具库,它包含了构造、验证以及变换AST节点的方法。该工具库包含考虑周到的工具方法,对编写处理AST逻辑非常有用。 这个工具库的具体的API可以参考Babel官网 (opens new window)

示例:

这里我们还是用import命令来演示一个例子,比如我们要判断import导入是什么类型的导入,这里先写出三种形式的导入

import { Ajax } from '../lib/utils';
import utils from '../lib/utils';
import * as utils from '../lib/utils';

在AST中用于表示上面导入的三个变量的节点是不同的,分别叫做ImportSpecifier、ImportDefaultSpecifier和ImportNamespaceSpecifier。具体可以参考这里 (opens new window)。 如果我们只对导入指定变量的import命令语句做处理,那么我们的babel插件就可以这样写

function plugin() {
  return ({ types }) => ({
    visitor: {
      ImportDeclaration(path, state) {
        const specifiers = path.node.specifiers;
        specifiers.forEach((specifier) => {
          if (!types.isImportDefaultSpecifier(specifier) && !types.isImportNamespaceSpecifier(specifier)) {
            // do something
          }
        })
      }
    }
  }
}

# 参考

  1. babel 手册 (opens new window)
  2. 深入Babel,这一篇就够了 (opens new window)
  3. babel 官网 (opens new window)
  4. 深入浅出 Babel 上篇:架构和原理 + 实战 (opens new window)
陕ICP备20004732号-3