写了个没用的自动根据 HTTP 响应数据生成 TS 类型文件的生成器
发布于 18 天前 作者 LuckyHH 738 次浏览 来自 分享

最近写了个小玩意,可以根据 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 数据类型生成的一些探索,如果您发现了文中的一些错误之处,或者有更简便的方式生成类型文件,欢迎在评论里提出来,大家一起探讨。

回到顶部