Babel 转译原理

Babel 的三个阶段:解析、转换、生成

问题

Babel 是如何将 ES6+ 代码转换为 ES5 代码的?解释 AST 解析、转换、生成三个阶段。

解答

Babel 的工作流程分为三个阶段:

源代码 → 解析(Parse) → AST → 转换(Transform) → 新AST → 生成(Generate) → 目标代码

1. 解析(Parse)

将源代码转换为 AST(抽象语法树)。

const parser = require('@babel/parser');

const code = 'const fn = (a, b) => a + b';

// 解析代码生成 AST
const ast = parser.parse(code, {
  sourceType: 'module'
});

console.log(JSON.stringify(ast, null, 2));

生成的 AST 结构(简化):

{
  "type": "Program",
  "body": [{
    "type": "VariableDeclaration",
    "declarations": [{
      "type": "VariableDeclarator",
      "id": { "type": "Identifier", "name": "fn" },
      "init": {
        "type": "ArrowFunctionExpression",
        "params": [
          { "type": "Identifier", "name": "a" },
          { "type": "Identifier", "name": "b" }
        ],
        "body": {
          "type": "BinaryExpression",
          "operator": "+",
          "left": { "type": "Identifier", "name": "a" },
          "right": { "type": "Identifier", "name": "b" }
        }
      }
    }]
  }]
}

2. 转换(Transform)

遍历 AST,通过插件对节点进行增删改。

const parser = require('@babel/parser');
const traverse = require('@babel/traverse').default;
const t = require('@babel/types');

const code = 'const fn = (a, b) => a + b';
const ast = parser.parse(code);

// 遍历 AST,将箭头函数转换为普通函数
traverse(ast, {
  // 访问箭头函数节点
  ArrowFunctionExpression(path) {
    const { params, body } = path.node;
    
    // 如果函数体不是 BlockStatement,包装成 return 语句
    const functionBody = t.isBlockStatement(body)
      ? body
      : t.blockStatement([t.returnStatement(body)]);
    
    // 创建普通函数表达式替换箭头函数
    const functionExpression = t.functionExpression(
      null,      // id
      params,    // 参数
      functionBody // 函数体
    );
    
    path.replaceWith(functionExpression);
  }
});

3. 生成(Generate)

将转换后的 AST 转回代码字符串。

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

const code = 'const fn = (a, b) => a + b';
const ast = parser.parse(code);

traverse(ast, {
  ArrowFunctionExpression(path) {
    const { params, body } = path.node;
    const functionBody = t.isBlockStatement(body)
      ? body
      : t.blockStatement([t.returnStatement(body)]);
    
    path.replaceWith(t.functionExpression(null, params, functionBody));
  }
});

// 生成代码
const output = generate(ast, {}, code);
console.log(output.code);
// 输出: const fn = function (a, b) { return a + b; };

完整示例:自定义 Babel 插件

// my-plugin.js
module.exports = function() {
  return {
    visitor: {
      // 将 console.log 转换为 console.warn
      CallExpression(path) {
        const { callee } = path.node;
        
        if (
          callee.type === 'MemberExpression' &&
          callee.object.name === 'console' &&
          callee.property.name === 'log'
        ) {
          callee.property.name = 'warn';
        }
      }
    }
  };
};

使用插件:

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

const code = 'console.log("hello")';

const result = babel.transformSync(code, {
  plugins: ['./my-plugin.js']
});

console.log(result.code);
// 输出: console.warn("hello");

关键点

  • 解析阶段:词法分析(tokenize)+ 语法分析,将代码转为 AST
  • 转换阶段:通过 visitor 模式遍历 AST,插件在此阶段工作
  • 生成阶段:深度优先遍历 AST,拼接代码字符串
  • @babel/types:用于创建和判断 AST 节点的工具库
  • path 对象:包含节点信息和操作方法(replaceWith、remove、insertBefore 等)