热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

前端调用后端接口xhr只看到一次实际调用两次_来自前端同学对后端同学的吐槽...

作者:李熠链接:https:juejin.impost5cfbe8c7e51d4556da53d07f前言去年的某个时候就想写一篇关于接口的吐槽,当时后端提出了接口方
f18fc61991589e1f81430f2310a03072.png
作者 : 李熠
链接 : https://juejin.im/post/5cfbe8c7e51d4556da53d07f

前言

去年的某个时候就想写一篇关于接口的吐槽,当时后端提出了接口方案对于我来说调用起来非常难受,但又说不上为什么,没有论点论据所以也就作罢。最近因为写全栈的缘故,团队内部也遇到了一些关于接口设计的问题,于是开始思考实现接口的最佳实践是什么。在参考了许多资料之后,逐渐对这个问题有了自己的理解。同时回想起过去的经验,终于恍然大悟自己当时的痛点在哪里。

既然是吐槽,那么请原谅我接下来态度的不友善。本文中列举的所有例子都是我个人的亲身经历。

谁应该主导接口的设计

或者更直白一些,应该是接口的消费方还是提供方来决定接口的设计?

当然是接口的消费方

「接口」最吊诡的地方在于提供方大费周章把它实现了,但它自己却(几乎)重来都不使用。于是这极易陷入一种自嗨的境地,因为他更本不知道接口的好坏。就好比一个从来不尝自己做的菜的厨子,你指望他的菜能好到哪里去,他的厨艺能好到哪里去。上面隐含的前提是(我认为)接口是有绝对好坏之分的,坏的接口消费者调用难受,提供者维护难受,还导致产品行为别扭体验变差。

然而接口的好坏与谁来主导设计有什么关系?因为坏接口产生的原因之一是提供方只站在开发者的角度解决问题:

例子一 (Chatty API)

某次需要实现允许用户创建仪表盘页面的功能(如果你对仪表盘页面感到陌生的话,可以想象它是一张集中了不同图表的页面,比如柱状图、折线图、饼图等等。用户可以添加自己想要的图表到页面中,并且手动调整它们的尺寸和位置。仪表盘通常用于总览某个产品或者服务的运行状态)。后端同学的接口初步设计是,当用户填写完基本信息、添加完图表、点击创建按钮之后,我需要连续调用两次接口才能完成一次仪表盘的创建:

1.利用用户填写的基本信息以及图表的尺寸和位置创建一个空的仪表盘

2.再向仪表盘中填充图表的具体信息,比如图表类型,使用的维度和指标等

很明显看出他完全是按照自己后端的存储结构在设计接口,不仅是存储结构,甚至存储过程都一览无余。想象一种极端的情况,那不只提供一些更新数据库表的接口得了,前端自己把通过接口把数据插入库中

面对这类底层性质的接口,消费者在集成时需要考虑接口的调用步骤以及理解背后的原理。如果后端的底层结构一旦发生更改,接口很有可能也需要发生更改,前端的调用代码也需要随之更改。

后端研发可能会辩解说:后端用了微服务啊,不同类型的数据存储在不同的服务上,所以你需要和不同的服务通信才能实现完整的存储。他们始终没有明白的事情是,后端的实现导致了接口的碎片化,那是你的问题,而不应该把这部分负担转移到前端的开发者上,其实也是间接转移到了用户身上。不要欺负我不懂后端,至少我了解加一层类似于 BFF 的 Orchestration Layer 就能解决这个问题

Netflix 的工程师 Daniel Jacobson 在他的文章 The future of API design: The orchestration layer 中指出, API 无非是要面对两类受众:

1.LSUD: Large set of unknown developers

2.SSKD: Small set of known developers

随着产品服务化的趋势,很有可能需要像 AWS 或者 Github 那样对公共开发者即 LSUD 暴露接口。且不说上面例子中的接口方案会不会被唾沫星子淹死,如此明显的暴露内部服务的细节是非常危险的事情。

所以在设计接口时,应该让消费者来主导。如果消费者没能给出很好的建议,那么至少提供者在设计时也应该站在消费者的立场上来思考问题。又或者,至少想一想如果你自己会乐意使用用你自己设计出来的接口吗?

使用后端思维设计接口不仅体现在 URI 的设计上,还有可能体现在请求参数和返回体结构上

例子二

假设现在需要一个请求批量文章的接口,接口同时返回多篇文章的内容,包括这些文章的内容,作者信息,评论信息等等。

理想情况下,我们期望返回的数据是以文章为单位的,比如

articles: [{id: ,author: {},comments: []},{id:author: {},comments: []}
]

However, 后端的返回结果可能是以实体为单位:

{articles: [],authors: [],comments: []
}

comments 里包含不同文章的 comment,我必须通过类似于 articleId 的字段对它们执行 group by 操作才能分离出属于不同文章的评论。对其他实体做同样的操作,最终手动的拼接成前端代码需要的 articles 数据结构

很明显这又是按照后端库表关系返回的结果,严格来说这并不算是 anti-pattern,在 redux 中也鼓励将数据 normalize。但如果前端用不到原始数据,请不要返回原始数据。例如我需要在页面上展示一个百分比格式的数据,除非用户有动态调整数据格式的需求,例如千分位、小数或者是切换精度等等,否则就直接返回给我百分比的字符串就好了,不要返回给我原始的浮点数据。前端对数据的二次加工还会给问题排查带来干扰,如果任何数据都需要前端进行二次加工,那么所以问题的排查都必须从前端发起,前端确认无误后再进入后端排查流程,这始终会占用两个端的人力,并且 delay 了排查的进度

关于 meta 信息

例子三:

后端接口在返回时通常会带上 meta 信息,meta 信息包含接口的状态以及如果失败时的失败原因,便于调试使用。后端提供的接口的 meta 信息的数据结构如下:

{meta: {code: 0,error: null,host: "127.0.0.1"},result: []
}

在我看来,以上数据结构有两个问题

meta 信息包含独立的状态信息

在包含状态码的 meta 信息接口设计中,一条默认的隐藏逻辑是:接口返回的 HTTP status code 一定是 200,数据是否真的获取成功需要通过 meta 里的自定状态码 code 进行判断(换句话说,上面你看到的接口实际上是 “接口的接口”)。最终在前端的代码中也不需要通过 HTTP code 判断返回是否正常,只需要判断接口里返回的meta.code即可

** 但是谁给你们的自信保证后端接口一定是不会挂的?!** 无论后端如何保证接口的坚固,前端仍然需要首先判断 HTTP code 是否为 200,再判断meta.code是否与预期的符合一致。这和信任无关,和我程序的健壮有关。

既然无论如何都要对接口判断两次,那为什么不将meta.code与 HTTP code 合二为一?更何况我还需要再本地维护一份自定义 code 的枚举值,还需要和后端保证同步。这就涉及到下一个问题了:

meta 信息的存放位置

我们需要 meta 信息没有错,但是我们没有那么需要 meta 信息。这体现在几点:

  1. 我们真的需要一个平行于返回结果的字段展示 meta 信息吗?
  2. 每一次请求我们都需要 meta 信息吗?
  3. meta 信息一定要在 meta 字段里吗?

以请求失败的错误信息为例,错误信息只会出现在接口非正常返回的情况下,但我们应该始终在返回体中用一个字段为它预留位置吗?

在关于 meta 信息存在位置的这个问题上,我倾向于将它们整合进入 HTTP Header 中。例如meta.code完全可以使用 HTTP code 代替,我看不出始终要保证 200 返回以及自定义 code 的意义在哪里

而至于其它的 meta 信息,可以通过以X-开头的自定义 HTTP Header 进行传递。例如

Github API 中关于使用频率限制的信息就放在 HTTP Header 中:

Status: 200 OK
X-RateLimit-Limit: 5000
X-RateLimit-Remaining: 4999
X-RateLimit-Reset: 1372700873

Design for today

例子四

我们需要为某个指标的折线图设计查询接口,查询以天为单位,也就是说该接口只会根据查询的日期返回指定日期的查询结果,后端提供的返回数据结构如下:

{data: [{date: "2019-06-08",result: []}]
}

虽然需求很明确的指示只会返回某天的查询结果,但是后端还是决定给我返回一个数组。他这么设计的理由是为了防止日后需求发生改变需要返回多日的查询结果。

这看上去是很聪明决策:“看,我预见性的 cover 了一个未来的需求!”,但实际上愚蠢至极:你的确 cover 了一个需求,不过是一个当前并不存在,未来也不见得会发生的需求;而且如果你真的想写 future-proof 的代码,那么还有未来千千万万的需求等待着你实现。

问题在于没有人知道将来是否真的会允许同时查询多日数据,即使某天需要支持同时查询多日数据了,数据结构也不一定非要如此。在数据分析领域我们面临的查询需求并不是线性从单个到多个,在其他业务领域也是这样。

这样导致的后果是你花费多余的时间实现了不需要的代码,并且前端也需要配合这样的数据结构进行实现。并且在将来的维护中,每个看到返回体是数组的人都会纳闷为什么返回的结果明明只有一条,还需要用数组封装,是不是我遗漏了什么?于是不得不投入精力来验证是否真的有可能返回更多的数据。API 和代码应该是精准的,准确表达你想实现的一切而不存在有歧义

有人可能会说不就是多了一层封装吗?实现上也花不了多少的功夫何至于大惊小怪。抱歉我不是针对这一个 case,而是在强调任何场景下无论实现的难易都不应该添加无意义的代码,“勿以恶小而为之” 就是这个道理

“关注当下” 还有另一个维度含义:

例子五

目前我们已经有创建单个文章的接口,现在需要支持批量创建文章。后端给出的建议是:不如调单个接口多次?

例子六

目前已经有一个接口能够取得文章相关数据,比如内容、评论、作者等等。现在我们需要增加一个新的页面用于展示用户信息。后端给出的建议是:不如使用文章数据接口,里面已经包含了作者信息,这样就不用开发新的接口了

以上的例子看似都是想实现对接口的复用,但实际上起到的是事倍功半的效果

在例五中,虽然语义上 “创建五篇文章” 和“连续五次创建一篇文章”是等效的,但是在实现和操作层面并不是如此。且不说调用五次和调用一次的性能大不相同,批量创建的五篇文章可能存在顺序关系,可能需要事务操作。

在例六中虽然能够达到我们实现的效果,但这不能算是接口的复用,只能算是接口的 hack(hack 和复用的区别在于是否用物品的初衷功能做事情)。并且 hack 接口是有风险的,对于接口的提供者而言他们更关心接口服务 “正统” 的消费者,在这个 case 中接口的存在是为了展示完整的文章信息,如果有一天 “文章信息” 这个需求发生了变化很有可能会导致作者信息同时发生变化,缩减字段甚至取消字段。那么它们没有义务这些 hack 用户负责。一个接口本应该就专注一件事情

所以最理想的事情是,为当前专注的业务开发独立的接口。在例六的例子中,可能我们在开发一个独立请求作者的信息的接口时实现代码完全复制自另一个接口的实现,但是接口的隔离在长远看来能给功能的维护带来更大的便利

不仅限于 REST API

“接口” 是一个概念。在概念之下如何实现它我们拥有很多种选择。目前看来绝大部分的方式是通过 REST API 来达成的,也并没有什么事情是 REST API 无法做到的,但事实上这几年技术的进步给了我们更多的选择,如果选择更有针对性的实现方案,效果会更好

例如在实时数据的场景下,理论上是由后端(有数据更新时)驱动前端视图的更新,这理应是 push 操作。但是在传统实现中,我们不得不仍然通过被动的等待和轮询实现功能。

对于事件驱动类型的需求使用 WebSocket 或者是 Streaming 似乎是更好的选择。如果是后端之间的交互还可以利用 WebHook。我通常对新技术持保留态度,但是不得不承认 GraphQL 在处理某些需求上也能够比 REST API 做的更好。并且大部分厂商对于 GraphQL 接口的支持表明它是可行的。

我了解实现 API 来只是后端实现功能的一个很小的环节,在接口背后是更多业务逻辑的修改和库表结构的更迭。甚至说接口部分有一半都是交给框架来实现的。但是,哪怕只有很小的机会,也应该把这个环节做到尽善尽美。

结束语

对于糟糕的接口设计我还能继续没完没了的抱怨下去,但突然然觉得洋洋洒洒的继续写下去似乎没有太大意义。讲真我不是来真的大吐苦水的,只是想表达接口设计也至关重要。在工作中痛心的看到很多问题明明用一些很基础的技巧就能够解决,而大家却对它熟视无睹以造成两败俱伤的境地。以上就是我认为的在接口设计中需要遵循的一些原则和考虑要素,相信能够解决大多数的痛点和避免部分的问题

后端同学们,如果你们有心让接口变得更好,多听听 “消费者” 的反馈。如果你们尝试使用过第三方接口开发过应用的话,例如 Slack、Github,你会发现它们的接口是在不断迭代的。不断有旧的接口被淘汰,新的接口投入使用。这种迭代背后不是闲着没事干,而是出于实际的用户的声音和需求

最后推荐我最近阅读的关于 API 设计的图书,收益匪浅:

  • Web API 的设计与开发
  • Designing Web APIs
  • APIs A Strategy Guide



推荐阅读
  • Go Cobra命令行工具入门教程
    本文介绍了Go语言实现的命令行工具Cobra的基本概念、安装方法和入门实践。Cobra被广泛应用于各种项目中,如Kubernetes、Hugo和Github CLI等。通过使用Cobra,我们可以快速创建命令行工具,适用于写测试脚本和各种服务的Admin CLI。文章还通过一个简单的demo演示了Cobra的使用方法。 ... [详细]
  • WebSocket与Socket.io的理解
    WebSocketprotocol是HTML5一种新的协议。它的最大特点就是,服务器可以主动向客户端推送信息,客户端也可以主动向服务器发送信息,是真正的双向平等对话,属于服务器推送 ... [详细]
  • STL迭代器的种类及其功能介绍
    本文介绍了标准模板库(STL)定义的五种迭代器的种类和功能。通过图表展示了这几种迭代器之间的关系,并详细描述了各个迭代器的功能和使用方法。其中,输入迭代器用于从容器中读取元素,输出迭代器用于向容器中写入元素,正向迭代器是输入迭代器和输出迭代器的组合。本文的目的是帮助读者更好地理解STL迭代器的使用方法和特点。 ... [详细]
  • 使用nodejs爬取b站番剧数据,计算最佳追番推荐
    本文介绍了如何使用nodejs爬取b站番剧数据,并通过计算得出最佳追番推荐。通过调用相关接口获取番剧数据和评分数据,以及使用相应的算法进行计算。该方法可以帮助用户找到适合自己的番剧进行观看。 ... [详细]
  • Java容器中的compareto方法排序原理解析
    本文从源码解析Java容器中的compareto方法的排序原理,讲解了在使用数组存储数据时的限制以及存储效率的问题。同时提到了Redis的五大数据结构和list、set等知识点,回忆了作者大学时代的Java学习经历。文章以作者做的思维导图作为目录,展示了整个讲解过程。 ... [详细]
  • 本文介绍了九度OnlineJudge中的1002题目“Grading”的解决方法。该题目要求设计一个公平的评分过程,将每个考题分配给3个独立的专家,如果他们的评分不一致,则需要请一位裁判做出最终决定。文章详细描述了评分规则,并给出了解决该问题的程序。 ... [详细]
  • 本文主要解析了Open judge C16H问题中涉及到的Magical Balls的快速幂和逆元算法,并给出了问题的解析和解决方法。详细介绍了问题的背景和规则,并给出了相应的算法解析和实现步骤。通过本文的解析,读者可以更好地理解和解决Open judge C16H问题中的Magical Balls部分。 ... [详细]
  • 本文讨论了使用差分约束系统求解House Man跳跃问题的思路与方法。给定一组不同高度,要求从最低点跳跃到最高点,每次跳跃的距离不超过D,并且不能改变给定的顺序。通过建立差分约束系统,将问题转化为图的建立和查询距离的问题。文章详细介绍了建立约束条件的方法,并使用SPFA算法判环并输出结果。同时还讨论了建边方向和跳跃顺序的关系。 ... [详细]
  • 本文介绍了如何在给定的有序字符序列中插入新字符,并保持序列的有序性。通过示例代码演示了插入过程,以及插入后的字符序列。 ... [详细]
  • 20211101CleverTap参与度和分析工具功能平台学习/实践
    1.应用场景主要用于学习CleverTap的使用,该平台主要用于客户保留与参与平台.为客户提供价值.这里接触到的原因,是目前公司用到该平台的服务~2.学习操作 ... [详细]
  • 如何用UE4制作2D游戏文档——计算篇
    篇首语:本文由编程笔记#小编为大家整理,主要介绍了如何用UE4制作2D游戏文档——计算篇相关的知识,希望对你有一定的参考价值。 ... [详细]
  • 深入理解Kafka服务端请求队列中请求的处理
    本文深入分析了Kafka服务端请求队列中请求的处理过程,详细介绍了请求的封装和放入请求队列的过程,以及处理请求的线程池的创建和容量设置。通过场景分析、图示说明和源码分析,帮助读者更好地理解Kafka服务端的工作原理。 ... [详细]
  • 使用圣杯布局模式实现网站首页的内容布局
    本文介绍了使用圣杯布局模式实现网站首页的内容布局的方法,包括HTML部分代码和实例。同时还提供了公司新闻、最新产品、关于我们、联系我们等页面的布局示例。商品展示区包括了车里子和农家生态土鸡蛋等产品的价格信息。 ... [详细]
  • Ihaveaworkfolderdirectory.我有一个工作文件夹目录。holderDir.glob(*)>holder[ProjectOne, ... [详细]
  • .NetCoreWebApi生成Swagger接口文档的使用方法
    本文介绍了使用.NetCoreWebApi生成Swagger接口文档的方法,并详细说明了Swagger的定义和功能。通过使用Swagger,可以实现接口和服务的可视化,方便测试人员进行接口测试。同时,还提供了Github链接和具体的步骤,包括创建WebApi工程、引入swagger的包、配置XML文档文件和跨域处理。通过本文,读者可以了解到如何使用Swagger生成接口文档,并加深对Swagger的理解。 ... [详细]
author-avatar
手机用户2502933795
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有