使用 TypeScript 开发前端项目,完善的类型批注是非常提升开发效率的。然而,当遇到 Restful,似乎只能为 Restful 返回的 JSON 数据手动书写类型,随着接口越来越多,手写类型是繁琐且低效的。 有没有一种简单的方式,可以拿到返回数据的类型呢?

JSON 类型文件生成

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 === 'b') return 'Province'
   },
})

上面的代码,将会在 /User/xdoer/types.ts 文件中生成导出 interface 为 ChinaRegion 的类型文件,产生的中间 inteface 名称为 Province,中间产物默认的 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 数据类型生成的一些探索,如果您发现了文中的一些错误之处,或者有更简便的方式生成类型文件,欢迎在评论里提出来,大家一起探讨。