前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >AST in TypeScript 实践

AST in TypeScript 实践

原创
作者头像
JoviCheng
发布2019-06-30 17:19:18
5.4K0
发布2019-06-30 17:19:18
举报
文章被收录于专栏:进击的全栈进击的全栈

实践来源

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

代码语言: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
AST

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

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

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

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

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

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

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

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 实践来源
  • TypeScript
  • AST with Babel
  • @babel/parser
  • @babel/traverse
  • @babel/types
  • @babel/generator
  • 总结
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档