关于 Babel AST 与插件机制详解

一、Babel 的整体编译流程

Babel 编译一个 JavaScript 文件,核心流程分三步:

代码字符串
   ↓
 解析 (Parse) — 用 @babel/parser 生成 AST
   ↓
 转换 (Transform) — 用插件遍历/修改 AST
   ↓
 生成 (Generate) — 用 @babel/generator 输出新 JS 代码

二、什么是 AST?

AST(Abstract Syntax Tree,抽象语法树)

AST 是 Babel 在处理 JS 时中间构建的数据结构,表示 JS 的语法结构。

例如代码:

let a = 1;

对应的 AST 结构大概是这样:

{
  "type": "VariableDeclaration",
  "kind": "let",
  "declarations": [
    {
      "type": "VariableDeclarator",
      "id": {
        "type": "Identifier",
        "name": "a"
      },
      "init": {
        "type": "NumericLiteral",
        "value": 1
      }
    }
  ]
}

可以在 ASTExplorer 中看到更直观的结构。


三、Babel 核心模块组成

模块 作用
@babel/parser 把代码转成 AST
@babel/traverse 遍历并修改 AST
@babel/types 判断、创建 AST 节点
@babel/generator 把 AST 重新转成 JS 代码
@babel/core 编译入口,用于调用插件链

四、写一个 Babel 插件:结构与实战

目标:将 let a = 1 改成 let b = 2

目录结构示例

my-plugin-test/
├── input.js         ← 原始待编译 JS
├── plugin.js        ← 自定义 Babel 插件
├── transform.js     ← 执行 Babel 编译过程

input.js

let a = 1;

plugin.js

module.exports = function ({ types: t }) {
  return {
    visitor: {
      Identifier(path) {
        if (path.node.name === "a") {
          path.node.name = "b";
        }
      },
      NumericLiteral(path) {
        if (path.node.value === 1) {
          path.node.value = 2;
        }
      }
    }
  };
};

transform.js

const fs = require("fs");
const parser = require("@babel/parser");
const traverse = require("@babel/traverse").default;
const generate = require("@babel/generator").default;
const plugin = require("./plugin");

const sourceCode = fs.readFileSync("input.js", "utf-8");

// Step 1: 解析为 AST
const ast = parser.parse(sourceCode, {
  sourceType: "module",
});

// Step 2: 应用插件逻辑(遍历 & 修改 AST)
traverse(ast, plugin({ types: require("@babel/types") }).visitor);

// Step 3: 生成新代码
const output = generate(ast, {}, sourceCode);
console.log(output.code);

运行结果:

node transform.js

输出:

let b = 2;

五、@babel/types 常用函数表

函数 用法 说明
t.identifier("x") 创建标识符节点 对应变量名
t.stringLiteral("abc") 创建字符串节点 用于赋值或参数
t.numericLiteral(123) 数字字面量 常用于赋值
t.variableDeclaration("let", [...]) 创建变量声明 第一个参数是 let/const/var
t.expressionStatement(expr) 表达式语句 常用于插入代码
t.isIdentifier(path.node) 判断是否是标识符节点 类型判断

六、插件常用操作方式(path API)

操作 说明 示例
path.node 获取当前节点 path.node.name === "x"
path.replaceWith(newNode) 替换当前节点 替换变量名
path.remove() 删除当前节点 清除无用代码
path.insertBefore(node) 在节点前插入 插入一段代码
path.insertAfter(node) 在节点后插入

七、实际用途

1)混淆变量还原

_0xabc123 这种变量还原成 loginToken

2)解密函数识别 & 重命名

识别 IIFE 内部生成的混淆函数,重命名为 decrypt()

3)删除死代码

移除 if (false) {...} 结构、debugger 调用

4)解构还原/解包还原

var { a } = obj; 还原为 var a = obj.a;

5)还原 async/await 编译结构(Generator)

我们想把这种编译后的代码:

var _fn = _asyncToGenerator(
  regeneratorRuntime.mark(function* () {
    var data;
    return regeneratorRuntime.wrap(function (_context) {
      while (1) switch (_context.prev = _context.next) {
        case 0:
          _context.next = 2;
          return fetchData();
        case 2:
          data = _context.sent;
          _context.next = 5;
          return saveData(data);
        case 5:
          _context.abrupt("return", "done");
      }
    });
  })
);

还原为 :

async function _fn() {
  const data = await fetchData();
  await saveData(data);
  return "done";
}

八、调试技巧

1)用 AST Explorer 调试节点结构

网址:AST explorer
选择:@babel/parserJavaScript → 看每个节点的 type、属性

2)打印当前节点信息

Identifier(path) {
  console.log("标识符节点:", path.node.name);
}

3)可视化生成新代码

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

九、小结

Babel 插件就是在遍历 AST 的过程中,对感兴趣的语法结构进行替换、插入或删除操作,最终生成新的 JS 代码。

你可能感兴趣的:(前端)