REST网络应用程序中的分页

343

这是对该问题的更通用的表述(消除了Rails特定部分)。

在RESTful web应用程序中,我不确定如何对资源实现分页。假设我有一个名为products的资源,以下哪种方法是最佳选择?为什么:

1. 仅使用查询字符串

例如:http://application/products?page=2&sort_by=date&sort_how=asc
这里的问题是我无法使用全页缓存,而且URL不够简洁易记。

2. 使用页面作为资源,使用查询字符串进行排序

例如:http://application/products/page/2?sort_by=date&sort_how=asc
在这种情况下,我看到的问题是http://application/products/pages/1不是唯一的资源,因为使用sort_by=price可能会得到完全不同的结果,并且我仍然无法使用页面缓存。

3. 使用页面作为资源和URL段进行排序

例如:http://application/products/by-date/page/2
我个人认为使用这种方法没有问题,但有人警告我这不是一个好的方式(他没有给出原因,如果您知道为什么不推荐,请告诉我)

欢迎提出任何建议、意见和批评。谢谢。


10
好的,我会尽力进行翻译。以下是需要翻译的内容:奖金问题:人们通常如何指定页面大小? - Heiko Rupp
1
不要忘记矩阵参数 http://www.w3.org/DesignIssues/MatrixURIs.html - CMCDragonkai
13个回答

113

我同意Fionn的看法,但我会更进一步地说,对我来说页面不是一个资源,而是请求的属性。这使我选择了只使用查询字符串的选项1。这种方式感觉很正确。我真的很喜欢Twitter API的RESTful结构。它既不太简单,也不太复杂,文档编写也很好。无论好坏,这是我在两种设计方式之间犹豫时的“首选”。


29
查询字符串并不是一流的资源标识符,它们只是用于对资源进行排序和分组的说明。 - S.Lott
1
@S.Lott 请求就是资源。你所谓的“一级资源”在菲尔丁的论文第5.2.1.1节中被定义为。此外,在同一节中,菲尔丁以源代码文件的最新修订版本作为资源的例子。这怎么可能成为一个资源,但最新的10个产品却成为“在产品资源上请求的属性”?我理解你的观点更为实用,但我认为它不太符合RESTful的思想。 - edsioufi
请注意,我的评论并不意味着我反对使用查询字符串而不是URL:只要API是超媒体驱动的,就两种方法都可行,正如@RichApodaca在他的回答中提到的那样。我只是指出从REST的角度来看,页面应该被视为一种资源。 - edsioufi

69

我认为版本3的问题更多是一个“观点”问题——您是否将页面视为资源还是页面上的产品。

如果您将页面视为资源,则这是一个完全合适的解决方案,因为对于第2页的查询将始终产生第2页。

但是,如果您将页面上的产品视为资源,则会出现问题,因为第2页上的产品可能会更改(旧产品被删除或其他原因),在这种情况下URI不总是返回相同的资源。

例如,客户存储了指向产品列表页面X的链接,下次打开该链接时,所询问的产品可能已不在页面X上。


6
如果您删除了某些内容,就不应该在同一个URI上出现其他内容。 如果您删除页面X的所有产品-页面X仍然可能是有效的,但现在包含来自页面X + 1的产品。因此,在“产品资源视图”中查看时,页面X的URI已变为页面X + 1的URI。 - Fionn
1
如果您将页面视为资源,则这是一个完美的解决方案,因为对于第2页的查询将始终产生第2页。这是否有意义?无论您将其作为资源还是其他任何内容,都将始终返回相同的URL(包含第2页的任何URL)并返回第2页。 - temoto
2
看到页面作为资源,可能应该引入POST /foo/page来创建新页面,对吧? - temoto
19
你的回答已经很流畅地指向了“正确的解决方案是1”,但没有明确表述。 - temoto
2
在我看来,页面是一个浮动的概念,与底层域无关。因此,不应将其视为资源。我所说的浮动是指它是流动的,页面的概念随着上下文而变化;你的API的一个用户可能是移动应用程序,每页只能消耗2个产品,而另一个用户可能是机器应用程序,可以消耗整个列表。简而言之,页面是底层域实体(产品)的“表示”,不应作为URL的一部分;只作为查询参数。 - Kingz
显示剩余2条评论

38

HTTP有一个非常适合分页的Range头部。您可以发送

Range: pages=1

只想要第一页。这可能会迫使您重新考虑什么是一个页面。也许客户端想要不同范围的项目。Range头也可以用来声明顺序:

Range: products-by-date=2009_03_27-

获取所有比该日期更新的产品或

Range: products-by-date=0-2009_11_30

获取所有早于该日期的产品。'0'可能不是最好的解决方案,但RFC似乎希望为范围起始点提供某些内容。可能存在无法解析units=-range_end的HTTP解析器。

如果headers不是(可接受的)选项,则我认为第一种解决方案(全部在查询字符串中)是处理页面的一种方式。但请规范化查询字符串(按字母顺序排序(键=值对)。这可以解决“?a=1&b=x”和“?b=x&a=1”的区别问题。


35
标题可能一开始看起来很好,但它们会禁止分享页面(例如通过复制URL)。因此,对于ajax请求,它们可能是一个不错的解决方案(因为由ajax修改的页面无论如何都不能在当前状态下共享),但我不会将它们用于常规分页。 - Markus
4
Range 头部仅用于字节范围。请参阅 HTTP 头部规范,第 14.35 节。 - Chris Westin
16
HTTP/1.1使用范围单位来处理Range(第14.35节)和Content-Range(第14.16节)头字段。 range-unit = bytes-unit | other-range-unit也许你指的是 HTTP/1.1定义的唯一范围单位是“bytes”。 HTTP/1.1实现可以忽略使用其他单位指定的范围。 这与你的陈述不同。 - temoto
1
@Markus 我想不到你分享 Rest API 资源的使用情况 :) - JakubKnejzlik
@JakubKnejzlik 分享不是问题,但使用HTTP头来进行分页会阻止使用HATEOAS链接进行分页。 - xarx

26

如果你的应用程序将分页视为生成同一资源的不同视图的技术,则选项1似乎是最佳选择。

话虽如此,URL方案相对不重要。如果您设计应用程序为超文本驱动(根据定义,所有REST应用程序都必须是这样),那么您的客户端将不会自己构建任何URI。相反,您的应用程序将向客户端提供链接,而客户端将跟随这些链接。

您的客户端可以提供一种链接类型即分页链接。

所有这些的一个好处是,即使下周您改变了分页URI结构并实现了完全不同的东西,您的客户端也可以在不进行任何修改的情况下继续工作。


3
关于在REST网络服务中使用超媒体链接的友好提醒。 - Paul D. Eden

12

我一直使用选项1的样式,因为在我的情况下数据经常更改,所以缓存不是一个问题。如果允许页面大小可配置,那么数据就无法被缓存。

我认为URL不难记忆或不干净。对我来说,这是查询参数的良好使用。该资源显然是产品列表,而查询参数只是告诉你想要如何显示列表-排序和哪一页。


1
+1 我认为你是正确的,我会选择查询参数(选项1)。 - andi
“我不觉得这个URL难记。” 这个观察在REST应用程序中毫无用处,因为这些应用程序通常只应该有一个书签... 如果用户(或客户端应用程序)试图“记住”URL,则这表明API不符合restful原则。 - edsioufi

10
奇怪的是,没有人指出选项3具有特定顺序的参数。 http//application/products/Date/Descending/Name/Ascending/page/2http//application/products/Name/Ascending/Date/Descending/page/2 指向相同的资源,但具有完全不同的URL。
对我来说,选项1似乎是最可接受的,因为它清楚地将“我想要什么”和“我想要如何”分开(它们之间甚至有问号)。可以使用完整的URL实现全页缓存(所有选项都会遭受相同问题)。
使用URL参数的方法唯一的好处是干净的URL。虽然您必须想出某种编码参数并无损解码它们的方式。当然,您可以使用URL编码/解码,但这会再次使URL变得丑陋 :)

1
这是两种不同的排序方式。第一种按日期降序排序,仅在名称升序方面打破平局;第二种按名称升序排序,仅在日期降序方面打破平局。 - Imran Rashid
事实上,这里给出的两个示例URL不仅在书写上不同,而且在含义上也不同。由于表示路径,不能保证当您先向左转再向右转或反之时找到相同的内容。话虽如此,将排序参数作为URL路径部分具有形式上的优势,而URL参数应该是可交换的,而不会改变整体含义,但确实会遇到编码陷阱,正如本文所述。 - Christian Gosch

8
我找到了一个相关的最佳实践网站:http://www.restapitutorial.com。在资源页面中,可以下载一份包含作者推荐REST最佳实践的完整PDF文件。其中包括有关分页的部分。作者建议将“范围头”和“查询字符串参数”两种方式都支持。 请求 下面是HTTP头示例:
Range: items=0-24

查询字符串参数示例:

GET http://api.example.com/resources?offset=0&limit=25

其中 offset 是起始项目编号,limit 是返回的最大项目数。

响应

响应应包括一个 Content-Range 头部,指示正在返回多少个项目以及尚未检索到多少个项目。

HTTP 头部示例:

Content-Range: items 0-24/66

Content-Range: items 40-65/*

在这个pdf文件中还有一些其他针对特定情况的建议。

7

我更倾向于使用查询参数offset和limit。

offset: 指定集合中条目的索引。

limit: 指定条目的数量。

客户端可以通过简单地更新偏移量来获取数据,如下所示:

offset = offset + limit

下一页的路径被认为是资源标识符。页面不是资源,而是资源集合的子集。由于分页通常是GET请求,因此查询参数最适合用于分页,而不是标题。
参考: https://metamug.com/article/rest-api-developers-dilemma.html#Requesting-the-next-page

5

我目前在我的ASP.NET MVC应用程序中使用类似于以下方案:

例如:http://application/products/by-date/page/2

具体而言,它是:http://application/products/Date/Ascending/3

然而,我并不满意以这种方式在路由中包含分页和排序信息。

物品列表(在此情况下为产品)是可变的。也就是说,下一次有人返回到包括分页和排序参数的URL时,他们得到的结果可能已经发生了变化。因此,http://application/products/Date/Ascending/3 作为一个唯一的URL,指向一个定义的、不变的产品集合的想法已经消失了。


1
第一个问题,关于多列排序,我认为所有3种方法都适用。因此,它并不是任何一种方法的利弊。关于第二个问题:这不可能发生在任何资源上吗?例如,产品也可以被编辑/删除。 - andi
我认为在多列上排序对于所有三种方法来说都是一个“缺点”,因为URL会变得越来越大且难以管理 - 这也是我考虑转移到基于表单的页面/排序参数的原因之一。对于第二个问题,我认为唯一持久标识符(如产品ID)与瞬态产品列表之间存在根本概念上的差异。对于已删除的产品,例如“该产品不存在于系统中”的消息会向您传达有关该产品的具体信息。 - Steve Willcock
1
从路由中删除所有分页和排序信息是好的。将它们推入POST参数是不好的。你好?问题是关于REST的。我们不仅仅是为了在REST中缩短URL而使用POST。动词有意义。 - temoto
1
个人而言,我不会使用表单参数来进行查询,因为这几乎需要使用POST或PUT HTTP方法(因为现在请求中有一个主体)。GET似乎是更合适的方法,因为POST和PUT都意味着修改资源。因此,当需要按多列排序时,我会选择向URL添加更多的查询参数。 - Paul D. Eden

1

我倾向于同意slf的观点,即“页面”并不是真正的资源。另一方面,选项3更清晰、易读,并且用户甚至可以根据需要直接输入。我对选项1和选项3犹豫不决,但我没有看到任何理由不使用选项3。

此外,正如有人提到的那样,使用隐藏参数而不是查询字符串或URL段的一个缺点是用户无法将特定页面添加到书签或直接链接。这可能是一个问题,也可能不是,这只是需要注意的一点。


1
关于您提到易于猜测的问题,这并不重要。如果构建超媒体API,用户永远不应该必须猜测URI。 - J.R. Garcia

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接