什么是 AST?
AST(Abstract Syntax Tree,抽象语法树)是源代码的树状表示形式,它将代码结构化为节点层次结构,每个节点代表代码中的一个构造(如变量声明、函数调用等)。
Babel AST 规范
Babel 使用基于 ESTree 规范的 AST,并扩展了 JSX、TypeScript 等语法支持。
AST 节点类型
常见节点类型
| 节点类型 | 说明 | 示例 |
|---|---|---|
Program | 程序根节点 | 整个文件 |
Identifier | 标识符 | 变量名、函数名 |
Literal | 字面量 | 1, "hello", true |
VariableDeclaration | 变量声明 | const, let, var |
FunctionDeclaration | 函数声明 | function foo() {} |
CallExpression | 函数调用 | foo() |
BinaryExpression | 二元表达式 | a + b |
MemberExpression | 成员表达式 | obj.prop |
AST 示例
javascript// 源代码 const sum = (a, b) => a + b; // AST(简化版) { "type": "VariableDeclaration", "kind": "const", "declarations": [{ "type": "VariableDeclarator", "id": { "type": "Identifier", "name": "sum" }, "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" } } } }] }
编写自定义 Babel 插件
1. 基础插件结构
javascript// my-plugin.js module.exports = function(babel) { const { types: t } = babel; return { name: 'my-custom-plugin', visitor: { // 访问者方法 Identifier(path) { console.log('Found identifier:', path.node.name); } } }; };
2. 实用插件示例
示例 1:替换 console.log
javascript// remove-console-plugin.js module.exports = function(babel) { const { types: t } = babel; return { name: 'remove-console', visitor: { CallExpression(path) { const { callee } = path.node; // 检查是否是 console.log 调用 if ( t.isMemberExpression(callee) && t.isIdentifier(callee.object, { name: 'console' }) && t.isIdentifier(callee.property, { name: 'log' }) ) { // 移除该节点 path.remove(); } } } }; };
示例 2:自动添加函数名
javascript// add-function-name-plugin.js module.exports = function(babel) { const { types: t } = babel; return { name: 'add-function-name', visitor: { FunctionDeclaration(path) { const { node } = path; // 如果函数没有名称,添加一个默认名称 if (!node.id) { node.id = t.identifier('anonymous'); } } } }; };
示例 3:国际化字符串提取
javascript// i18n-plugin.js module.exports = function(babel) { const { types: t } = babel; const strings = []; return { name: 'i18n-extractor', visitor: { StringLiteral(path) { const { node } = path; // 收集所有字符串 strings.push(node.value); // 替换为国际化函数调用 path.replaceWith( t.callExpression( t.identifier('t'), [t.stringLiteral(node.value)] ) ); } }, post(state) { // 输出收集到的字符串 console.log('Extracted strings:', strings); } }; };
3. 使用插件
javascript// babel.config.js module.exports = { plugins: [ './remove-console-plugin.js', ['./i18n-plugin.js', { /* 插件选项 */ }] ] };
Path 对象详解
Path 的核心方法
javascript// 访问者中的 path 对象 visitor: { Identifier(path) { // 节点信息 console.log(path.node); // AST 节点 console.log(path.parent); // 父节点 console.log(path.parentPath); // 父路径 // 节点操作 path.remove(); // 删除节点 path.replaceWith(newNode); // 替换节点 path.insertBefore(newNode); // 在前面插入 path.insertAfter(newNode); // 在后面插入 // 遍历 path.traverse({ ... }); // 子树遍历 path.skip(); // 跳过子树 path.stop(); // 停止遍历 // 检查 path.isIdentifier(); // 检查节点类型 path.findParent((p) => ...); // 查找父节点 path.getFunctionParent(); // 获取函数父节点 path.getStatementParent(); // 获取语句父节点 // 作用域 path.scope.hasBinding('name'); // 检查绑定 path.scope.rename('old', 'new'); // 重命名 path.scope.generateUid('name'); // 生成唯一标识符 } }
高级插件技巧
1. 状态管理
javascriptmodule.exports = function(babel) { return { name: 'stateful-plugin', pre(state) { // 遍历前初始化状态 this.counter = 0; }, visitor: { Identifier(path) { this.counter++; } }, post(state) { // 遍历后输出结果 console.log(`Found ${this.counter} identifiers`); } }; };
2. 处理 JSX
javascript// 将 <div>Hello</div> 转换为 h('div', null, 'Hello') module.exports = function(babel) { const { types: t } = babel; return { name: 'jsx-transform', visitor: { JSXElement(path) { const { openingElement, children } = path.node; const tagName = openingElement.name.name; // 创建 h() 调用 const callExpr = t.callExpression( t.identifier('h'), [ t.stringLiteral(tagName), t.nullLiteral(), ...children.map(child => { if (t.isJSXText(child)) { return t.stringLiteral(child.value.trim()); } return child; }) ] ); path.replaceWith(callExpr); } } }; };
3. 源码映射支持
javascriptmodule.exports = function(babel) { return { name: 'sourcemap-plugin', visitor: { Identifier(path) { // 保留原始位置信息 path.addComment('leading', ` Original: ${path.node.name} `); } } }; };
调试技巧
javascript// 查看 AST const parser = require('@babel/parser'); const code = 'const a = 1'; const ast = parser.parse(code); console.log(JSON.stringify(ast, null, 2)); // 使用 @babel/template 简化节点创建 const template = require('@babel/template').default; const buildRequire = template(` var IMPORT_NAME = require(SOURCE); `); const ast2 = buildRequire({ IMPORT_NAME: t.identifier('myModule'), SOURCE: t.stringLiteral('my-module') });
最佳实践
- 使用
path而非直接操作node- Path 提供更多上下文信息 - 善用
path.scope- 正确处理变量作用域 - 使用
@babel/template- 简化复杂 AST 节点的创建 - 测试插件 - 使用
@babel/core的transformSync进行单元测试 - 参考官方插件 - 学习 Babel 官方插件的实现方式