为 HTTP 请求的 JSON 响应自动生成 TS 类型 - V2EX
V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
ThomasTrainset
V2EX    前端开发

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

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

    使用 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     Solana     2268 人在线   最高记录 6679       Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 21ms UTC 15:48 PVG 23:48 LAX 07:48 JFK 10:48
    Do have faith in what you're doing.
    ubao msn snddm index pchome yahoo rakuten mypaper meadowduck bidyahoo youbao zxmzxm asda bnvcg cvbfg dfscv mmhjk xxddc yybgb zznbn ccubao uaitu acv GXCV ET GDG YH FG BCVB FJFH CBRE CBC GDG ET54 WRWR RWER WREW WRWER RWER SDG EW SF DSFSF fbbs ubao fhd dfg ewr dg df ewwr ewwr et ruyut utut dfg fgd gdfgt etg dfgt dfgd ert4 gd fgg wr 235 wer3 we vsdf sdf gdf ert xcv sdf rwer hfd dfg cvb rwf afb dfh jgh bmn lgh rty gfds cxv xcv xcs vdas fdf fgd cv sdf tert sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf shasha9178 shasha9178 shasha9178 shasha9178 shasha9178 liflif2 liflif2 liflif2 liflif2 liflif2 liblib3 liblib3 liblib3 liblib3 liblib3 zhazha444 zhazha444 zhazha444 zhazha444 zhazha444 dende5 dende denden denden2 denden21 fenfen9 fenf619 fen619 fenfe9 fe619 sdf sdf sdf sdf sdf zhazh90 zhazh0 zhaa50 zha90 zh590 zho zhoz zhozh zhozho zhozho2 lislis lls95 lili95 lils5 liss9 sdf0ty987 sdft876 sdft9876 sdf09876 sd0t9876 sdf0ty98 sdf0976 sdf0ty986 sdf0ty96 sdf0t76 sdf0876 df0ty98 sf0t876 sd0ty76 sdy76 sdf76 sdf0t76 sdf0ty9 sdf0ty98 sdf0ty987 sdf0ty98 sdf6676 sdf876 sd876 sd876 sdf6 sdf6 sdf9876 sdf0t sdf06 sdf0ty9776 sdf0ty9776 sdf0ty76 sdf8876 sdf0t sd6 sdf06 s688876 sd688 sdf86