在研发效能提升的大背景下,从优化团队研发整体流程、项目管理部署,或者细分到代码需求研发减少单需求键盘输入次数,都是很有意义的事情。那么从前端一线研发流程中,能够进行效能提升的一大方式是:代码生成。生成的逻辑大体可分为两种:一种纯输出,即代码片段生成,比如: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 对象是这样的:
到这里已经确定了输入和输出,那么至于中间的黑箱子就是我们代码生成器要做的事情。
二、架构设计
通过代码生成器整体的拆解,我们可以大体知道它需要如下能力:
http 接口请求能力
文件读写能力
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 种方式,一种是利用 vscode
的 workspace
工作区提供的监听 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,
})
}
复制代码
四、总结
到这里便大体过了整个代码生成器主体核心结构,关于更详细的内容建议动手实践下。
评论