写点什么

效能研发:做一款 GraphQL 代码生成器

作者:梁龙先森
  • 2021 年 12 月 15 日
  • 本文字数:3767 字

    阅读完需:约 12 分钟

效能研发:做一款GraphQL代码生成器

在研发效能提升的大背景下,从优化团队研发整体流程、项目管理部署,或者细分到代码需求研发减少单需求键盘输入次数,都是很有意义的事情。那么从前端一线研发流程中,能够进行效能提升的一大方式是:代码生成。生成的逻辑大体可分为两种:一种纯输出,即代码片段生成,比如:VS Code 编辑器支持配置根据关键词联想代码片段。另一种是:根据输入和人工预设输出代码,比如:平常用的根据 Swagger API 进行 TypeScript 类型声明生成、api 接口请求生成等,以及本文要介绍的进行 GraphQL 代码生成。以上说明的生成器,我都设计研发并在团队内应用,取得不错反响。


开始正文前,如果你对 GraphQL 或者 VSCode 插件开发不甚了解,建议先阅读如下文章:

推开GraphQL大门

如何做一款属于自己的VS Code插件?

一、需求背景

在用 GraphQL 搭建服务,同样会为使用者生成对应 GraphQL 文档,方便他们查看和构造对应的 GraphQL 语句。生成的文档是这样子的,树的顶级是 Query 或 Mutation 方法,点击对应方法,则展开该方法所需要的入参,以及该方法返回的参数类型定义。若参数为引用类型,则点击会继续在下一层级树进行展示。


那么该文档是怎么进行生成的呢?实际上我们并不关心它的中间黑箱子是怎么处理的,我们只关心输入,至于黑箱子中我们可以自己实现。在打开文档的浏览器,我们打开调试面板查看网络面板,我们可以发现它实际是通过部署文档的应用接口 http://***/graphql 返回的整个协议数据。接口协议中的 types 字段包含了所有定义的 Query 和 Mutation 方法,以及对应的数据类型。


输入确定了,那么我们希望输出什么呢?GraphQL 语句和 DTO 对象。

GraphQL 语句是这样的:

DTO 对象是这样的:

到这里已经确定了输入和输出,那么至于中间的黑箱子就是我们代码生成器要做的事情。

二、架构设计

通过代码生成器整体的拆解,我们可以大体知道它需要如下能力:

  1. http 接口请求能力

  2. 文件读写能力

  3. UI 页面交互能力

那么前端能承接这些能力的产品有 CLI 脚手架、桌面应用、Node 服务+web,以及 VS Code 插件。通过对交互体验、以及团队当前开发成本两个维度考虑,我们最终选择采用 VS Code 插件来做为它的产品。


需求整体架构图如下,配置文件配置 GraphQL 接口所需参数,展示层用配置参数去获取 GraphQL 接口数据,并处理用 VS Code UI 组件进行展示。当触发选择方法,则进入 GraphQL 代码转换生成。代码生成后实际上是字符串,为了展示工整,经过加工层的 Prettier 对代码进行美化,然后输出。

三、需求实现

1. 配置监听器

配置监听器主要是为了监听 graphQL 相关接口配置,当监听到变化,能够去刷新数据。那么 graphQL 需要配置哪些数据呢?根据对 graphQL 文档接口的查看,可以定义出它需要下面三个主体的请求参数,当然项目有差异,可以自己实践。

{  url: "http:***/graphql",  headers: {    env: "dev",    Authorization:"",  },  params: {}}
复制代码

配置监听有 2 种方式,一种是利用 vscodeworkspace 工作区提供的监听 settings.json 配置文件的能力,代码如下:

import vscode from 'vscode'
vscode.workspace.onDidChangeConfiguration(() => { // 监听到配置文件变更,则执行该回调函数})
复制代码

另一种是借助文件系统监听能力。代码如下:

import chokidar from 'chokidar'
const configFilePath = '配置文件地址'
chokidar.watch(configFilePath, { ignoreInitial: true,}).on('all', function (event, path){ // 监听到配置文件变化})
复制代码

当你监听到配置文件变化,需要读取配置文件数据,并将配置数据缓存不销毁,以供后面使用。两种配置监听方案,考虑到项目中需要把 .vscode/settings.json 配置文件上传到 git 仓库,而这块 graphQL 配置是跟随不同项目配置,不上传 git 仓库,因此我们采用了自定义配置文件进行监听。当然实际在通过chokidar 监听配置到时候,需要考虑 cjs 模块的特殊性,require是存在缓存到,并且要递归监听配置文件到引用。

2. UI 展示层

数据请求这里采用的是 axios 这个库,因为它底层通过适配器实现了在浏览器和 node.js 环境的支持。它在 node 环境中实际上是通过 node-fetch 去请求数据。获取完数据我们希望它如下展示,支持多选,支持模糊搜索方法。

vscode 插件也提供了对应的能力:

import vscode from 'vscode'
const commands = { graphQL: async ()=> { const methods = [] // 等待用户选中数据后返回selectMethod,若没选择返回undefined const selectMethod = await vscode.window.showQuickPick(methods || [], { title: '请选择方法', canPickMany: true }) }}
for (const command in commands) { vscode.commands.registerCommand(`pp.cmd.graphQl.${command}`, commands[command])}
复制代码

这里注意的是弹窗事件的触发我们绑定到了命令上,并且完成了注册。我们希望新增右键菜单供选择去触发弹出事件,因此需要配置右键菜单项:

{  "contributes":{    // 注册命令    "commands": [    	{        "command": "pp.cmd.graphQl.dialog",        "title": "GraphQL代码生成器"      },    ]    "menus": {      // 配置又键菜单      "editor/context": [{         "when": "editorFocus",         "command": "pp.cmd.graphQl.dialog",         "group": "navigation"      }]    }  }}
复制代码

3. 代码生成器

代码生成器我们要把什么生成什么呢?查看 /graphql 接口协议,我们可以确定方法的结构,下面以getProjectMethod 这个方法为例子,它的结构如下:

args:是方法的入参,它是一个对象数组,每个对象保护了属性的名字和类型。

type:是方法响应参数类型,图中的实际表示为 [ProjectMethod!]


那么单方法的输入确定了,那么中间转换就不难了。这里希望手动实现,都是算法实现,没什么好描述,但要注意:对于循环引用问题,要设置最大循环次数,然后断开链接。


下面介绍下两个计算过程中基本会使用到的方法:

/** * 生成单个参数类型 id:String! * @param name 初始命名 * @param linkType type引用链 * @returns */export const generateTypeStr = (name: string, linkType: GraphQlMethodType[]) => {  let argType = name  const revertType = linkType?.reverse()  for (let i = 0; i < revertType?.length; i++) {    const { kind, name } = revertType[i]    // 非空:尾部拼接“!”    if (kind === 'NON_NULL') argType += '!'    // 列表:包一层中括号    if (kind === 'LIST') argType = `[${argType}]`    if (name) argType += name  }  return argType}
/** * 递推获取类型链-ofType,数结构拍平成数组[{kind,name}] * @returns {linkType: type类型数组,linkRefType: type存在的引用值} */export const getGraphQlTypeLink = () => { const linkType: GraphQlMethodType[] = [] return function loop(type: GraphQlMethodType) { const { ofType, ...otherProps } = type linkType.push(!ofType ? type : otherProps) if (ofType) { loop(ofType) } return linkType }}
复制代码

4. 加工层

代码生成器输出到代码并不是非常美观,当然手工美化也需要时间,因此引入了 Prettier 进行处理。

const prettier = require('prettier')/** * 美化字符串 * @param str * @returns */export const prettierDoc = (str: string) => {  return prettier.format(str, { semi: false, parser: 'babel' })}
复制代码

5. 代码输出

代码输出存在 3 种方式,一种是直接利用文件读写能力,往项目写入文件,但这种情况不友好,一方面是如法写入实际它需要放置到目录,另一方面查看内容都需要打开文件费劲。另一种是,用虚拟文档创建一个文档,然后设置它可编辑内容,这种方式是每次关闭文档会弹出确认是否要保存。最后一种是创建一个不可编辑到虚拟文档,然后放置内容。那么此时,你查看完生成代码便可以直接复制到项目,关闭它也不会提示。

相关代码如下:

  async preSaveDocument(docStr: string, filePath: string) {    // 自定义协议    const scheme = 'pupu'    // 实现供应器函数:并不生成uri协议    const myProvider = new (class implements vscode.TextDocumentContentProvider {      onDidChangeEmitter = new vscode.EventEmitter<vscode.Uri>()      onDidChange = this.onDidChangeEmitter.event      // 跟据uri,返回对应内容      provideTextDocumentContent(uri: vscode.Uri): string {        // 这里也可以根据uri解析参数做对应内容处理,但这里不需要        return docStr      }    })()    // 注册供应器    global.ctx.subscriptions.push(vscode.workspace.registerTextDocumentContentProvider(scheme, myProvider))    // 生成uri协议    const uri = vscode.Uri.parse('pupu' + ':' + filePath)    // 根据uir协议,打开内容,内容由注册的供应器提供    const doc = await vscode.workspace.openTextDocument(uri)    vscode.window.showTextDocument(doc, {      // 用于控制编辑器选项卡是否将替换为下一个编辑器或是否保留      preview: false,      // 显示编辑器的可选视图列      viewColumn: vscode.ViewColumn.Active,    })  }
复制代码

四、总结

到这里便大体过了整个代码生成器主体核心结构,关于更详细的内容建议动手实践下。

用户头像

梁龙先森

关注

无情的写作机器 2018.03.17 加入

vite原理/微前端/性能监控方案...,正在来的路上...

评论

发布
暂无评论
效能研发:做一款GraphQL代码生成器