JS `AST` (Abstract Syntax Tree) 转换工具链:`Recast`, `Babel` `traverse`

各位观众老爷,大家好!今天咱们来聊聊JavaScript AST(Abstract Syntax Tree)转换工具链,特别是 RecastBabeltraverse 方法。这玩意儿听起来玄乎,其实就是把 JavaScript 代码当成一棵树来玩,然后咱们可以像园丁一样修剪、嫁接这棵树,最终得到我们想要的“新树”。

开场白:AST是个啥?

想象一下,你写了一段 JavaScript 代码:

const x = 1 + 2;
console.log(x);

电脑怎么理解这段代码呢?它可不是直接读文字的,它会先把这段代码转换成一种叫做 AST 的东西。AST 就像是代码的骨架,把代码的结构清晰地展现出来。

你可以把 AST 想象成一棵倒过来的树,根节点代表整个程序,叶子节点代表最小的语法单元,比如变量名、数字、运算符等等。

第一部分:Recast – 保留代码格式的“整容大师”

Recast 的优势:

  • 保留代码格式: 这是 Recast 最牛逼的地方。如果你用 Babel 直接转换代码,空格、换行、注释可能会丢失。Recast 就像一个整容大师,在改变代码结构的同时,尽量保持代码原有的容貌。
  • 易于使用: Recast 提供了一系列 API,可以方便地读取、修改和生成 AST。

Recast 的基本用法:

const recast = require("recast");

const code = `
  function add(a, b) {
    return a + b;
  }
`;

// 解析代码成 AST
const ast = recast.parse(code);

// 遍历 AST,找到所有的函数声明
recast.visit(ast, {
  visitFunctionDeclaration: function(path) {
    // path.node 代表当前节点,也就是函数声明
    console.log("找到一个函数:", path.node.id.name);
    this.traverse(path); // 继续遍历子节点
  }
});

// 修改 AST
recast.visit(ast, {
  visitBinaryExpression: function(path) {
    // 把所有的加法改成乘法
    if (path.node.operator === "+") {
      path.node.operator = "*";
    }
    this.traverse(path);
  }
});

// 生成新的代码
const newCode = recast.print(ast).code;

console.log("修改后的代码:", newCode);

这段代码做了什么?

  1. recast.parse(code) 把 JavaScript 代码解析成 AST。
  2. recast.visit(ast, { ... }) 遍历 AST,第一个参数是 AST,第二个参数是一个对象,定义了各种节点的处理函数。
  3. visitFunctionDeclaration: function(path) { ... } 这是一个处理函数,当遍历到函数声明节点时,就会执行这个函数。path 对象包含了当前节点的信息,比如 path.node 代表当前节点,path.parent 代表父节点等等。
  4. this.traverse(path) 继续遍历当前节点的子节点。
  5. recast.print(ast).code 把修改后的 AST 转换成 JavaScript 代码。

Recast 的进阶用法:

Recast 还提供了一些更高级的 API,比如:

  • recast.types.builders 用于创建新的 AST 节点。
  • recast.types.namedTypes 用于判断节点的类型。

举个例子,我们可以用 recast.types.builders 创建一个新的函数调用节点:

const recast = require("recast");

const code = `
  function add(a, b) {
    return a + b;
  }
`;

const ast = recast.parse(code);

// 创建一个新的函数调用节点:console.log(add(1, 2));
const callExpression = recast.types.builders.expressionStatement(
  recast.types.builders.callExpression(
    recast.types.builders.identifier("console.log"),
    [
      recast.types.builders.callExpression(
        recast.types.builders.identifier("add"),
        [
          recast.types.builders.literal(1),
          recast.types.builders.literal(2)
        ]
      )
    ]
  )
);

// 把新的节点添加到 AST 的末尾
ast.program.body.push(callExpression);

const newCode = recast.print(ast).code;

console.log("修改后的代码:", newCode);

这段代码会在原代码的末尾添加一行 console.log(add(1, 2));

第二部分:Babel 的 traverse – 灵活强大的 AST 遍历器

Babel 的优势:

  • 生态强大: Babel 是 JavaScript 领域最流行的编译工具之一,拥有庞大的插件生态系统。
  • 性能优异: Babel 经过了精心的优化,性能非常出色。
  • 功能丰富: Babel 提供了各种各样的 API,可以满足各种复杂的 AST 转换需求。

Babel 的 traverse 方法:

traverse 是 Babel 提供的一个用于遍历 AST 的方法,它比 Recast 的 visit 方法更加灵活和强大。

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

const code = `
  function add(a, b) {
    return a + b;
  }
`;

// 解析代码成 AST
const ast = babel.parse(code);

// 遍历 AST,找到所有的函数声明
babel.traverse(ast, {
  FunctionDeclaration(path) {
    // path.node 代表当前节点,也就是函数声明
    console.log("找到一个函数:", path.node.id.name);
  }
});

// 修改 AST
babel.traverse(ast, {
  BinaryExpression(path) {
    // 把所有的加法改成乘法
    if (path.node.operator === "+") {
      path.node.operator = "*";
    }
  }
});

// 生成新的代码
const newCode = babel.transformFromAstSync(ast, code).code;

console.log("修改后的代码:", newCode);

这段代码和 Recast 的例子功能一样,但是使用了 Babel 的 traverse 方法。

path 对象详解:

在 Babel 的 traverse 方法中,path 对象非常重要,它包含了当前节点的所有信息,以及一些非常有用的方法。

属性/方法 描述
node 当前节点
parent 父节点
parentPath 父节点的 path 对象
scope 当前节点的作用域
type 节点的类型
replaceWith() 用一个新的节点替换当前节点
remove() 删除当前节点
insertBefore() 在当前节点之前插入一个节点
insertAfter() 在当前节点之后插入一个节点
skip() 跳过当前节点的子节点
stop() 停止遍历

Babel 插件:

Babel 插件是 Babel 生态系统的重要组成部分,它可以让你以一种模块化的方式扩展 Babel 的功能。一个 Babel 插件就是一个函数,它接收 Babel 的 API 对象作为参数,并返回一个对象,该对象包含了一个 visitor 属性,visitor 属性定义了各种节点的处理函数。

// my-babel-plugin.js
module.exports = function(api) {
  return {
    visitor: {
      Identifier(path) {
        // 把所有的变量名改成 "hello"
        path.node.name = "hello";
      }
    }
  };
};

使用这个插件:

const babel = require("@babel/core");
const myPlugin = require("./my-babel-plugin.js");

const code = `
  const x = 1;
  console.log(x);
`;

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

console.log(result.code); // 输出:const hello = 1; console.log(hello);

第三部分:Recast 和 Babel 的结合使用

既然 Recast 擅长保留代码格式,而 Babel 擅长 AST 转换,那我们是不是可以把它们结合起来使用呢?答案是肯定的!

const recast = require("recast");
const babel = require("@babel/core");

const code = `
  function add(a, b) {
    return a + b;
  }
`;

// 使用 Recast 解析代码成 AST
const ast = recast.parse(code);

// 使用 Babel 遍历 AST,并进行修改
babel.traverse(ast, {
  BinaryExpression(path) {
    // 把所有的加法改成乘法
    if (path.node.operator === "+") {
      path.node.operator = "*";
    }
  }
});

// 使用 Recast 生成新的代码
const newCode = recast.print(ast).code;

console.log("修改后的代码:", newCode);

这段代码先用 Recast 解析代码,然后用 Babel 遍历和修改 AST,最后用 Recast 生成新的代码。这样既可以利用 Babel 强大的 AST 转换能力,又可以保留代码原有的格式。

第四部分:实战演练 – 自动添加 console.log

现在我们来做一个稍微复杂一点的例子:自动在每个函数入口处添加 console.log

const recast = require("recast");
const babel = require("@babel/core");

const code = `
  function add(a, b) {
    return a + b;
  }

  const multiply = (a, b) => {
    return a * b;
  };
`;

const ast = recast.parse(code);

babel.traverse(ast, {
  FunctionDeclaration(path) {
    const functionName = path.node.id.name;
    const consoleLog = babel.template.statement(`console.log("${functionName} is called");`)();
    path.node.body.body.unshift(consoleLog);
  },
  ArrowFunctionExpression(path) {
    const consoleLog = babel.template.statement(`console.log("Arrow function is called");`)();
    if (path.node.body.type === 'BlockStatement') {
      path.node.body.body.unshift(consoleLog);
    } else {
      //如果箭头函数是单表达式的,需要包裹一下
      const blockStatement = babel.types.blockStatement([
        consoleLog,
        babel.types.returnStatement(path.node.body)
      ]);
      path.node.body = blockStatement;
    }
  }
});

const newCode = recast.print(ast).code;

console.log("修改后的代码:", newCode);

这段代码会在每个函数(包括箭头函数)的入口处添加一行 console.log

代码解释:

  1. babel.template.statement(code) 这是一个 Babel 提供的模板 API,可以把一段字符串代码转换成 AST 节点。
  2. path.node.body.body.unshift(consoleLog) 在函数体的最前面插入 consoleLog 节点。
  3. ArrowFunctionExpression 处理: 箭头函数有两种形式:一种是 (a, b) => a + b 这种单表达式的,另一种是 (a, b) => { return a + b; } 这种带花括号的。对于单表达式的箭头函数,我们需要手动创建一个 BlockStatement,然后把 console.logreturn 语句添加到 BlockStatement 中。

第五部分:总结

今天我们学习了 JavaScript AST 转换工具链,包括 Recast 和 Babel 的 traverse 方法。Recast 擅长保留代码格式,Babel 擅长 AST 转换,我们可以把它们结合起来使用,以实现各种各样的代码转换需求。

希望今天的讲座对大家有所帮助!下次有机会再见!

友情提示:

  • AST 转换是一个非常强大的技术,可以用于各种各样的场景,比如代码优化、代码分析、代码生成等等。
  • 学习 AST 转换需要一定的 JavaScript 基础,以及对 AST 结构的理解。
  • 多看一些开源项目的代码,可以学习到很多 AST 转换的技巧。
  • 遇到问题不要怕,多查资料,多尝试,总能解决的。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注