AST in TypeScript 实践

实践来源

  最近参与了一个 Node 项目脚手架的开发工作,为了提高编码效率,导师提议写一个 VSCode 的插件,功能上大体有点像 snippets 代码段,但比 snippets 优秀的地方是,插件还能实现以下两大功能:

  1. 可遍历目前工程目录下所有的 @provide ,结合 VSCode API 可以实现快速添加 @inject
  2. 可识别相应文件代码段,灵活插入代码段

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 代码的特定位置中插入代码,我们就需要先解析目前的代码段。

// currentFileContent 为当前文件的字符串
require('@babel/parser').parse(currentFileContent, {

    sourceType: 'module',

    plugins: [

      'typescript',

      ['decorators', { decoratorsBeforeExport: true }],

      'classProperties',

     'classPrivateProperties'

    ]

  })

  通过这种方式,我们就可以把当前的代码转换为 AST 了

  感兴趣的同学可以到 AST Explorer 尝试一下,这个工具可以解析你提供的 JavaScript 代码,并且会以一种非常直观的图形化结构语法书呈现。

  我们可以尝试一下解析这三行简单的 JavaScript 代码,

let foo = 'Hello'
function PrintHello(){
  console.log(foo)
}

  在 AST Explorer 中,上面三行代码被解析为这样的结构。

AST

  在 AST Explorer 中,我们甚至查看生成的 JSON 格式的解析结果。

  在解析后得到 AST 后,下一步我们就需要开始分析它的结构了。

@babel/traverse

  在查看解析得到 AST 的 JSON 解析结果后,我们可以发现即便是几行简单的代码也会解析得到几百行的 JSON 结果,为了让我们可以快速得到想要的节点,我们可以使用 babel/traverse 这个工具,进行对 AST 快速的节点遍历与筛选。

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 中。

// 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 的用法非常简单:

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 工具库的大概用法,也只是简单描述了插件实现的大概思路。

  下一步计划是,等到把这个插件真正完善后,再详细写一篇关于该插件具体思路及改进的问题。

  初次尝试,如有错误内容,敬请原谅,烦请多多指教!

原创声明,本文系作者授权云+社区发表,未经许可,不得转载。

如有侵权,请联系 yunjia_community@tencent.com 删除。

发表于

我来说两句

0 条评论
登录 后参与评论

扫码关注云+社区

领取腾讯云代金券