瞎扯淡 大家觉得在 Rails 端生成带类型的前端 API 调用代码有搞头吗?

mizuhashi · 2022年09月09日 · 最后由 mizuhashi 回复于 2022年10月03日 · 1038 次阅读

这样可以确保后端返回的 json 和前端 ts 里的 json 是相同类型的,前端在调用 api 的时候也可以确保提供了后端要求的 params。

例如这样的 ruby 代码:

endpoint :recipe_data do
  params do
    number :id
  end

  response do
    recipe :recipe
  end
end

会生成前端的 ts 代码:

interface recipe {id: number, title: string, subtitle: string}
function api_recipe_data(params: {id: number}) : Promise<{recipe: recipe}> {
  return useApi('api/recipe_data', params)
}

export default { api_recipe_data}

其中recipe是一个自定义的类型,在另一个地方定义了它应该有的 interface。这个目前的实现并不复杂,但是如果要做成通用的 rails 插件估计很麻烦,不知道值不值得

graphql?

hooopo 回复

我不太熟悉 graphql 的生态,可能我这其他人 adopt 它很难(可能也包括我自己),不过思路可能有接近的地方。这个应该更接近 rpc,如果前端后端都是 ts,那https://trpc.io/ 这个会非常爽,但估计对于 rails 能生成一套前端函数已经不错了

“确保后端返回的 json 和前端 ts 里的 json 是相同类型的”不是太理解。在我看来 API 是个契约,它一变动,前后端都得跟着作调整,你的意思是把这个前后端开发者协调的过程给省掉吗?不太明白是打算解决什么问题。

qiumaoyuan 回复

对,这个过程可以省掉,甚至连协调 url 的过程都可以省掉,但更重要的是要在前端用 ts,不生成的话需要自己用 ts 写一套类型,然后假定拿到的 json 一定是符合前端类型的,数据的字段啊类型都可能出错,降低效率

mizuhashi 回复

我觉得你可以找个 graphql 的 demo 玩一下,看你的描述效果是一样的

hooopo 回复

看到了那个 apollo codegen,确实可以实现生成 ts,这个应该更适合普遍使用,我的就在自己项目里瞎玩就好了😂

我之前拿 JSON:API (有点类似 GraphQL) 对比了一下传统 API. 我不知道自己有没有理解错,跟大家交流一下。我感觉这种方式几乎是直接把后端的模型暴露给了前端,前端在知道后端的模型之后,可以直接根据规范针对已知的模型进行种种操作,包括但不限于:按属性排序、按属性过滤、列表、分页、查看详情、删除、更新等。如果再定义了模型间的关系(比如 has many comments),还可以通过 side loading 和 side posting 的方式对模型及其相关联的模型同时进行查询和修改,可以自由控制数据的粒度,似乎也省掉了很多前后端沟通。这样基本上弱化了后端的功能,后端直接退化成了一个对象数据库。

跟传统 API 的区别在于,传统 API 返回的某些字段可能是后端计算之后返回的,JSON 结构也可能跟后端模型不完全一致。如果直接返回原始模型给前端,这样的计算工作会被放到前端,一些业务逻辑也都往前端移了,最终的结果是前端计算量和代码工程量都变大。而一个项目中,前端可能有很多种实现,比如 HTML, Android, iOS 等,这些前端的工作量都跟着变大的话,感觉不是很划算。我没有在项目中使用 JSON:API 和 GraphQL 的实战经验,这只是我在粗略了解 JSON:API 之后的一些推测,不知道实际情况是不是这样。

qiumaoyuan 回复

我感觉这种方式几乎是直接把后端的模型暴露给了前端

它实际是多抽象了一层(GraphQL 里叫 type,JSONAPI 里应该叫 resource)。由于可以使用框架命令根据 DB schema 来生成这一层的内容,所以看起来像是暴露了模型。然而实际在项目里必然会更改这些生成的代码,删除大量的 meta data 字段,重新定义字段

跟传统 API 的区别在于,传统 API 返回的某些字段可能是后端计算之后返回的,也就是说返回的 JSON 结构跟后端模型结构并不是完全一致的

拿 GraphQL 举例,type 中的字段逻辑是可以自定义的,当前端想要 full_name, 而数据库只有 first_name 和 last_name,完全可以在对应的 type 中定义一个 full_name 字段。前端想要日期格式的 created_at, 而 DB 只有 time 格式的 created_at, 也可以在对应的 type 中转换。甚至你也可以完全定义一个不对应任何模型的 type 出来。所以只要后端肯干,完全可以不把业务逻辑交给前端。

spike76 回复

原来如此

一个人全干没问题 多人协作有种你教我做事的感觉

qiumaoyuan 回复

我觉得传统 restful 和 graphql 的区别也没有那么大,因为标准的 restful 任何东西都要抽象成资源,就算是订单取消都要"POST /orders/:id/cancellations",这里面的cancellation不一定就是真的对应模型,可能只是order的一个字段,后端暴露的资源完全可以是虚构的

zouyu 回复

这就是团队内斗的一部分,急需高限制性的手段来规范他人的写法,所以我引入了 typescript vue3 还有一系列规范,防止他们继续用 vue2 堆屎

16 楼 已删除
17 楼 已删除

虽然没有实践过,但我觉得在一定情况下还是有搞头的,其最大的作用是减少前后端之间的沟通成本,并在前端处将调用 API 的方式标准化。不过你说的这个语法:

endpoint :recipe_data do
  params do
    number :id
  end

  response do
    recipe :recipe
  end
end

在 Rails 下不支持啊。我自己写了一个框架,参见 https://github.com/yetrun/web-frame,它里面定义了类似于你的语法。不过我在里面解决的问题跟你提到的不一样,我要生成的是标准化的文档,即在一定程度上保证实现和文档是一致的。不过,你参考 Dain::SwaggerDocUtil#generate 方法,将其改成生成 TS 代码,应该也是能够做到的。

yetrun 回复

因为这个 dsl 是我自己写的,ts 生成也有了,目前用起来还是很爽的,后端加上检查之后返回类型不吻合的数据会抛错,这样前端可以保证不会遇到类型错误了😃

mizuhashi 回复

能实现成这样就不错了,只不过好奇这个怎么做成 Rails 插件。

yetrun 回复

我的做法是新开了个类似 routes 的文件夹,然后里面定义 endpoints 的 dsl,然后在 routes.rb 里面读取和引入这些 endpoints 定义成实际的 routes。然后这些 routes 都是按约定对应到 controllers 上的。

然后再用 rails 的 FileUpdateWatcher 监视这个目录的变化,然后当有变化的时候触发 routes reload,就可以实时更新,别的类型定义等都存成类文件,要用的时候按名字 constantize,这样可以利用 rails 的 autoload。

这样的话实际上对 rails 的侵入部分就只是 routes,做成插件应该不难,难的是确定各种约定,以满足不同用户的需求

mizuhashi Camille:让前端和 Rails 进行类型安全的通信 提及了此话题。 03月20日 08:47
需要 登录 后方可回复, 如果你还没有账号请 注册新账号