Introduction to abstract syntax trees (AST)
什么是抽象语法树?
抽象语法树(AST)是源代码结构的一种树状表现形式。树中的每一个节点都代表了源代码中出现的一个特定构造。这里的“抽象”意味着它并不会把真实语法里的每一个细节都表现出来(比如分号、括号等),而是专注于代码的结构和语义内容。
AST 是现代 JavaScript 工具得以运行的基石:
- 代码检查器(如 ESLint) —— 分析代码,找出其中的错误和风格违规之处
- 代码格式化工具(如 Prettier) —— 统一且一致地重新格式化代码
- 转译器(如 Babel) —— 将现代 JS 代码转换为向后兼容的版本
- 打包工具(如 Webpack, Rollup) —— 分析代码依赖并对其进行优化
- 代码修改工具(Codemods) —— 自动化地进行大规模的代码重构
简单来说,AST 就像是把人类写的代码“翻译”成了机器更容易理解和操作的树状数据结构,前面提到的所有工具,其实都是在这棵树上“动手术”的!
为什么用 AST 而不是正则表达式?
虽然正则表达式在处理简单的文本替换时确实能派上用场,但 AST 能提供:
- 结构化的理解 —— 能精准地知道每一段代码具体代表了什么
- 可靠性 —— 能妥善处理各种边缘情况、嵌套结构以及复杂的语法
- 精确性 —— 只转换你真正想要转换的部分,避免误伤(匹配到不该匹配的东西)
- 可维护性 —— 转换逻辑更容易被人理解,也更方便后续扩展
简单打个比方,正则表达式就像是拿着放大镜在文本里“盲目”地找相似的字眼,很容易出错;而 AST 则是拿着这张代码的“结构蓝图”在精准施工,既安全又靠谱!
真实世界的实战应用案例
我曾经接到一个任务,要对一个名为 Arco.Design 的开源组件库的演示(Demo)代码进行重构。旧的演示代码使用的是
ReactDOM.render 配合一个“魔法变量”(magic container variable)来挂载组件,但我希望把它们全部转换成自带完整语法的独立函数式组件,这样就能在现代框架中轻松渲染了。正如这个 Arco.Design 演示代码重构的 Pull Request 里所展示的那样,这样的演示文件有数百个之多。如果纯靠人工手动去改,不仅极其枯燥乏味,还非常容易出错。而通过 AST 转换,我可靠且高效地自动化完成了这次重构。接下来,我将一步步带大家完整走一遍构建这个 AST 转换工具的全过程。
(作者这里提到的“魔法变量”通常是指那种在旧代码里隐式存在、没有显式声明的变量,这种代码在现代开发中确实不太友好,用 AST 来批量“大扫除”简直是再合适不过了!)
先前的代码:
// Legacy demo code using ReactDOM.render with magic container variable
import { Button, Space } from '@arco-design/web-react'
ReactDOM.render(
<Space size="large">
<Button type="primary">Primary</Button>
<Button type="secondary">Secondary</Button>
<Button type="dashed">Dashed</Button>
<Button type="outline">Outline</Button>
<Button type="text">Text</Button>
</Space>,
// Magic variable representing the mount point
CONTAINER
)
之后的代码:
// Self-contained functional component with export
import { Button, Space } from '@arco-design/web-react'
const App = () => {
return (
<Space size="large">
<Button type="primary">Primary</Button>
<Button type="secondary">Secondary</Button>
<Button type="dashed">Dashed</Button>
<Button type="outline">Outline</Button>
<Button type="text">Text</Button>
</Space>
)
}
export default App
两段代码的核心区别(超详细)
1. 代码结构完全不同
- 第一段代码:直接渲染,没有组件封装
- 第二段代码:封装成独立组件,是标准 React 项目写法
2. 是否定义了组件(最大区别)
第一段
直接写
没有创建组件,就是一段 “立即执行的渲染代码”。
ReactDOM.render(...)
第二段
创建了一个函数组件 App,这是现代 React 最标准、最常用的写法。
3. 是否导出组件(能否被其他文件使用)
第一段
没有导出,只能在当前文件自己运行。
第二段
导出了组件,意味着别的文件可以 import 引入使用它。
4. 是否挂载到页面(ReactDOM.render)
第一段
有
直接把内容渲染到页面 DOM 上。
第二段
没有
只定义组件,不负责渲染到页面,渲染交给入口文件(main.jsx/index.jsx)处理。
5. 使用场景不同
第一段代码
- 适合演示、在线编辑器、临时测试
- 不是工程化写法
- 无法复用
第二段代码
- 标准企业项目写法
- 可复用、可维护、可测试
- 符合组件化思想
- 能被路由、父组件、入口文件调用
6. 代码职责不同
- 第一段:定义 UI + 渲染到页面(两件事一起做)
- 第二段:只定义 UI 组件(一件事,单一职责)
一句话总结区别
第一段是 “直接渲染的演示代码”,第二段是 “标准可复用的 React 组件”。
-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
(看来接下来就要开始敲命令、写代码了,准备好你的终端吧!)
一步步构建 AST 转换工具
让我们把这个转换过程拆解成几个容易理解的小步骤。
安装依赖
首先,我们需要安装所需的依赖包:
npm install @babel/parser @babel/traverse @babel/generator
这三个包通常是配合起来打一套“组合拳”的:
- @babel/parser —— 负责把代码字符串解析并转换成 AST(抽象语法树)
- @babel/traverse —— 负责遍历这棵树,并对它进行修改
- @babel/generator —— 负责把修改后的 AST 重新转换回代码
(顺便提一句:这些示例都是基于 JavaScript 的。如果你需要转换的是 TypeScript 代码,记得在使用
@babel/parser 时加上 TypeScript 插件,或者直接去用 TypeScript 官方的编译器 API 哦。)第一步:将代码解析为 AST
把源代码字符串转换成一种结构化的树状表现形式。
(简单来说,就是让程序把原本只是一串文本的代码,变成有层级、有结构的 AST 数据,这样我们才能在下一步对它动手脚!)
const babelParser = require('@babel/parser')
const code = `
import { Button, Space } from '@arco-design/web-react';
ReactDOM.render(
<Space size="large">
<Button type="primary">Primary</Button>
<Button type="secondary">Secondary</Button>
<Button type="dashed">Dashed</Button>
<Button type="outline">Outline</Button>
<Button type="text">Text</Button>
</Space>,
CONTAINER
);
`
const ast = babelParser.parse(code, {
sourceType: 'module', // Enable ES6 imports/exports
plugins: ['jsx'], // Enable JSX syntax parsing
})
我进行代码完善后,可以将AST树结构打印出来:
const babelParser = require('@babel/parser');
const code = `
import { Button, Space } from '@arco-design/web-react';
ReactDOM.render(
<Space size="large">
<Button type="primary">Primary</Button>
<Button type="secondary">Secondary</Button>
<Button type="dashed">Dashed</Button>
<Button type="outline">Outline</Button>
<Button type="text">Text</Button>
</Space>,
CONTAINER
);
`;
// 1. 解析代码生成 AST
const ast = babelParser.parse(code, {
sourceType: 'module',
plugins: ['jsx'], // 必须开启,否则无法解析 JSX
});
// 2. 格式化打印 AST(最关键!美化输出,方便查看)
console.log('===== AST 完整结构 =====');
console.log(JSON.stringify(ast, null, 2)); // 缩进2格,人类可读
// 3. 额外:快速查看 AST 根节点信息(方便调试)
console.log('\n===== AST 根节点简要信息 =====');
console.log('AST 类型:', ast.type);
console.log('程序体数量:', ast.program.body.length);
console.log('第一行节点类型:', ast.program.body[0].type);
AST树结构
"D:\Program Files\nodejs\node.exe" I:\python\AST学习\AST基础应用\code.js
===== AST 完整结构 =====
{
"type": "File",
"start": 0,
"end": 341,
"loc": {
"start": {
"line": 1,
"column": 0,
"index": 0
},
"end": {
"line": 14,
"column": 0,
"index": 341
}
},
"errors": [],
"program": {
"type": "Program",
"start": 0,
"end": 341,
"loc": {
"start": {
"line": 1,
"column": 0,
"index": 0
},
"end": {
"line": 14,
"column": 0,
"index": 341
}
},
"sourceType": "module",
"interpreter": null,
"body": [
{
"type": "ImportDeclaration",
"start": 1,
"end": 56,
"loc": {
"start": {
"line": 2,
"column": 0,
"index": 1
},
"end": {
"line": 2,
"column": 55,
"index": 56
}
},
"specifiers": [
{
"type": "ImportSpecifier",
"start": 10,
"end": 16,
"loc": {
"start": {
"line": 2,
"column": 9,
"index": 10
},
"end": {
"line": 2,
"column": 15,
"index": 16
}
},
"imported": {
"type": "Identifier",
"start": 10,
"end": 16,
"loc": {
"start": {
"line": 2,
"column": 9,
"index": 10
},
"end": {
"line": 2,
"column": 15,
"index": 16
},
"identifierName": "Button"
},
"name": "Button"
},
"local": {
"type": "Identifier",
"start": 10,
"end": 16,
"loc": {
"start": {
"line": 2,
"column": 9,
"index": 10
},
"end": {
"line": 2,
"column": 15,
"index": 16
},
"identifierName": "Button"
},
"name": "Button"
}
},
{
"type": "ImportSpecifier",
"start": 18,
"end": 23,
"loc": {
"start": {
"line": 2,
"column": 17,
"index": 18
},
"end": {
"line": 2,
"column": 22,
"index": 23
}
},
"imported": {
"type": "Identifier",
"start": 18,
"end": 23,
"loc": {
"start": {
"line": 2,
"column": 17,
"index": 18
},
"end": {
"line": 2,
"column": 22,
"index": 23
},
"identifierName": "Space"
},
"name": "Space"
},
"local": {
"type": "Identifier",
"start": 18,
"end": 23,
"loc": {
"start": {
"line": 2,
"column": 17,
"index": 18
},
"end": {
"line": 2,
"column": 22,
"index": 23
},
"identifierName": "Space"
},
"name": "Space"
}
}
],
"source": {
"type": "StringLiteral",
"start": 31,
"end": 55,
"loc": {
"start": {
"line": 2,
"column": 30,
"index": 31
},
"end": {
"line": 2,
"column": 54,
"index": 55
}
},
"extra": {
"rawValue": "@arco-design/web-react",
"raw": "'@arco-design/web-react'"
},
"value": "@arco-design/web-react"
},
"attributes": []
},
{
"type": "ExpressionStatement",
"start": 58,
"end": 340,
"loc": {
"start": {
"line": 4,
"column": 0,
"index": 58
},
"end": {
"line": 13,
"column": 2,
"index": 340
}
},
"expression": {
"type": "CallExpression",
"start": 58,
"end": 339,
"loc": {
"start": {
"line": 4,
"column": 0,
"index": 58
},
"end": {
"line": 13,
"column": 1,
"index": 339
}
},
"callee": {
"type": "MemberExpression",
"start": 58,
"end": 73,
"loc": {
"start": {
"line": 4,
"column": 0,
"index": 58
},
"end": {
"line": 4,
"column": 15,
"index": 73
}
},
"object": {
"type": "Identifier",
"start": 58,
"end": 66,
"loc": {
"start": {
"line": 4,
"column": 0,
"index": 58
},
"end": {
"line": 4,
"column": 8,
"index": 66
},
"identifierName": "ReactDOM"
},
"name": "ReactDOM"
},
"computed": false,
"property": {
"type": "Identifier",
"start": 67,
"end": 73,
"loc": {
"start": {
"line": 4,
"column": 9,
"index": 67
},
"end": {
"line": 4,
"column": 15,
"index": 73
},
"identifierName": "render"
},
"name": "render"
}
},
"arguments": [
{
"type": "JSXElement",
"start": 77,
"end": 324,
"loc": {
"start": {
"line": 5,
"column": 2,
"index": 77
},
"end": {
"line": 11,
"column": 10,
"index": 324
}
},
"openingElement": {
"type": "JSXOpeningElement",
"start": 77,
"end": 97,
"loc": {
"start": {
"line": 5,
"column": 2,
"index": 77
},
"end": {
"line": 5,
"column": 22,
"index": 97
}
},
"name": {
"type": "JSXIdentifier",
"start": 78,
"end": 83,
"loc": {
"start": {
"line": 5,
"column": 3,
"index": 78
},
"end": {
"line": 5,
"column": 8,
"index": 83
}
},
"name": "Space"
},
"attributes": [
{
"type": "JSXAttribute",
"start": 84,
"end": 96,
"loc": {
"start": {
"line": 5,
"column": 9,
"index": 84
},
"end": {
"line": 5,
"column": 21,
"index": 96
}
},
"name": {
"type": "JSXIdentifier",
"start": 84,
"end": 88,
"loc": {
"start": {
"line": 5,
"column": 9,
"index": 84
},
"end": {
"line": 5,
"column": 13,
"index": 88
}
},
"name": "size"
},
"value": {
"type": "StringLiteral",
"start": 89,
"end": 96,
"loc": {
"start": {
"line": 5,
"column": 14,
"index": 89
},
"end": {
"line": 5,
"column": 21,
"index": 96
}
},
"extra": {
"rawValue": "large",
"raw": "\"large\""
},
"value": "large"
}
}
],
"selfClosing": false
},
"closingElement": {
"type": "JSXClosingElement",
"start": 316,
"end": 324,
"loc": {
"start": {
"line": 11,
"column": 2,
"index": 316
},
"end": {
"line": 11,
"column": 10,
"index": 324
}
},
"name": {
"type": "JSXIdentifier",
"start": 318,
"end": 323,
"loc": {
"start": {
"line": 11,
"column": 4,
"index": 318
},
"end": {
"line": 11,
"column": 9,
"index": 323
}
},
"name": "Space"
}
},
"children": [
{
"type": "JSXText",
"start": 97,
"end": 102,
"loc": {
"start": {
"line": 5,
"column": 22,
"index": 97
},
"end": {
"line": 6,
"column": 4,
"index": 102
}
},
"extra": {
"rawValue": "\n ",
"raw": "\n "
},
"value": "\n "
},
{
"type": "JSXElement",
"start": 102,
"end": 141,
"loc": {
"start": {
"line": 6,
"column": 4,
"index": 102
},
"end": {
"line": 6,
"column": 43,
"index": 141
}
},
"openingElement": {
"type": "JSXOpeningElement",
"start": 102,
"end": 125,
"loc": {
"start": {
"line": 6,
"column": 4,
"index": 102
},
"end": {
"line": 6,
"column": 27,
"index": 125
}
},
"name": {
"type": "JSXIdentifier",
"start": 103,
"end": 109,
"loc": {
"start": {
"line": 6,
"column": 5,
"index": 103
},
"end": {
"line": 6,
"column": 11,
"index": 109
}
},
"name": "Button"
},
"attributes": [
{
"type": "JSXAttribute",
"start": 110,
"end": 124,
"loc": {
"start": {
"line": 6,
"column": 12,
"index": 110
},
"end": {
"line": 6,
"column": 26,
"index": 124
}
},
"name": {
"type": "JSXIdentifier",
"start": 110,
"end": 114,
"loc": {
"start": {
"line": 6,
"column": 12,
"index": 110
},
"end": {
"line": 6,
"column": 16,
"index": 114
}
},
"name": "type"
},
"value": {
"type": "StringLiteral",
"start": 115,
"end": 124,
"loc": {
"start": {
"line": 6,
"column": 17,
"index": 115
},
"end": {
"line": 6,
"column": 26,
"index": 124
}
},
"extra": {
"rawValue": "primary",
"raw": "\"primary\""
},
"value": "primary"
}
}
],
"selfClosing": false
},
"closingElement": {
"type": "JSXClosingElement",
"start": 132,
"end": 141,
"loc": {
"start": {
"line": 6,
"column": 34,
"index": 132
},
"end": {
"line": 6,
"column": 43,
"index": 141
}
},
"name": {
"type": "JSXIdentifier",
"start": 134,
"end": 140,
"loc": {
"start": {
"line": 6,
"column": 36,
"index": 134
},
"end": {
"line": 6,
"column": 42,
"index": 140
}
},
"name": "Button"
}
},
"children": [
{
"type": "JSXText",
"start": 125,
"end": 132,
"loc": {
"start": {
"line": 6,
"column": 27,
"index": 125
},
"end": {
"line": 6,
"column": 34,
"index": 132
}
},
"extra": {
"rawValue": "Primary",
"raw": "Primary"
},
"value": "Primary"
}
]
},
{
"type": "JSXText",
"start": 141,
"end": 146,
"loc": {
"start": {
"line": 6,
"column": 43,
"index": 141
},
"end": {
"line": 7,
"column": 4,
"index": 146
}
},
"extra": {
"rawValue": "\n ",
"raw": "\n "
},
"value": "\n "
},
{
"type": "JSXElement",
"start": 146,
"end": 189,
"loc": {
"start": {
"line": 7,
"column": 4,
"index": 146
},
"end": {
"line": 7,
"column": 47,
"index": 189
}
},
"openingElement": {
"type": "JSXOpeningElement",
"start": 146,
"end": 171,
"loc": {
"start": {
"line": 7,
"column": 4,
"index": 146
},
"end": {
"line": 7,
"column": 29,
"index": 171
}
},
"name": {
"type": "JSXIdentifier",
"start": 147,
"end": 153,
"loc": {
"start": {
"line": 7,
"column": 5,
"index": 147
},
"end": {
"line": 7,
"column": 11,
"index": 153
}
},
"name": "Button"
},
"attributes": [
{
"type": "JSXAttribute",
"start": 154,
"end": 170,
"loc": {
"start": {
"line": 7,
"column": 12,
"index": 154
},
"end": {
"line": 7,
"column": 28,
"index": 170
}
},
"name": {
"type": "JSXIdentifier",
"start": 154,
"end": 158,
"loc": {
"start": {
"line": 7,
"column": 12,
"index": 154
},
"end": {
"line": 7,
"column": 16,
"index": 158
}
},
"name": "type"
},
"value": {
"type": "StringLiteral",
"start": 159,
"end": 170,
"loc": {
"start": {
"line": 7,
"column": 17,
"index": 159
},
"end": {
"line": 7,
"column": 28,
"index": 170
}
},
"extra": {
"rawValue": "secondary",
"raw": "\"secondary\""
},
"value": "secondary"
}
}
],
"selfClosing": false
},
"closingElement": {
"type": "JSXClosingElement",
"start": 180,
"end": 189,
"loc": {
"start": {
"line": 7,
"column": 38,
"index": 180
},
"end": {
"line": 7,
"column": 47,
"index": 189
}
},
"name": {
"type": "JSXIdentifier",
"start": 182,
"end": 188,
"loc": {
"start": {
"line": 7,
"column": 40,
"index": 182
},
"end": {
"line": 7,
"column": 46,
"index": 188
}
},
"name": "Button"
}
},
"children": [
{
"type": "JSXText",
"start": 171,
"end": 180,
"loc": {
"start": {
"line": 7,
"column": 29,
"index": 171
},
"end": {
"line": 7,
"column": 38,
"index": 180
}
},
"extra": {
"rawValue": "Secondary",
"raw": "Secondary"
},
"value": "Secondary"
}
]
},
{
"type": "JSXText",
"start": 189,
"end": 194,
"loc": {
"start": {
"line": 7,
"column": 47,
"index": 189
},
"end": {
"line": 8,
"column": 4,
"index": 194
}
},
"extra": {
"rawValue": "\n ",
"raw": "\n "
},
"value": "\n "
},
{
"type": "JSXElement",
"start": 194,
"end": 231,
"loc": {
"start": {
"line": 8,
"column": 4,
"index": 194
},
"end": {
"line": 8,
"column": 41,
"index": 231
}
},
"openingElement": {
"type": "JSXOpeningElement",
"start": 194,
"end": 216,
"loc": {
"start": {
"line": 8,
"column": 4,
"index": 194
},
"end": {
"line": 8,
"column": 26,
"index": 216
}
},
"name": {
"type": "JSXIdentifier",
"start": 195,
"end": 201,
"loc": {
"start": {
"line": 8,
"column": 5,
"index": 195
},
"end": {
"line": 8,
"column": 11,
"index": 201
}
},
"name": "Button"
},
"attributes": [
{
"type": "JSXAttribute",
"start": 202,
"end": 215,
"loc": {
"start": {
"line": 8,
"column": 12,
"index": 202
},
"end": {
"line": 8,
"column": 25,
"index": 215
}
},
"name": {
"type": "JSXIdentifier",
"start": 202,
"end": 206,
"loc": {
"start": {
"line": 8,
"column": 12,
"index": 202
},
"end": {
"line": 8,
"column": 16,
"index": 206
}
},
"name": "type"
},
"value": {
"type": "StringLiteral",
"start": 207,
"end": 215,
"loc": {
"start": {
"line": 8,
"column": 17,
"index": 207
},
"end": {
"line": 8,
"column": 25,
"index": 215
}
},
"extra": {
"rawValue": "dashed",
"raw": "\"dashed\""
},
"value": "dashed"
}
}
],
"selfClosing": false
},
"closingElement": {
"type": "JSXClosingElement",
"start": 222,
"end": 231,
"loc": {
"start": {
"line": 8,
"column": 32,
"index": 222
},
"end": {
"line": 8,
"column": 41,
"index": 231
}
},
"name": {
"type": "JSXIdentifier",
"start": 224,
"end": 230,
"loc": {
"start": {
"line": 8,
"column": 34,
"index": 224
},
"end": {
"line": 8,
"column": 40,
"index": 230
}
},
"name": "Button"
}
},
"children": [
{
"type": "JSXText",
"start": 216,
"end": 222,
"loc": {
"start": {
"line": 8,
"column": 26,
"index": 216
},
"end": {
"line": 8,
"column": 32,
"index": 222
}
},
"extra": {
"rawValue": "Dashed",
"raw": "Dashed"
},
"value": "Dashed"
}
]
},
{
"type": "JSXText",
"start": 231,
"end": 236,
"loc": {
"start": {
"line": 8,
"column": 41,
"index": 231
},
"end": {
"line": 9,
"column": 4,
"index": 236
}
},
"extra": {
"rawValue": "\n ",
"raw": "\n "
},
"value": "\n "
},
{
"type": "JSXElement",
"start": 236,
"end": 275,
"loc": {
"start": {
"line": 9,
"column": 4,
"index": 236
},
"end": {
"line": 9,
"column": 43,
"index": 275
}
},
"openingElement": {
"type": "JSXOpeningElement",
"start": 236,
"end": 259,
"loc": {
"start": {
"line": 9,
"column": 4,
"index": 236
},
"end": {
"line": 9,
"column": 27,
"index": 259
}
},
"name": {
"type": "JSXIdentifier",
"start": 237,
"end": 243,
"loc": {
"start": {
"line": 9,
"column": 5,
"index": 237
},
"end": {
"line": 9,
"column": 11,
"index": 243
}
},
"name": "Button"
},
"attributes": [
{
"type": "JSXAttribute",
"start": 244,
"end": 258,
"loc": {
"start": {
"line": 9,
"column": 12,
"index": 244
},
"end": {
"line": 9,
"column": 26,
"index": 258
}
},
"name": {
"type": "JSXIdentifier",
"start": 244,
"end": 248,
"loc": {
"start": {
"line": 9,
"column": 12,
"index": 244
},
"end": {
"line": 9,
"column": 16,
"index": 248
}
},
"name": "type"
},
"value": {
"type": "StringLiteral",
"start": 249,
"end": 258,
"loc": {
"start": {
"line": 9,
"column": 17,
"index": 249
},
"end": {
"line": 9,
"column": 26,
"index": 258
}
},
"extra": {
"rawValue": "outline",
"raw": "\"outline\""
},
"value": "outline"
}
}
],
"selfClosing": false
},
"closingElement": {
"type": "JSXClosingElement",
"start": 266,
"end": 275,
"loc": {
"start": {
"line": 9,
"column": 34,
"index": 266
},
"end": {
"line": 9,
"column": 43,
"index": 275
}
},
"name": {
"type": "JSXIdentifier",
"start": 268,
"end": 274,
"loc": {
"start": {
"line": 9,
"column": 36,
"index": 268
},
"end": {
"line": 9,
"column": 42,
"index": 274
}
},
"name": "Button"
}
},
"children": [
{
"type": "JSXText",
"start": 259,
"end": 266,
"loc": {
"start": {
"line": 9,
"column": 27,
"index": 259
},
"end": {
"line": 9,
"column": 34,
"index": 266
}
},
"extra": {
"rawValue": "Outline",
"raw": "Outline"
},
"value": "Outline"
}
]
},
{
"type": "JSXText",
"start": 275,
"end": 280,
"loc": {
"start": {
"line": 9,
"column": 43,
"index": 275
},
"end": {
"line": 10,
"column": 4,
"index": 280
}
},
"extra": {
"rawValue": "\n ",
"raw": "\n "
},
"value": "\n "
},
{
"type": "JSXElement",
"start": 280,
"end": 313,
"loc": {
"start": {
"line": 10,
"column": 4,
"index": 280
},
"end": {
"line": 10,
"column": 37,
"index": 313
}
},
"openingElement": {
"type": "JSXOpeningElement",
"start": 280,
"end": 300,
"loc": {
"start": {
"line": 10,
"column": 4,
"index": 280
},
"end": {
"line": 10,
"column": 24,
"index": 300
}
},
"name": {
"type": "JSXIdentifier",
"start": 281,
"end": 287,
"loc": {
"start": {
"line": 10,
"column": 5,
"index": 281
},
"end": {
"line": 10,
"column": 11,
"index": 287
}
},
"name": "Button"
},
"attributes": [
{
"type": "JSXAttribute",
"start": 288,
"end": 299,
"loc": {
"start": {
"line": 10,
"column": 12,
"index": 288
},
"end": {
"line": 10,
"column": 23,
"index": 299
}
},
"name": {
"type": "JSXIdentifier",
"start": 288,
"end": 292,
"loc": {
"start": {
"line": 10,
"column": 12,
"index": 288
},
"end": {
"line": 10,
"column": 16,
"index": 292
}
},
"name": "type"
},
"value": {
"type": "StringLiteral",
"start": 293,
"end": 299,
"loc": {
"start": {
"line": 10,
"column": 17,
"index": 293
},
"end": {
"line": 10,
"column": 23,
"index": 299
}
},
"extra": {
"rawValue": "text",
"raw": "\"text\""
},
"value": "text"
}
}
],
"selfClosing": false
},
"closingElement": {
"type": "JSXClosingElement",
"start": 304,
"end": 313,
"loc": {
"start": {
"line": 10,
"column": 28,
"index": 304
},
"end": {
"line": 10,
"column": 37,
"index": 313
}
},
"name": {
"type": "JSXIdentifier",
"start": 306,
"end": 312,
"loc": {
"start": {
"line": 10,
"column": 30,
"index": 306
},
"end": {
"line": 10,
"column": 36,
"index": 312
}
},
"name": "Button"
}
},
"children": [
{
"type": "JSXText",
"start": 300,
"end": 304,
"loc": {
"start": {
"line": 10,
"column": 24,
"index": 300
},
"end": {
"line": 10,
"column": 28,
"index": 304
}
},
"extra": {
"rawValue": "Text",
"raw": "Text"
},
"value": "Text"
}
]
},
{
"type": "JSXText",
"start": 313,
"end": 316,
"loc": {
"start": {
"line": 10,
"column": 37,
"index": 313
},
"end": {
"line": 11,
"column": 2,
"index": 316
}
},
"extra": {
"rawValue": "\n ",
"raw": "\n "
},
"value": "\n "
}
]
},
{
"type": "Identifier",
"start": 328,
"end": 337,
"loc": {
"start": {
"line": 12,
"column": 2,
"index": 328
},
"end": {
"line": 12,
"column": 11,
"index": 337
},
"identifierName": "CONTAINER"
},
"name": "CONTAINER"
}
]
}
}
],
"directives": [],
"extra": {
"topLevelAwait": false
}
},
"comments": []
}
===== AST 根节点简要信息 =====
AST 类型: File
程序体数量: 2
第一行节点类型: ImportDeclaration
进程已结束,退出代码为 0
解析器会读取代码并生成 AST,其中每一个 JavaScript 结构(比如导入语句、函数调用、JSX)都会变成一个节点。 为了支持 ES6 的导入语法,我们需要指定
sourceType: 'module',同时为了让解析器能读懂 React 的 JSX 语法,我们还需要添加 jsx 插件。在开始编写转换代码之前,为了更好地理解代码是如何映射成 AST 节点的,我们可以使用 AST Explorer 这个工具来可视化地探索 AST 结构:
- 访问网站:打开 astexplorer.net
- 粘贴代码:把你想要分析的代码粘贴到编辑器里
- 实时预览:观察页面上实时生成的 AST 可视化结构
- 查看细节:把鼠标悬停在某个节点上,就能看到它的具体类型和属性
(这个工具真的非常实用,就像给代码做了一次“CT 扫描”,能让你一眼看清代码的“骨骼”结构,写转换规则的时候完全不迷路!)
第二步:识别目标模式
在 AST(抽象语法树)中找到那些目标
ReactDOM.render() 的调用。(简单来说,就是告诉程序:“嘿,去那棵大树里帮我找找,哪些节点是
ReactDOM.render() 这玩意儿,把它们都给我揪出来!”)traverse(ast, {
CallExpression(path) {
// 遍历所有 函数调用(如 console.log、render、setState)
// 判断是不是 ReactDOM.render()
const isReactDOMRender =
path.get('callee').isMemberExpression() && // 是 xx.yy() 形式
path.get('callee.object').isIdentifier({ name: 'ReactDOM' }) && // 对象是 ReactDOM
path.get('callee.property').isIdentifier({ name: 'render' }) // 方法是 render
if (isReactDOMRender) {
console.log('找到 ReactDOM.render 了!')
}
},
})
traverse 使用访问者模式(visitor pattern)来遍历这棵 AST。我们定义的 CallExpression(调用表达式)访问者,会去检查每一个函数调用。我们会判断它是不是一个 MemberExpression(成员表达式,也就是类似 obj.method 这种形式),并且进一步确认它的对象(object)是不是 ReactDOM,属性(property)是不是 render。(简单来说,就是程序会像巡逻一样走过每一个函数调用,然后拿着放大镜比对:“你是不是
ReactDOM.render 呀?”只有完全匹配上了,才会被我们抓出来处理!)第三步:提取 JSX 元素
获取传递给
ReactDOM.render() 的那个 JSX 元素。(毕竟我们的目标是把原本的
ReactDOM.render(<App />, container) 改写成函数式组件,所以得先把里面的 <App /> 给单独拿出来才行~)// 引入 Babel 遍历器(用来遍历/查找 AST 节点)
const traverse = require('@babel/traverse').default
// 开始遍历 AST 语法树
traverse(ast, {
// 访问所有【函数调用】节点(如:ReactDOM.render、console.log、add() 都是 CallExpression)
CallExpression(path) {
// 这里可以先写判断是否是 ReactDOM.render 的逻辑(略,和你之前代码一致)
// if (...) {
// ==============================================
// 核心代码:获取 ReactDOM.render 的第一个参数(也就是要渲染的 JSX 元素)
// ==============================================
// path.get('arguments.0'):
// 从当前函数调用节点中,获取【参数列表】里的【第一个参数】对应的 AST 路径
// .node:取出路径对应的【真实 AST 节点】
const jsxElement = path.get('arguments.0').node
/**
* 上面这行代码的完整解释:
* 1. ReactDOM.render(JSX, 容器) 函数有 2 个参数
* 2. 第一个参数就是我们要渲染的 JSX 元素
* 3. path.get('arguments.0') 定位到第一个参数
* 4. .node 获取这个参数的真实 AST 节点对象
*/
// 打印 JSX 节点的类型(正常输出:JSXElement,代表这是一个 JSX 元素)
console.log('JSX element type:', jsxElement.type)
// }
}
})
ReactDOM.render() 接受两个参数:要渲染的 JSX 元素和 DOM 容器。我们使用 path.get('arguments.0') 来访问第一个参数(也就是那个 JSX),然后再通过 .node 来获取它实际的 AST 节点。(这里的
path.get('arguments.0') 就像是顺着树枝找到了第一个分叉口,而 .node 就是把这个分叉口上结的“果实”——也就是具体的代码数据,直接摘到了我们手里!)第四步:创建函数式组件的 AST
为
const App = () => { return <JSX> } 这段代码构建对应的 AST 节点。(简单来说,刚才我们已经把旧的 JSX 元素提取出来了,现在就要用 Babel 提供的工具,亲手把这些节点拼装成一个新的、完整的函数式组件结构,为最后的替换做好准备!)
// 定义要创建的组件名称,这里是 App
const componentName = 'App'
// 手动构造一个 React 函数组件的 AST 节点结构
// 最终生成的代码:const App = () => { return <JSX /> }
const component = {
type: 'VariableDeclaration', // 节点类型:变量声明(对应 const/let/var)
kind: 'const', // 变量声明类型:使用 const 声明
declarations: [ // 变量声明数组(一个声明可以同时定义多个变量,这里只定义一个)
{
type: 'VariableDeclarator', // 节点类型:变量声明器(负责 变量名 = 值)
id: {
type: 'Identifier', // 节点类型:标识符(就是变量/函数名)
name: componentName, // 变量名:App
},
init: { // 变量初始化的值(也就是 = 后面的内容)
type: 'ArrowFunctionExpression', // 节点类型:箭头函数 () => {}
params: [], // 箭头函数参数:空数组(代表无参数 () =>)
body: {
type: 'BlockStatement', // 节点类型:代码块语句(对应 {} 大括号)
body: [ // 代码块内部的语句列表
{
type: 'ReturnStatement', // 节点类型:return 语句
argument: jsxElement, // return 后面返回的内容(就是之前提取的 JSX 元素)
},
],
},
},
},
],
}
我们正在手动构建一个函数式组件的 AST 结构。每一个 JavaScript 的语法结构,都有一个与之对应的 AST 节点类型:
- VariableDeclaration(变量声明)对应
const - VariableDeclarator(变量声明符)对应赋值部分
- ArrowFunctionExpression(箭头函数表达式)对应箭头函数
- BlockStatement(块级语句)对应那对花括号
{} - ReturnStatement(返回语句)对应
return
(这就好比是在搭积木,虽然我们在代码里写的是连贯的
const App = () => { ... },但在 AST 的世界里,它们其实是由这些不同类型的积木块一层层嵌套拼装起来的!)第五步:创建导出语句
为
export default App 构建对应的 AST。(这样一来,我们刚刚手动拼装好的那个函数式组件,就能被其他文件正常引入和使用啦!)
// 构造一个【默认导出】的 AST 节点
// 最终生成的代码:export default App
const exportDefault = {
// 节点类型:默认导出声明
// 对应 JS 语法:export default ...
type: 'ExportDefaultDeclaration',
// 默认导出的“内容”
declaration: {
// 导出的是一个“标识符”(变量名/组件名)
type: 'Identifier',
// 标识符名称:就是上面定义的组件名 App
name: componentName,
},
}
ExportDefaultDeclaration 这个节点代表的就是 export default 语句。它的 declaration(声明)字段指向了我们具体要导出的内容——在这个例子里,就是一个指向我们组件名字的 Identifier(标识符)节点。(简单来说,AST 里的
ExportDefaultDeclaration 就像是一个快递盒,而 declaration 字段就是盒子里的东西。因为我们导出的是 App 这个组件名,所以盒子里装的就是一个写着“App”名字的标签节点啦!)第六步:修改程序
移除旧的代码,并添加我们全新的组件。
(简单来说,就是把之前找到的那个老式
ReactDOM.render 调用给删掉,然后在同一个位置把我们刚刚辛苦拼装好的新函数式组件给塞进去。大功告成!)// 遍历 AST 抽象语法树
traverse(ast, {
// 监听所有【函数调用表达式】节点(如 ReactDOM.render、console.log 都是)
CallExpression(path) {
// 判断:是否是 ReactDOM.render 调用(你之前写的校验逻辑)
if (/* ...ReactDOM.render 校验判断... */) {
// ==============================================
// 1. 找到 AST 根节点(整个文件的根 Program)
// ==============================================
// path.findParent:向上查找父节点
// p.isProgram():找到类型为 Program 的根节点(整个文件的代码体)
const program = path.findParent((p) => p.isProgram())
// ==============================================
// 2. 删除原来的 ReactDOM.render() 这行代码
// ==============================================
// path.remove():把当前找到的函数调用节点从 AST 中删除
path.remove()
// ==============================================
// 3. 把【新创建的 React 组件】插入到文件代码中
// ==============================================
// program.node.body = 整个文件的代码数组(每一行代码都是一个元素)
// push(component):把我们之前手动构造的 <App> 组件追加到文件里
program.node.body.push(component)
// ==============================================
// 4. 把【export default 导出语句】插入到文件
// ==============================================
// 把 export default App 追加到文件最后
program.node.body.push(exportDefault)
}
}
})
我们向上导航到
Program 节点(也就是整棵 AST 的根节点),先把 ReactDOM.render() 的调用从树里移除,然后将我们新写的组件和导出语句,追加到程序主体(body)的末尾。(这就好比把老零件从机器里拆掉,然后把升级好的新零件组装到机器的最外层框架上,整个代码转换的过程就彻底完成啦!)
执行前(你的原始代码)
执行后(自动变成标准组件)
第七步:生成转换后的代码
将修改后的 AST 重新转换回 JavaScript 代码。
(这一步就像是把刚刚在 AST 世界里重新拼装好的乐高模型,重新拍成一张漂亮的照片,也就是我们最终想要的
.js 文件啦!)// 引入 Babel 代码生成器
// 作用:把修改后的 AST 语法树 重新生成 浏览器/Node 能运行的 JavaScript 代码
const generator = require('@babel/generator').default
// ==============================================
// 执行代码生成
// ==============================================
// generator(修改后的AST, 配置项, 原始代码)
const output = generator(
ast, // 第一个参数:我们**已经修改好的 AST**(删了ReactDOM.render、加了组件、加了导出)
{}, // 第二个参数:生成配置(空对象=使用默认配置,如保留格式、不压缩等)
code // 第三个参数:原始代码(用于生成 SourceMap 时做对比,一般传原始code即可)
)
// ==============================================
// 输出最终生成的代码
// ==============================================
// output.code 就是生成好的、完整的 JavaScript 字符串
console.log(output.code)
@babel/generator 会遍历整棵 AST,并将每个节点重新转换回源代码。它会自动处理代码的格式化,不过最终生成的代码格式可能无法与原始代码完全一模一样。(简单来说,它的任务就是把我们刚刚在 AST 里拼装好的新组件“打印”成文本。虽然代码逻辑是绝对准确的,但像空格、换行这些排版细节,它可能会按照自己的默认风格来,不一定能完美复刻你原来的代码风格哦~)
输出如下所示:
import { Button, Space } from '@arco-design/web-react'
const App = () => {
return (
<Space size="large">
<Button type="primary">Primary</Button>
<Button type="secondary">Secondary</Button>
<Button type="dashed">Dashed</Button>
<Button type="outline">Outline</Button>
<Button type="text">Text</Button>
</Space>
)
}
export default App
完整解决方案
既然我们已经理解了每一个步骤,这里就把它们整合成一个连贯的脚本,来展示完整的转换过程:
(这就好比把前面拆开的乐高零件,一次性拼成最终的成品啦!准备好看看完整的代码了吗?)
const babelParser = require('@babel/parser')
const traverse = require('@babel/traverse').default
const generator = require('@babel/generator').default
const code = `
import { Button, Space } from '@arco-design/web-react';
ReactDOM.render(
<Space size="large">
<Button type="primary">Primary</Button>
<Button type="secondary">Secondary</Button>
<Button type="dashed">Dashed</Button>
<Button type="outline">Outline</Button>
<Button type="text">Text</Button>
</Space>,
CONTAINER
);
`
// Parse the code into an AST
const ast = babelParser.parse(code, {
sourceType: 'module',
plugins: ['jsx'],
})
// Transform the AST
traverse(ast, {
CallExpression(path) {
// Find ReactDOM.render() calls
if (
path.get('callee').isMemberExpression() &&
path.get('callee.object').isIdentifier({ name: 'ReactDOM' }) &&
path.get('callee.property').isIdentifier({ name: 'render' })
) {
const componentName = 'App'
const jsxElement = path.get('arguments.0').node
// Create functional component: const App = () => { return <JSX> }
const component = {
type: 'VariableDeclaration',
kind: 'const',
declarations: [
{
type: 'VariableDeclarator',
id: { type: 'Identifier', name: componentName },
init: {
type: 'ArrowFunctionExpression',
params: [],
body: {
type: 'BlockStatement',
body: [
{
type: 'ReturnStatement',
argument: jsxElement,
},
],
},
},
},
],
}
// Create export: export default App
const exportDefault = {
type: 'ExportDefaultDeclaration',
declaration: { type: 'Identifier', name: componentName },
}
// Modify the program
const program = path.findParent((p) => p.isProgram())
path.remove()
program.node.body.push(component)
program.node.body.push(exportDefault)
}
},
})
// Generate transformed code
const output = generator(ast, {}, code)
console.log(output.code)

常见的 AST 节点类型参考
在处理 JavaScript 的 AST(抽象语法树)时,你经常会遇到下面这些节点类型:
(这就像是 AST 世界的“常用词汇表”,掌握了这些,看起代码树状结构来就轻松多啦!)
| Node Type | Represents | Example |
|---|---|---|
Program |
Root node containing all code | (entire file) |
ImportDeclaration |
Import statements | import React from 'react' |
ExportDefaultDeclaration |
Default exports | export default App |
VariableDeclaration |
Variable declarations | const x = 1 |
FunctionDeclaration |
Function definitions | function foo() {} |
ArrowFunctionExpression |
Arrow functions | () => {} |
CallExpression |
Function calls | foo() |
MemberExpression |
Property access | obj.prop |
Identifier |
Variable/function names | myVariable |
JSXElement |
JSX tags | <div>...</div> |
BlockStatement |
Code blocks | { ... } |
ReturnStatement |
Return statements | return value |
最佳实践
在进行 AST 转换时,建议遵循以下准则:
- 充分测试:AST 转换可能会遇到一些细微的边缘情况。请编写全面的测试用例,并用各种代码模式进行测试,包括嵌套结构、注释以及不常见的代码格式。
- 优雅地处理错误:在转换逻辑外包上 try-catch 语句,并在处理前对输入进行校验。
- 从简单入手:在正式写转换代码之前,先在 AST Explorer 上进行实验。动手转换前,一定要先验证你对 AST 结构的猜想是否正确。
- 说明代码意图:AST 代码往往比较复杂,记得加上注释,解释清楚每一步转换具体是做什么的。
更多资源
- AST Explorer:交互式的 AST 可视化工具。
- Babel Plugin Handbook:深入钻研 Babel 插件开发的权威指南。
- jscodeshift:Facebook 推出的一个更高级、更易用的代码修改(codemod)工具包。
结语
AST 解锁了极其强大的代码转换能力,这远远超出了普通正则表达式所能达到的极限。虽然学习曲线确实稍微陡峭一些,但在可靠性、精确度和可维护性方面,AST 绝对是任何严肃代码处理任务的专业首选。不妨从简单的转换开始探索 AST,你很快就会发现在自动化处理繁琐的重构任务,以及确保代码库整体一致性方面,它能为你带来多大的帮助!

浙公网安备 33010602011771号