V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
ThomasTrainset
V2EX  ›  前端开发

为 HTTP 请求的 JSON 响应自动生成 TS 类型

  •  
  •   ThomasTrainset · 2021-08-31 19:45:40 +08:00 · 1222 次点击
    这是一个创建于 1171 天前的主题,其中的信息可能已经有所发展或是发生改变。

    使用 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 启动项目,点击页面按钮,发起请求后即可看到效果。

    Aug-31-2021 18-53-50.gif

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

    原文首发于我的个人博客:Restful-API 的一种动态生成数据类型的方法 | 文享日志 (aiyou.life)

    目前尚无回复
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   1009 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 23ms · UTC 21:33 · PVG 05:33 · LAX 13:33 · JFK 16:33
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.