AST抽象语法树和Babel插件

AST(Abstract Syntax Tree, AST)抽象语法树,可以把代码转译成语法树的表现形式

例如下面的代码:

var a = 3;
a + 5

AST抽象出来的树结构:

image.png

Program代表的是根节点

  • VariableDeclaration变量声明
    • Identifier 标识符 + Numeric Literal数字字面量
  • BinaryExpression(二项式)
    • Identifier 标识符,operator 二项式运算符,Numeric Literal数字字面量

可以到 astexplorer.net 查看AST的解析结果

编译器过程

大多数编译器的工作过程可以分为三部分:

  • Parse(解析)
  • Transform(转换)
  • Generate(代码生成)
image.png

安装 esprima 来理解编译的过程:

npm install esprima estraverse escodegen
const esprima = require('esprima')
const estraverse = require('estraverse')
const escodegen = require('escodegen')

let code = `var a = 3`

// Parse(解析)
let ast = esprima.parseScript(code);

//Transform(转换)
estraverse.traverse(ast, {
  enter(node) {
    console.log("enter",node.type);
  },
  leave(node) {
    console.log("leave",node.type);
  }
});

// Generate(代码生成)
const result = escodegen.generate(ast);

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

AST 对语法树的遍历是 深度优先遍历,所以会先向下遍历走到尽头,然后再向上遍历退出刚遍历过的节点,寻找下一个分支,所以遍历的过程中控制台会打印下面的信息:

enter Program
enter VariableDeclaration
enter VariableDeclarator
enter Identifier
leave Identifier
enter Literal
leave Literal
leave VariableDeclarator
leave VariableDeclaration
leave Program

通过type的判断我们可以修改变量的值:

estraverse.traverse(ast, {
  enter(node) {
    if(node.type === "Literal"){
      node.value = "change";
    }
  }
});

// var a = "change";

babel插件

来看下 babel是如何工作的, 首先通过npm安装 @babel/corebabel-types:

npm install @babel/core

我们知道babel能编译es6代码,例如最基础的const和箭头函数:

// es2015 的 const 和 arrow function
const add = (a, b) => a + b;

// Babel 转译后
var add = function add(a, b) {
  return a + b;
};

我们可以到 astexplorer 查看生成的语法树:

{
  "type": "Program",
  "body": [
    {
      "type": "VariableDeclaration", // 变量声明
      "declarations": [ // 具体声明
        {
          "type": "VariableDeclarator", // 变量声明
          "id": {
            "type": "Identifier", // 标识符(最基础的)
            "name": "add" // 函数名
          },
          "init": {
            "type": "ArrowFunctionExpression", // 箭头函数
            "id": null,
            "expression": true,
            "generator": false,
            "params": [ // 参数
              {
                "type": "Identifier",
                "name": "a"
              },
              {
                "type": "Identifier",
                "name": "b"
              }
            ],
            "body": { // 函数体
              "type": "BinaryExpression", // 二项式
              "left": { // 二项式左边
                "type": "Identifier",
                "name": "a"
              },
              "operator": "+", // 二项式运算符
              "right": { // 二项式右边
                "type": "Identifier",
                "name": "b"
              }
            }
          }
        }
      ],
      "kind": "const"
    }
  ],
  "sourceType": "module"
}

通过代码模拟一下:

const babel = require('babel-core');
const t = require('babel-types');

let code = `let add = (a, b)=>{return a+b}`;
let ArrowPlugins = {
visitor: {
    ArrowFunctionExpression(path) {
      let { node } = path;
      let body = node.body;
      let params = node.params;
      let r = t.functionExpression(null, params, body, false, false);
      path.replaceWith(r);
    }
  }
}
let result = babel.transform(code, {
  plugins: [
    ArrowPlugins
  ]
})
console.log(result.code);

我们可以在访问者visitor中捕获到匹配的type,在回调函数里面替换箭头函数。

class 转换

const babel = require("@babel/core");
const typs = require("@babel/types");

const code = `
class Animal {
    constructor(name){
        this.name = name
    }
    getName(){
        return this.name
    }
}
`

const classPlugins = {
    visitor:{
        ClassDeclaration(path){
            let node = path.node;
            let body = node.body.body;
            let id = node.id;
            let params = node.params;
            let methods = body.map(method=>{
                if(method.kind === "constructor"){
                    return typs.functionDeclaration(id, method.params, method.body)
                }else{
                    // Animal.prototype
                    let left = typs.memberExpression(id,typs.identifier("prototype"));
                    // Animal.prototype.getName
                    left = typs.memberExpression(left,method.key);
                    let right = typs.functionExpression(null,method.params,method.body);
                    return typs.assignmentExpression("=",left,right);
                }
            })
            path.replaceWithMultiple(methods);
        }
    }
}

const result = babel.transform(code, {
  plugins: [classPlugins]
})

console.log(result.code)

import 转换

const babel = require('@babel/core');
const types = require('@babel/types');

const code = `import antd,{Button} from "antd"`;

const importPlugin = {
  visitor: {
    ImportDeclaration(path) {
      let node = path.node
      let specifiers = node.specifiers
      if (
        !(
          specifiers.length == 1 &&
          types.isImportDefaultSpecifier(specifiers[0])
        )
      ) {
        specifiers = specifiers.map((specifier) => {
          let local = types.importDefaultSpecifier(specifier.local);
          if (types.isImportDefaultSpecifier(specifier)) {
            return types.importDeclaration([local],types.stringLiteral(node.source.value))
        } else {
            return types.importDeclaration([local],types.stringLiteral(node.source.value+"/lib/"+specifier.local.name))
          }
        });
        path.replaceWithMultiple(specifiers)
      }
    },
  },
}

const result = babel.transform(code, {
  plugins: [importPlugin],
});

console.log(result.code)

参考链接