最近写了个小玩意,可以根据 HTTP 响应的 JSON 数据,自动生成 TS 类型文件。。 做之前也没调研清楚,花了些时间搞完了之后,发现现在大家似乎都是用 swagger 的一些工具,来自动生成 TS 类型文件。。。欲哭无泪,想着不能白搞,如果有些内容对大家有所启发也是极好的,遂还是写篇文章,讲讲过程。
前言
使用 TypeScript 开发前端项目,完善的类型批注是非常提升开发效率的。然而,当遇到 Restful,似乎只能为 Restful 返回的 JSON 数据手动书写类型,随着接口越来越多,手写类型是繁琐且低效的。 有没有一种简单的方式,可以拿到返回数据的类型呢?
JSON 类型文件生成
首先,我们目前前后端分离的开发模式中,接口响应数据一般都是 JSON 格式的,那怎么为 JSON 数据生成 TS 类型?
JSON 类型
Json 中数据类型有 6 种: string 、number、boolean、array、object、null
其中 string、number、boolean 的类型可以直接使用 typeof
判别类型。
null 有些复杂,它可能是其他 5 中类型中的一种,无法判断具体是什么类型,因而只能填充 any
对于 object,它可能由 Json 的 6 种数据结构组成,可以使用递归遍历的方式,来判断 value 的类型
而对于 array ,array 中的每一项数据结构应当都是相同的,因而只需要取出第一项进行处理,处理逻辑与上述几种类型相同。
文件生成
可以使用 node fs api,利用拼接字符串的形式,将 JSON 类型处理后,输出到类型文件中。这样简单且有效,但不那么优雅,且易出错。
可以借助 ts-morph 这个库,来完成类型的生成和导出。
ts-morph 使用伪代码如下:
const project = createProject()
project.addInterface({ name, value }).setIsExport(true)
saveProject(project)
相比 fs API,ts-morph 使用更简单
Restful 整合
可以根据 JSON 数据生成类型文件后,很容易想到,在请求库的拦截器中,拦截响应,执行 JSON 类型文件生成。但值得注意的是,前端项目中,Node API 不能使用,因为你的代码是运行在浏览器的。怎么解决这个问题?
类型生成器脚本
既然前端项目中不能集成JSON类型文件生成工具,那么可以编写 Node 脚本来解决问题。后端提供一个接口后,前端新增一个接口,脚本配置文件也要注册一个接口,最后运行一下脚本即可。
那么看看脚本需要完成哪些功能。
首先脚本需要集成一个请求库,用以发起请求,接收服务端的 JSON 数据。
然后还要集成上面的 JSON类型文件生成脚本。
此外,还需要维护一份配置文件,文件中要有请求参数列表,用以动态生成类型文件。为了避免同时发起的请求数量太多,导致电脑死机,或者服务端宕机,还要对请求进行并发控制。
每次执行脚本,所有请求都会再发送一遍,所以还要考虑检测文件是否生成,再去请求。
考虑到可维护性,建议单独维护一个 URL 的映射文件,在Node脚本和前端项目,引用 URL 文件的URL 地址。
有了这样一个脚本,每次新增一个接口时,需要在配置文件中配一下接口和请求参数,然后手动执行一下脚本。这样也不太方便,可以使用 chokidar 监听文件变更,使用 shelljs 来执行脚本。
可以看到,上面的步骤繁琐且复杂,维护这样一个复杂配置文件,会让人望而却步。并且这样的配置文件对于一些复杂的请求,涉及到的 Token 校验, Post 的 Body 处理,响应的 Data 的处理等等都要区别与前端项目,再单独处理一遍。
有没有更好的办法,来完成类型生成的目的?
Server-Clinet 类型生成器
写这样一个脚本,主要的难点在于Node脚本怎么便捷的拿到前端项目的响应数据,也就是前端拿到数据后怎么通知到脚本?
这么一想,事情就简单了,如果 Node 脚本中开启一个 HTTP Server,前端拿到数据后,再向 HTTP Server 发起一个 POST 请求,将一些参数携带过去,指挥 HTTP Server 向目标目录生成类型文件即可。
但这一套流程还有个缺点,类型文件是“运行时”生成的,生成类型文件前,需要前端项目先调用一次请求。但是,这一点缺点无伤大雅,开发代码时,肯定需要先测试接口能不能通什么的。
工具链
基于几天的尝试,我开发了几个库,完成了这样一件事情,最后看 demo 的效果,还不错。
Demo 项目
我基于 Vite React TypeScript 写了一个 demo 项目:restful-types-generate-example。
clone 项目后,运行 yarn 安装, yarn dev 启动项目,点击页面按钮,发起请求后即可看到效果。
JsonTypesGenerator
json-types-generator 是根据第一小节中介绍的原理完成的
使用方式如下:
import jsonTypesGenerator from 'json-types-generator'
const json = { a: { b: 1, c: { d: true } } }
jsonTypesGenerator({
data: json,
outPutPath: '/User/xdoer/types.ts',
rootInterfaceName: 'ChinaRegion',
customInterfaceName(key, value, data) {
if (key === 'a') return 'Province'
return key
},
})
上面的代码,将会在 /User/xdoer/types.ts
文件中生成导出 interface 为 ChinaRegion
的类型文件,产生的中间 inteface 名称为 Province
。不传入 customInterfaceName的情况下,中间产物默认的 interface 名称为 key 的大写
<!----/User/xdoer/types.ts---->
export interface ChinaRegion {
a: Province
}
export interface Province {
b: number
c: c
}
export interface c {
d: boolean
}
ResponseTypesServer
response-types-server 是上文提到的 Server-Clinet 类型生成器 中的 Server 部分。只需要向这个Server 发送 POST 请求,即可生成类型。
使用方式如下:
import server from '@prequest/response-types-server'
// 默认开启的端口为 10086
server()
// 你可以通过传参指定端口
server({ port: 10010 })
发送的请求,路径任意, POST 请求参数为:
参数 | 类型 | 含义 |
---|---|---|
outPutDir | string | 类型文件输出目录 |
outPutName | string | 文件名称 |
overwrite | boolean | 文件可复写 |
data | Json | 要解析的 Json 数据 |
interfaceName | string | 导出的接口名称 |
ResponseTypesClient
response-types-client 是上文提到的 Server-Clinet 类型生成器 中的 Client 部分。它是一个中间件 Wrapper,只要将其注册到请求库中间件中,即可发起请求。
下面的 demo 使用了我自己封装的请求库 PreQuest,基于 Koa 中间件模型的请求库应该都可以使用,比如说 Umi-Request。对于 Axios,需要自己在拦截器中实现,也非常容易。
使用方式如下:
import { create, Request, Response } from '@prequest/xhr'
import generateMiddleware, { TypesGeneratorInject } from '@prequest/response-types-client'
// 生成中间件
const middleware = generateMiddleware<Request, Response>({
enable: process.env.NODE_ENV === 'development',
httpAgent: create({ path: 'http://localhost:10010/' }),
outPutDir: 'src/api-types'
parseResponse(res) {
// res 应当返回接口 data 数据
return res as any
},
typesGeneratorConfig(req, res) {
const { path } = req
const { data } = res
if (!path) throw new Error('path not found')
// 根据请求路径生成文件名和类型导出名称
const outPutName = path.replace(/.*\/(\w+)/, (_, __) => __)
const interfaceName = outPutName.replace(/^[a-z]/, g => g.toUpperCase())
return {
data,
outPutName,
interfaceName,
overwrite: true
}
}})
// 注入 TypesGeneratorInject, 可在请求时,根据 rewriteType 参数强制重新生成类型文件
export const prequest = create<TypesGeneratorInject, {}>({ baseURL: 'http://localhost:3000' })
// 注册中间件
prequest.use(middleware)
ResponseTypesGenerator
此外,还有基于上文 “类型生成器脚本” 一节中的原理,进行了一个失败的尝试:response-types-generator,也一并放到这里,感兴趣的可以看看
结语
以上基于我浅薄的学识进行的一些对 Restful 响应的 JSON 数据类型生成的一些探索,如果您发现了文中的一些错误之处,或者有更简便的方式生成类型文件,欢迎在评论里提出来,大家一起探讨。