【51CTO.com快译】众所周知,我们设计API的目标往往是要通过我们的服务,为用户提供一定的功能。虽然HTTP和URL资源都允许数据流进行一定程度的基本交互,但是它们在面对其他特定需求时,往往会让您的API显得力不从心。在此,我们以分页为例,即:如果某个数据库中存放着上百万篇的文章,那么我们很可能无法在单次响应中,将每一篇文章都发送给客户。针对此类需求,业界提出了参数化(parametrization)的解决方法。
什么是参数化?
通常而言,参数化是一种请求配置。在编程语言中,我们可以通过某个函数来请求对应的返回值。如果一个函数不带任何参数,那么我们将无法直接去影响它的返回值。
API也是如此,尤其是那些无状态的REST API。毕竟,所有REST的交互都是无状态的。也就是说,每个请求都会包含那些方便连接器理解该请求的所有信息,而且它们与之前的任何请求都无关。
在实际应用中,我们有许多种方法可以向HTTP请求中添加参数,其中包括:查询字符串,POST、PUT报文,各种PATCH请求、以及标头(header)。它们各自都有自己的用例和规则。
那么,添加所有参数数据的最简单方法,就是将所有内容放入报文内。在具体应用中,每个端点都会用到POST方法,而许多API也会把所有的参数放置在报文中。长此以往,传统的API里积累了越来越多的参数,以致于它们不再适合查询字符串等操作。显然,我们应当避免在设计API时此类极端情况的发生。
添加何种参数?
就REST而言,作为API查询语言的GraphQL,为用户提供了满足数据查询的运行时。那么,我们是否应当添加那些在HTTP规范中已经标准化的标头字段类来作为参数(请参见:http://www.rfcreader.com/#rfc2616_line4589)呢?
- 如果使用Accept标头,我们可以定义响应所采用的格式或媒体类型。我们既可以使用它来告知API所需要的JSON或XML,又可以使用它来获取API的版本(请参见:https://www.moesif.com/blog/technical/api-design/Best-Practices-for-Versioning-REST-and-GraphQL-APIs/?utm_source=dzone&utm_medium=paid&utm_campaign=placed%20article&utm_term=rest%20api%20design%20best%20practices%20for%20parameters%20and%20query%20string%20usage)。
- 而如果使用Cache-Control标头,我们则可以防止API发送带有no-cache的缓存响应,且无需使用查询字符串作为cache buster(?cb=)。
- 当然,授权也可以被视为一种参数。根据API在实际授权过程中的详细信息,是否通过授权,来产生不同种类的响应。因此,HTTP也为此定义了一个授权类型的标头(请参见:http://www.rfcreader.com/#rfc2616_line4922)。
在我们了解了各种默认标头字段后,下面我们来讨论是否应该为自己的参数创建一个自定义的标头字段,或者将其放入URL的查询字符串中。
何时该使用查询字符串?
如果我们已获悉待添加的参数不属于默认的标头字段,且并不敏感,那么我们就应当通过查看查询字符串,以确认是否合适。在查询字符串的时候,有个<isindex>的HTML元素,可被用于向服务器发送一些关键字。而服务器则会据此做出响应,并列出与关键字相匹配的页面列表。接着,查询字符串会被重新用于Web表单,以通过GET的请求方式,将数据发送到服务器处。
因此,查询字符串的主要用例便是过滤,它会着重过滤搜索和分页这两种特殊的情况。有关此方面的详细讨论,请参见:https://www.moesif.com/blog/technical/api-design/REST-API-Design-Filtering-Sorting-and-Pagination/?utm_source=dzone&utm_medium=paid&utm_campaign=placed%20article&utm_term=rest%20api%20design%20best%20practices%20for%20parameters%20and%20query%20string%20usage。不过,正如针对Web表单的目的所示,它可以被用于不同类型的参数。而RESTful API可以将POST或PUT请求与报文一起使用,以实现将表单数据发送给服务器。
我们来看一个有关嵌套表示(nested representation)的参数示例。默认情况下,我们需要返回文章的普通表示形式(plain representation)。当我们将?withComments查询字符串添加到端点时,?withComments会以普通表示形式返回某篇文章的评论,而且仅需要一个请求即可。至于此类参数是应该被放在自定义的标头中,还是查询字符串里,则主要取决于开发人员的个人经验与偏好。
根据HTTP的规范陈述(请参见:http://www.rfcreader.com/#rfc2616_line1761):标头字段可以被视为函数的参数。不过,将查询字符串添加到URL中会明显比创建客户端标头,要更快、更显著。实际上,这些字段充当了请求修饰符,其语义等同于编程语言在方法调用中的参数。
在实际应用中,您会发现:在所有端点上保持相同的参数更适合于标头。例如:身份验证令牌可以由每个请求所发送。而那些高度动态的参数(尤其是仅对少数几个端点有效的参数)则应该被放在查询字符串中。例如:每个端点的过滤器参数都会有所不同。
数组和映射参数
对于开发人员而言,他们可能会经常碰到的一个问题是:如何处理查询字符串中的数组参数?例如,我们可能碰到需要搜索多个名称的需求场景。而通常的解决方案是:使用方括号。如下列代码所示:
不过,HTTP的规范指出:由IPv6 [RFC3513]或更高版本标识的主机通过将IP方括号(“[”和“]”)来区分。这是URI语法中唯一允许使用方括号字符的地方。
我们在许多HTTP服务器和客户端的实现场景中,都应当牢记上述规范。当然,为了简便起见,我们可以采用“多次使用一个参数名称”的另一种解决方案。如下列代码所示:
该方法虽然有效,但是它会导致开发人员体验上的下降。通常情况下,客户端只会使用类似于地图(map-like)的数据结构。而该结构在被添加到URL中之前,会进行简单的字符串转换。而这恰恰会导致后续的数值被覆盖。可见,在发送请求之前,我们需要进行更加复杂的转换。
与此同时,也有人会采用另一种方法:使用“,”字符来分隔数值,直接在URL中出现未经编码的字符。如下列代码所示:
而对于那些类似地图的数据结构,我们也可以使用无需编码的“.”字符。如下列代码所示:
另外,您还可以对整个查询字符串进行URL编码(请参见:https://en.wikipedia.org/wiki/Percent-encoding),以便直接使用任何想要的字符或格式。不过值得一提的是,这同样也会大幅降低开发人员的使用体验。
何时不该使用查询字符串?
在实际应用中,由于查询字符串往往是URL的一部分。而那些隐藏在客户端和API之间的攻击者(如:中间人,MIM)则可以轻而易举地读取到我们的URL,因此我们不应该简单地将诸如密码之类的敏感数据,直接放入查询字符串之中。
虽然大多数HTTP客户端在URL中都会允许使用五位数(five-figure)长度的字符,但是如果我们未能全面设计和考虑URL的整体长度,那么开发人员在调试此类字符串时,往往也会经历各种繁琐和不便。
当然,由于我们能够将任何内容都定义为资源,因此我们有时候完全可以使用POST端点,来进行大量参数的调用与传递,进而将报文中的所有数据都发送给API。
可见,为了避免向查询字符串中那些具有多个参数的资源发送GET请求,进而导致冗长且不可识别的URL产生,我们可以设计出诸如搜索类型的资源,根据API的需求,来缓存各种计算的结果。同时,我们还可以向/searches端点发布新的请求。而该请求会将我们的搜索相关配置与参数保存在报文之中。通过返回一个搜索ID,以便我们使用它来获取搜索的结果。
总结
作为API设计人员或架构师,我们所追求的优秀实践,实际上就是要找出API的最理想使用方式,以及最简单的实现用例。综上所述,我们建议您注意如下两个方面:
- 从一开始就能够借助Moesif(人工智能API服务平台)之类的服务,着手分析自身API的使用模式。
- 嵌套式资源虽然可以提高URL的可读性,但是如果我们嵌套得太多的话,则会导致URL参数的冗长。因此,如果您发现自己创建了一个具有超大的查询字符串端点,那么我建议您最好从其中提取一部分子资源,放置到报文中作为参数发送出去。
原文标题:REST API Design Best Practices for Parameters and Query String Usage,作者:Kay Ploesser
【51CTO译稿,合作站点转载请注明原文译者和出处为51CTO.com】