超实用的AST的基本操作
很多小伙伴在学习前端过程中,会遇到AST的解析。比如,vue源码,react源码,甚至很多底层框架里面随着大量业务的迁移架构的过程中,大大小小都会遇到需要批量转换代码的问题
本文适合在不了解AST原理知识的情况下,仍然对AST充满好奇心的开发者们。
什么是AST?
我们可以把AST看成一棵千变万化的树,它能够变成任何我们开发中想要的东西。
抽象语法🌲(Abstract Syntax Tree) 简称AST,是以树状形式表现编程语言的语法结构,树上的每个节点都表示源代码中的一种结构。JavaScript引擎工作工作的第一步就是将代码解析为AST,Babel、eslint、prettier等工具都基于AST。
调试工具推荐
在正式学习AST之前,我们需要准备几个趁手的工具来帮助我们学习。
AST exporter
这是一个AST的在线调试工具,有了它,我们可以很直观的看到AST生成前后以及代码公户,它分为5个部分,我们接下来都依赖这个工具进行代码操作
👁图:
在toolTopBar
区域,Transform
下有个jscodeshift
,接下来我们重点讲下它
jscodeshift
它是一个AST的转换器,我们通过它来讲原始代码转成ast语法树,并借助其开放的api操作ast,最终转换成我们想要的代码
jscodeshift
的api
基于recast
封装。recast
是对babel/travers&babel/types
的封装,他提供简易的ast操作,而travers
是babel中用于操作ast
的工具,type
s我们可以先把它理解成为一个字典,用于描述树的类型。
同时,jscodeshift
还提供额外的功能,使得开发者们可以在项目开发调试阶段投入使用,同时不需要去感知babel转译前后的过程,只需要专注于如何操作或者改变树,并得知结果。
AST的权威API
babel-types
ast语法字典,方便我们快速查阅结构树的类型,它是我们想要通过ast生成某行代码的重要工具之一
假如我们有这样一段代码
var a= 1
转换成AST之后,以JSON格式展示如下;(注意:此时我选择了jscodeshift作为转义器了,并选取了JSON展示的核心代码)
{
"type": "Program",
"sourceType": "script",
"body": [
{
"type": "VariableDeclaration",
"kind": "var",
"declarations": [
{
"type": "VariableDeclarator",
"id": {
"type": "Identifier",
"name": "a"
},
"init": {
"type": "Literal",
"value": 1
}
}
]
}
]
}
当你操作对象init中的value值1改为2时,对应的js也会跟着改变var a = 2
,当把name对应的值改成b时,对应的var a = 1
也会改变var b = 1
不知道大家是否能想到什么呢?我总结的一个结论就是: AST并不难,不就是AST操作一组有规则的 JSON
AST的节点
探索AST节点类型
常用节点含义对照表
大家不要觉得多,你真想懂AST是什么,就把这个类型详细的看一遍表达的意思,看完规则后瞬间明白AST
的json中,那些看不懂的type它到底是个什么玩意了(详细信息可参考@babel/types),真的就是描述语法的词汇罢了!
jscodeshift简易操作
查找
api | 类型 | 参数 | 描述 |
---|---|---|---|
find | fn | type:ast类型 | 找到所有符合筛选条件的ast类型的ast字节,并返回一个array |
forEach | fn | Callback:接受一个回调,默认传递被调用的ast节点遍历ast节点,仝js的forEach函数 |
更多可通过ast explore 在操作区console查看、或直接查看 jscodeshift/collections
我们先来看一个例子,假设有如下代码
import * as React from 'react';
import styles from './index.module.scss';
import { Button } from "@alifd/next";
const Button = () => {
return (
<div>
<h2>转译前</h2>
<div>
<Button type="normal">Normal</Button>
<Button type="primary">Prirmary</Button>
<Button type="secondary">Secondary</Button>
<Button type="normal" text>Normal</Button>
<Button type="primary" text>Primary</Button>
<Button type="secondary" text>Secondary</Button>
<Button type="normal" warning>Normal</Button>
</div>
</div>
);
};
export default Button;
执行文件(通过jscodeshift进行操作)
module.exports = (file, api) => {
const j = api.jscodeshift;
const root = j(file.source);
root
.find(j.ImportDeclaration, { source: { value: "@alifd/next" } })
.forEach((path) => {
path.node.source.value = "antd";
})
root
.find(j.JSXElement, {openingElement: { name: { name: 'h2' } }})
.forEach((path) => {
path.node.children = [j.jsxText('转译后')]
})
root
.find(j.JSXOpeningElement, { name: { name: 'Button' } })
.find(j.JSXAttribute)
.forEach((path) => {
const attr = path.node.name
const attrVal = ((path.node.value || {}).expression || {}).value ? path.node.value.expression : path.node.value
if (attr.name === "type") {
if (attrVal.value === 'normal') {
attrVal.value = 'default'
}
}
if (attr.name === "size") {
if (attrVal.value === 'medium') {
attrVal.value = 'middle'
}
}
if (attr.name === "warning") {
attr.name = 'danger'
}
if (attr.name === "text") {
const attrType = path.parentPath.value.filter(item => item.name.name === 'type')
attr.name = 'type'
if (attrType.length) {
attrType[0].value.value = 'link'
j(path).replaceWith('')
} else {
path.node.value = j.stringLiteral('link')
}
}
});
return root.toSource();
}
该例代码大致解读如下
- 将js转换为ast
- 遍历代码中所有包含@alifd/next的引用模块,并做如下操作
- 改变该模块名为antd。
- 找到代码中标签名为h2的代码块,并修改该标签内的文案。
- 遍历代码中所有Button标签,并做如下操作
- 改变标签中type和size属性的值
- 改变标签中text属性变为 type = "link"
- 改变标签中warning属性为danger
- 返回由ast转换后的js。
最终输出结果
import * as React from 'react';
import styles from './index.module.scss';
import { Button } from "antd";
const Button = () => {
return (
<div>
<h2>转译后</h2>
<div>
<Button type="default">Normal</Button>
<Button type="primary">Prirmary</Button>
<Button type="secondary">Secondary</Button>
<Button type="link" >Normal</Button>
<Button type="link" >Primary</Button>
<Button type="link" >Secondary</Button>
<Button type="default" danger>Normal</Button>
</div>
</div>
);
};
export default Button;
代码说明
-
获取必要的数据
// 获取操作ast用的api,获取待编译的文件主体内容,并转换为AST结构。 // api.jscodeshift : 对jscodeshift库的引用 // api.stats : --dry运行期间收集统计信息的功能 // api.report : 将传递的字符串打印到stdout const j = api.jscodeshift; const root = j(file.source); // file.source: 待操作的文件主体 file.path : 文件路径
执行jscodeshift命令后,执行文件接收 3 个参数
代码转换
// root: 被转换后的ast跟节点
root
// ImportDeclaration 对应 import 句式
.find(j.ImportDeclaration, { source: { value: "@alifd/next" } })
.forEach((path) => {
// path.node 为import句式对应的ast节点
path.node.source.value = "antd";
})
遍历代码中所有包含@alifd/next的引用模块,并做如下操作
-
改变模块名
root // JSXElement 对应 element 完整句式,如 <h2 ...> ... </h2> // openingElement 对应 element 的 开放标签句式, 如 <h2 ...> .find(j.JSXElement, {openingElement: { name: { name: 'h2' } }}) .forEach((path) => { // jsxText 对应 text path.node.children = [j.jsxText('转译后')] })
-
筛选标签为h2的html,更改该标签的内容的text为“转译后”
root // 筛选Button的 element开放句式 .find(j.JSXOpeningElement, { name: { name: 'Button' } }) // JSXAttribute 对应 element 的 attribute 句式, 如 type="normal" ... .find(j.JSXAttribute) .forEach((path) => { const attr = path.node.name const attrVal = ((path.node.value || {}).expression || {}).value ? path.node.value.expression : path.node.value if (attr.name === "type") { if (attrVal.value === 'normal') { attrVal.value = 'default' } } if (attr.name === "size") { if (attrVal.value === 'medium') { attrVal.value = 'middle' } } if (attr.name === "warning") { attr.name = 'danger' } if (attr.name === "text") { // 判断该ast节点的兄弟节点是否存在 type, // 如果有,则修改type的值为link,如果没有则改变当前节点为type=“link” const attrType = path.parentPath.value.filter(item => item.name.name === 'type') attr.name = 'type' if (attrType.length) { attrType[0].value.value = 'link' j(path).replaceWith('') } else { // stringLiteral 对应 string类型字段值 path.node.value = j.stringLiteral('link') } } });