我正在参加「掘金·启航计划」
前言
👮🏻面试官:“你好,看你简历,你说你会AST,请介绍一下你对AST的理解”
👦🏻我:“你好!AST代表抽象语法树,它是源代码的结构化表示。在前端开发中,AST可以用于代码解析、分析和转换。”
👮🏻面试官:“那么你能举一个AST在代码转换方面的具体应用例子吗?”
👦🏻我:“比如使用babel将es6转成es5。”
👮🏻面试官:“除了代码转换,AST还能在哪些方面发挥作用?”
👦🏻我:“AST在静态分析、代码检查和自动化任务中也发挥重要作用。我们可以使用AST进行代码的静态分析,如查找未使用的变量、检测代码风格问题等。另外,AST还可以用于生成文档、创建自定义工具和进行性能优化等方面。”
👮🏻面试官:“来、那你来给我写个插件?”
👦🏻我😳:“额,我觉得。。。先这样,然后这样,再这样。。。嗯,对,就这样。。。”
👮🏻面试官:“好的,回去等消息吧。”
。。。
一脸懵逼。
😳 那么到底怎么使用AST来处理代码呢?虽然我们了解过AST,但是很少用过,毕竟在平时的工作中很少有使用场景。
哎,卷起来吧。
什么是AST
这个问题,大家都知道的,这里就简单描述一下😊:
AST(抽象语法树)是一种表示编程语言语法结构的树形结构,它将代码中的每个语句和表达式转换为树形结构中的一个节点。AST可以看作是源代码的一种抽象表示形式,它可以帮助开发者更好地理解代码的结构和逻辑。
例如,下面是一个简单的JavaScript代码示例:
function greet(name) {
console.log('Hello, ' + name + '!');
}
greet('World');
上述代码可以转换为以下AST:
Program
|- FunctionDeclaration: greet
| |- Identifier: name
| |- BlockStatement
| |- ExpressionStatement
| |- CallExpression
| |- MemberExpression
| | |- Identifier: console
| | |- Identifier: log
| |- BinaryExpression
| |- BinaryExpression
| | |- Literal: 'Hello, '
| | |- Identifier: name
| |- Literal: '!'
|- ExpressionStatement
|- CallExpression
|- Identifier: greet
|- Literal: 'World'
可以看到,AST以树形结构表示了代码中的各个语句和表达式,每个节点代表一种语法结构。例如FunctionDeclaration、Identifier、BlockStatement、ExpressionStatement等。
在AST中,节点之间的父子关系代表了它们在代码中的嵌套关系。就好比函数嵌入函数。
概念还是非常简单的。上过学的都看的懂。
那么问题来了,代码是怎么转成AST的呢?我们自己转?
当然不用,有现成的工具可以用。比如:@babel/parser
,acorn
等JS工具。本篇文章也不会去介绍怎么把代码字符串转成AST,虽然不难,但是没有这个必要自己去写个转换的工具。😂
本文使用了@babel/parser
,毕竟最流行嘛。开源就是好。
既然已经有办法转成AST了,那接下来就交给我吧。
插件开发
虽然是插件开发实战,但是本文主要说的是AST的能力,省略了插件的开发过程,关注AST的核心开发能力。
假设我们已经有一个插件开发项目:LegComments(后腿哥注释),一个给js函数添加注释模板的插件。
VSCode扩展商店应该有很多类似的插件了,有的达到了几十万下载量了,所以我们开发的这个插件还是很有市场的。
哈哈!🚀
想想就很激动,我们再开发一个下载量几十万的插件。
准备工作
先来分析一下,这个插件的功能
- 获取当前js文件中的所有内容、转成AST
- 遍历AST
- 找到函数声明
- 找到函数的参数
- 按照固定的模板生成多行注释
- 在函数前面添加注释
- 将处理后的AST生成代码文本
- 替换当前js文件内容
因为我们使用babel来转换,所以需要安装以下依赖:
npm i –save @babel/parser @babel/traverse @babel/types @babel/generator
介绍一下各个包的作用:
@babel/parser
将代码文本字符串转成(AST)。
@babel/parser
的主要作用包括:
-
解析代码:
@babel/parser
接受一段代码作为输入,并将其解析为相应的AST表示。它可以处理不同版本的JavaScript代码,包括ES5、ES6和更高版本的代码。 -
生成AST:
@babel/parser
将代码解析为一棵AST,它是一个树状结构,用于表示代码的语法结构和含义。AST由一系列节点组成,每个节点表示代码的一个部分,如表达式、语句、函数等。 -
支持扩展:
@babel/parser
支持插件系统,允许开发者根据需要添加自定义的解析规则或扩展现有的解析功能。这使得@babel/parser
可以处理一些非标准语法或特定领域的语言扩展。
@babel/traverse
提供了用于遍历和修改AST的功能。它允许开发者在AST上进行深度遍历,查找特定节点,进行节点替换或修改,以及执行各种其他操作。
@babel/traverse
的主要作用:
- 遍历AST
- 查找特定节点
- 修改节点
@babel/types
用于创建、操作和检查AST节点的功能。它允许开发者以编程方式创建和修改AST节点,而无需手动构建AST节点的数据结构。
@babel/types
的主要作用:
- 创建AST节点:
@babel/types
提供了一系列的工厂函数,用于创建各种类型的AST节点。例如,可以使用t.identifier(name)创建一个标识符节点,使用t.stringLiteral(value)创建一个字符串字面量节点。 - 操作AST节点:
@babel/types
提供了一系列的操作函数,用于在AST节点上进行常见的操作。例如,可以使用t.isIdentifier(node)来检查一个节点是否是标识符节点,使用t.cloneNode(node)来克隆一个节点。 - 修改AST节点:
@babel/types
提供了很多操作函数,来对AST节点进行修改、删除或替换。例如,可以使用path.replaceWith(newNode)来用新的节点替换一个节点,使用path.insertBefore(newNode)在当前节点之前插入一个新节点。
@babel/generator
提供了将AST节点转换为字符串表示的功能,使得开发者可以将修改后的AST重新生成为代码。
@babel/generator
的主要作用:
生成代码:@babel/generator
接受一个AST作为输入,并将其转换为相应的代码字符串表示。它将AST节点逐个遍历,并将节点转换为代码字符串,最终生成完整的代码。
保留代码格式:@babel/generator
会尽可能地保留代码的格式,包括缩进、换行和空格等。这样可以确保生成的代码与原始代码在可读性和格式上保持一致。
支持配置:@babel/generator
提供了一些配置选项,允许开发者根据需要定制生成的代码的输出。例如,您可以配置是否使用分号来结束语句、是否使用单引号或双引号来表示字符串等。
开发
导入相关模块:
// 导入依赖包
const parser = require('@babel/parser');
const traverse = require('@babel/traverse').default;
const t = require('@babel/types');
const generator = require('@babel/generator').default;
解析AST
获取当前js文件内容,解析成ast。
// 获取当前编辑器
let editor = vscode.window.activeTextEditor;
let text=editor.document.getText();
if(!text){
return
}
let ast = parser.parse(text);
添加注释
使用@babel/traverse
来遍历处理AST。
//添加注释
function addComment(node) {
const params = node.params;
if (!(params && params.length > 0)) {
return
}
var comments = ['*', "* 该注释由后腿哥为你生成"]
params.map(param => {
comments.push(`* @param {*} ${param.name}`)
})
comments.push('');
t.addComment(node, 'leading', comments.join('\n'))
}
// 遍历AST树
traverse(ast, {
//查找类方法
ClassMethod(path) {
addComment(path.node)
},
//查找函数声明
FunctionDeclaration(path) {
addComment(path.node)
}
});
注意
leadingComments 表示头部注释
前面说到@babel/traverse
它允许开发者在AST上进行深度遍历,查找特定节点,进行节点替换或修改,以及执行各种其他操作。
上面代码中,我们通过traverse来查找ClassMethod(类成员函数)
,FunctionDeclaration(函数声明)
类型的节点,然后给他们添加多行注释addComment
。
注意
@babel/traverse有很多遍历器,可以查看文档获取。这些遍历器都是定义在
@babel/types
中的
添加注释,我们使用@babel/types
提供的apiaddComment
来实现。
测试一下:
输入:
function add(a, b) {
return a + b;
}
function output(name){
}
输出:
/**
* 该注释由后腿哥为你生成
* @param {*} a
* @param {*} b
*/
function add(a, b) {
return a + b;
}
/**
* 该注释由后腿哥为你生成
* @param {*} name
*/
function output(name) {}
可以发现,功能正常。
但是有个问题,每次执行的插件的时候都会生一个注释,和以前的注释重复了,所以我们还得需要一个删除注释的功能。
但是删除也只能删除由插件生成的注释,总不能把之手动写的注释也删除了吧。
添加一个函数用于判断是否已经添加了注释
//判断是否已经添加过注释
function hasAddComment(path){
const leadingComments = path.node.leadingComments;
if(leadingComments){
return leadingComments.find(a=>{
return a.value.indexOf('该注释由后腿哥为你生成')!=-1
})
}else{
return false
}
}
上面的代码,我们直接判断函数前是否有leadingComments
,并且判断是否由我们的插件生成的。该注释由后腿哥为你生成
。
这样就可以判断出来了,然后我们还得写一个删除注释的,功能,因为可能觉得插件生成的注释不好,所以得要删除,或者构建生产代码的时候,需要删除注释。
删除注释
节点的函数,最简单了,可以过直接赋值的形式,或者调用node.remove
来实现。
//移除通过插件添加的注释
function removeLeadingComments(path){
const leadingComments = path.node.leadingComments;
if (leadingComments && leadingComments.length > 0) {
path.node.leadingComments = [];
}
}
上面的代码,我们直接将函数节点的leadingComments
置空。好了。试试我们的插件吧。
可以看到首次运行插件。函数添加上了注释,再次运行插件,注释删除了。是不是很神奇
完整的插件代码
将整个extension.js
的代码贴出,仅供参考
const vscode = require('vscode');
// 导入Babel/Parse依赖包
const parser = require('@babel/parser');
const traverse = require('@babel/traverse').default;
const t = require('@babel/types');
const generator = require('@babel/generator').default;
function activate(context) {
let disposable = vscode.commands.registerCommand('add-comment-template.add', function () {
// 获取当前编辑器
let editor = vscode.window.activeTextEditor;
let text=editor.document.getText();
//解析JavaScript代码
let ast = parser.parse(text);
function addComment(node) {
const params = node.params;
if (!(params && params.length > 0)) {
return
}
var comments = ['*', "* 该注释由后腿哥为你生成"]
params.map(param => {
comments.push(`* @param {*} ${param.name}`)
})
comments.push('');
t.addComment(node, 'leading', comments.join('\n'))
}
function hasAddComment(path){
const leadingComments = path.node.leadingComments;
if(leadingComments){
return leadingComments.find(a=>{
return a.value.indexOf('该注释由后腿哥为你生成')!=-1
})
}else{
return false
}
}
function removeLeadingComments(path){
const leadingComments = path.node.leadingComments;
if (leadingComments && leadingComments.length > 0) {
path.node.leadingComments = [];
}
}
//获取整个编辑器的内容范围
function getFullRange(editor){
let document = editor.document;
let lastLine = document.lineCount - 1;
let range = new vscode.Range(0, 0, lastLine, document.lineAt(lastLine).text.length);
return range
}
// 遍历AST树
traverse(ast, {
//查找类方法
ClassMethod(path) {
addComment(path.node)
},
//查找函数声明
FunctionDeclaration(path) {
if(hasAddComment(path)){
removeLeadingComments(path)
}else{
addComment(path.node)
}
}
});
// 生成JavaScript代码并替换编辑器中的文本
let newCode = generator(ast).code;
editor.edit((editBuilder) => {
editBuilder.replace(getFullRange(editor), newCode);
});
});
context.subscriptions.push(disposable);
}
function deactivate() { }
module.exports = {
activate,
deactivate
}
总结
通过本文的示例,我们展示了如何使用AST开发自定义插件。我们从面试的要求开始,逐步实现了一个给JS函数添加注释的插件,并通过Babel进行代码转换。AST作为强大的工具,让我们能够深入分析和修改代码,为我们定制化的需求提供了解决方案。
AST在前端工具和插件开发中有广泛的应用,它可以用于自动化任务、代码检查、性能优化等方面。掌握AST的使用将有助于提升我们的开发效率和代码质量。
希望本文能够对您理解和应用AST有所帮助。通过学习和实践,您可以发现更多有趣和实用的AST应用场景。祝您在前端开发的旅程中取得更多的成就!
也期待与大家的交流!
原文链接:https://juejin.cn/post/7233324626418794554 作者:前端后腿哥