实践来源
最近参与了一个 Node 项目脚手架的开发工作,为了提高编码效率,导师提议写一个 VSCode 的插件,功能上大体有点像 snippets 代码段,但比 snippets 优秀的地方是,插件还能实现以下两大功能:
- 可遍历目前工程目录下所有的 @provide ,结合 VSCode API 可以实现快速添加 @inject
- 可识别相应文件代码段,灵活插入代码段
TypeScript
该 Node 项目由 TypeScript 编写,虽然 TypeScript 在前期编写时对变量类型的定义约束需要消耗我们额外的一点精力,但不得不说的是,在后期 Coding 阶段,配合宇宙编辑器 VSCode 的代码提示,写代码可以跟开火箭一样,行云流水。
回到 AST 的话题中,因为 TypeScript 在近几年才算热门,AST 在 TypeScript 的应用上的优秀实践也难得一见,相关的文档及教程也不算太完整,于是开始了 AST in TypeScript 的踩坑之旅。
AST with Babel
Babel 是一个 JavaScript 编译器,主要用于将 ECMAScript 2015 版本的代码转换为向后兼容的 JavaScript 语法,以便能够运行在当前和旧版本的浏览器或其他环境中。
Babel 主要通过三个步骤实现以上的流程:解析(Parse)、转换(transform)、生成(generate),对应的Babel提供了许多库去完成以上的事情。
插件实现的大概思路如下:
1.读取 api/index.ts 中定义的好的接口文件,并组合成一个数组,供开发者选择。
2.然后当开发者在想要插入 API 接口时,插件会调用 VSCode 的 vscode.window.showQuickPick
API,弹出 QuickPick 供开发者选择。
3.当开发者选择接口后,为了防止重复引用,插件会去判断当前文件是否已经引用了该接口模块,如果已经引入则报错,如果没有,则会去判断接口应该插入的位置。
4.完成接口模块在当前代码段的插入。
下面会大概介绍完成以上工作所用到的 Babel 库。
@babel/parser
想要在 JavaScript 代码的特定位置中插入代码,我们就需要先解析目前的代码段。
代码语言:txt复制// currentFileContent 为当前文件的字符串
require('@babel/parser').parse(currentFileContent, {
sourceType: 'module',
plugins: [
'typescript',
['decorators', { decoratorsBeforeExport: true }],
'classProperties',
'classPrivateProperties'
]
})
通过这种方式,我们就可以把当前的代码转换为 AST 了
感兴趣的同学可以到 AST Explorer 尝试一下,这个工具可以解析你提供的 JavaScript 代码,并且会以一种非常直观的图形化结构语法书呈现。
我们可以尝试一下解析这三行简单的 JavaScript 代码,
代码语言:txt复制let foo = 'Hello'
function PrintHello(){
console.log(foo)
}
在 AST Explorer 中,上面三行代码被解析为这样的结构。
在 AST Explorer 中,我们甚至查看生成的 JSON 格式的解析结果。
在解析后得到 AST 后,下一步我们就需要开始分析它的结构了。
@babel/traverse
在查看解析得到 AST 的 JSON 解析结果后,我们可以发现即便是几行简单的代码也会解析得到几百行的 JSON 结果,为了让我们可以快速得到想要的节点,我们可以使用 babel/traverse 这个工具,进行对 AST 快速的节点遍历与筛选。
代码语言:txt复制traverse(fileAST, {
ImportDeclaration: function(path) {
if (path.node.source.value === 'api/apis') {
// 判断是否重复引用 API 逻辑
let currentApi = path.node.specifiers.map(item => {
return item.local.name
})
if (currentApi.indexOf(FirstLetterToUpperCase(name)) === -1) {
path.pushContainer('specifiers', APINode)
// TODO 加入判断 inject 是否重复逻辑
let injectNode = InjectNodeConstructor(name)
traverse(fileAST, {
ClassBody: function(path) {
path.node.body.splice(
path.node.body.length - 1,
0,
injectNode
)
}
})
} else {
vscode.window.showInformationMessage('该 API 已经引入')
path.stop()
return
}
}
}
})
如上文所描述,通过 babel/traverse 这个工具,我们可以首先遍历一次 AST 去判断之前是否已经引用过相应的接口模块。
babel/traverse 非常强大,它支持绝大部分类型节点的筛选,具体的文档,可查看 Babel-Handbook .
@babel/types
这个插件的核心功能,就是将开发者选择的接口模块,变成代码插入到当前代码段中,那么在构造新的代码段这个过程中,babel/types 就派上用场了。
通过 babel/types 的 API ,我们可以很方便的构造出对应的 AST 语法块,而后加入到 AST 中。
代码语言:txt复制// API 节点构造器
function APINodeConstructor(apiName: string): t.ImportSpecifier {
return t.importSpecifier(
t.identifier(FirstLetterToUpperCase(apiName 'API')),
t.identifier(FirstLetterToUpperCase(apiName 'API'))
)
}
// Service 节点构造器
function ServiceNodeConstructor(
serviceName: string,
servicePath: string
): t.ImportDeclaration {
return t.importDeclaration(
[
t.importSpecifier(
t.identifier(FirstLetterToUpperCase(serviceName)),
t.identifier(FirstLetterToUpperCase(serviceName))
)
],
t.stringLiteral(servicePath)
)
}
如上,通过 babel/types 的 t. 封装好了相应的节点构造器,只需传入对应的 Name 参数即可返回相应的 AST 节点。
同样,babel/types 也支持多种节点类型的构造,具体文档可参考Babel - types .
@babel/generator
最后,我们已经完成对 AST 的查找,更改,插入操作了,下一步就是把 AST 转换成 JavaScript 代码了,这时候我们就会用到 babel/generator .
同样,babel/generator 的用法非常简单:
代码语言:txt复制import {parse} from '@babel/parser';
import generate from '@babel/generator';
...
const generateCode = generate(outCode, {
retainLines: true,
sourceMaps: false,
decoratorsBeforeExport: true
})
generate
第一个参数为需要编译成代码的 AST ,第二个参数为 options,具体可参考 Babel - generator.
至此,整个插件的核心流程就完成了。
总结
上次了解到 AST 还是在分析 Vue.js 是如何编译 Template 的,但没有深入去细究(虽然这次也不算太深入),这次的实践过程大概了解了 Babel 对于代码处理的过程以及所使用到的一些库。
篇幅有限,只是简单描述了一些 Babel 工具库的大概用法,也只是简单描述了插件实现的大概思路。
下一步计划是,等到把这个插件真正完善后,再详细写一篇关于该插件具体思路及改进的问题。
初次尝试,如有错误内容,敬请原谅,烦请多多指教!