Skip to content

工作原理

parse(解析)

将原始代码字符串转为 AST 树,主要通过 @babel/parser这个包进行转换

js
const parser = require("@babel/parser");

const code = `function square(n) {
  return n * n;};`
const result = parser.parse(code)
console.log(result);

最终会输出 AST

js
{
  type: "FunctionDeclaration",
  id: {
    type: "Identifier",
    name: "square"
  },
  params: [{
    type: "Identifier",
    name: "n"
  }],
  body: {
    type: "BlockStatement",
    body: [{
      type: "ReturnStatement",
      argument: {
        type: "BinaryExpression",
        operator: "*",
        left: {
          type: "Identifier",
          name: "n"
        },
        right: {
          type: "Identifier",
          name: "n"
        }
      }
    }]
  }
}

一个 AST 可以由单一的节点或是成百上千个节点构成,它们组合在一起可以描述用于静态分析的程序语法。每个节点都有个字符串的 type 类型,用来表示节点的类型,babel 中定义了包含所有 JavaScript 语法的类型

parse 过程中会有一些 babel 插件,让babel 可以解析出更多的语法,如 jsx,typescript

transform(转化)

对上一步 parse 生成的 AST 进行深度优先遍历,从而对于匹配节点进行增删改查来修改树形结构。在 babel 中会用所配置的 plugin 或 presets 对 AST 进行修改后,得到新的 AST,我们的 babel 插件大部分用于这个阶段

babel 中通过 @babel/traverse 这个包来对 AST 进行遍历,找出需要修改的节点再进行转换,这个过程有点类似操作 DOM 树

js
const traverse = require('@babel/traverse').default

const newAst = traverse(ast, {
  enter(path) {
    if (path.isIdentifier({
        name: "n"
      })) {
      path.node.name = "x";
    }
  },
  FunctionDeclaration: {
    enter() {
      console.log('enter function declaration')
    },
    exit() {
      console.log('exit function declaration')
    }
  }
});

上面的例子中,通过识别标识符把 "n" 换成了 "x",其中的 path 是遍历过程中的路径,会保留上下文信息,有很多属性和方法,可以在访问到指定节点后,根据 path 进行自定义操作,比如

  • path.node 指向当前 AST 节点,path.parent 指向父级 AST 节点;
  • path.getSibling、path.getNextSibling、path.getPrevSibling 获取兄弟节点;
  • path.isxxx 判断当前节点是不是 xx 类型;
  • path.insertBefore、path.insertAfter 插入节点;
  • path.replaceWith、path.replaceWithMultiple、replaceWithSourceString 替换节点;
  • path.skip 跳过当前节点的子节点的遍历,path.stop 结束后续遍

有了 @babel/traverse 我们可以在 tranform 阶段做很多自定义的事情,例如删除 console.log 语句,在特定的地方插入一些表达式等等,从而影响输出结果

generate(生成)

AST 转换完之后就要输出目标代码字符串,这个阶段是个逆向操作,用新的 AST 来生成我们所需要的代码,在生成阶段本质上也是遍历抽象语法树,根据抽象语法树上每个节点的类型和属性递归调用从而生成对应的字符串代码,在 babel 中通过 @babel/generator 包的 api 来实现

js
const parser = require('@babel/parser');
const traverse = require('@babel/traverse').default;
const generate = require('@babel/generator').default;

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

const ast = parser.parse(code);

traverse(ast, {
  enter(path) {
    if (path.isIdentifier({
        name: "n"
      })) {
      path.node.name = "x";
    }
  },
});

const output = generate(ast, {}, code);
console.log(output)

// {
//   code: 'function square(x) { return x * x;}',
//   map: null,
//   rawMappings: undefined
// }