这是对 this question 的更通用的重新表述(消除了 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
我个人认为使用这种方法没有问题,但有人警告我这不是一个好方法(他没有给出理由,所以如果你知道为什么不推荐,请告诉我)
任何建议,意见,批评都非常受欢迎。谢谢。
我同意 Fionn 的观点,但我会更进一步说,对我来说,页面 不是 资源,它是请求的属性。这让我只选择了选项 1 查询字符串。感觉刚刚好。我真的很喜欢 Twitter API 的结构平静。不太简单,不太复杂,有据可查。无论是好是坏,当我在做某事时以一种方式与另一种方式做某事时,这是我的“首选”设计。
我认为版本 3 的问题更多是“观点”问题——您将页面视为资源还是页面上的产品。
如果您将页面视为资源,这是一个完美的解决方案,因为对第 2 页的查询将始终产生第 2 页。
但是,如果您将页面上的产品视为资源,您就会遇到问题,即第 2 页上的产品可能会更改(旧产品已删除或其他),在这种情况下,URI 并不总是返回相同的资源。
例如,客户存储了指向产品列表页面 X 的链接,下次打开该链接时,相关产品可能不再出现在页面 X 上。
HTTP 有很好的 Range 标头,也适合分页。你可以寄
Range: pages=1
只有第一页。这可能会迫使您重新考虑什么是页面。也许客户想要不同范围的物品。 Range 标头也可用于声明订单:
Range: products-by-date=2009_03_27-
获得比该日期更新的所有产品或
Range: products-by-date=0-2009_11_30
获取早于该日期的所有产品。 '0' 可能不是最好的解决方案,但 RFC 似乎想要范围开始的东西。可能部署的 HTTP 解析器不会解析units=-range_end。
如果标题不是(可接受的)选项,我认为第一个解决方案(全部在查询字符串中)是一种处理页面的方法。但是请规范化查询字符串(按字母顺序排序(键=值)对)。这解决了“?a=1&b=x”和“?b=x&a=1”的微分问题。
range-unit = bytes-unit | other-range-unit
也许您指的是 The only range unit defined by HTTP/1.1 is "bytes". HTTP/1.1 implementations MAY ignore ranges specified using other units.
这与您的陈述不同。
选项 1 似乎是最好的,因为您的应用程序将分页视为一种为同一资源生成不同视图的技术。
话虽如此,URL 方案是相对无关紧要的。如果您将应用程序设计为 hypertext-driven(因为所有 REST 应用程序都必须按照定义),那么您的客户端将不会自行构建任何 URI。相反,您的应用程序将向客户端提供链接,客户端将跟随它们。
您的客户可以提供的一种链接是分页链接。
所有这些令人愉快的副作用是,即使您改变了对分页 URI 结构的想法并在下周实施完全不同的东西,您的客户也可以继续工作而无需任何修改。
我一直使用选项 1 的风格。缓存不是问题,因为在我的情况下数据经常更改。如果您允许页面的大小是可配置的,那么再次无法缓存数据。
我不觉得网址难以记住或不干净。对我来说,这是对查询参数的一种很好的使用。该资源显然是一个产品列表,查询参数只是告诉您希望列表如何显示 - 排序和哪个页面。
奇怪的是,没有人指出选项 3 具有特定顺序的参数。 http//application/products/Date/Descending/Name/Ascending/page/2 和 http//application/products/Name/Ascending/Date/Descending/page/2
指向相同的资源,但具有完全不同的 url。
对我来说,选项 1 似乎是最容易接受的,因为它清楚地将“我想要什么”和“我想要什么”分开(它们之间甚至还有问号,哈哈)。可以使用完整的 URL 实现整页缓存(无论如何,所有选项都会遇到同样的问题)。
使用 URL 中的参数方法的唯一好处是干净的 URL。尽管您必须想出一些方法来编码参数并无损解码它们。当然你可以使用 URLencode/decode,但它会让 url 再次变得丑陋 :)
寻找我遇到的这个网站的最佳实践:
http://www.restapitutorial.com
在资源页面中有一个下载 .pdf 的链接,其中包含作者建议的完整 REST 最佳实践。其中有一个关于分页的部分。
作者建议添加对使用 Range 标头和使用查询字符串参数的支持。
要求
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 中有一些针对更具体案例的其他建议。
我更喜欢使用查询参数偏移量和限制。
偏移量:用于集合中项目的索引。
限制:用于项目计数。
客户端可以简单地不断更新偏移量,如下所示
offset = offset + limit
下一页。
该路径被视为资源标识符。并且页面不是资源,而是资源集合的子集。由于分页通常是 GET 请求,因此查询参数最适合分页而不是标题。
参考:https://metamug.com/article/rest-api-developers-dilemma.html#Requesting-the-next-page
我目前在我的 ASP.NET MVC 应用程序中使用与此类似的方案:
例如http://application/products/by-date/page/2
具体来说是:http://application/products/Date/Ascending/3
但是,我对以这种方式在路由中包含分页和排序信息并不满意。
项目列表(在这种情况下为产品)是可变的。即下次有人返回包含分页和排序参数的url时,他们得到的结果可能已经改变。因此,将 http://application/products/Date/Ascending/3
作为指向一组已定义、不变的产品的唯一 url 的想法就消失了。
我倾向于同意 slf 的观点,即“页面”并不是真正的资源。另一方面,选项 3 更简洁,更易于阅读,用户更容易猜到甚至在必要时输入。我在选项 1 和 3 之间纠结,但没有看到任何不使用选项 3 的理由。
此外,虽然它们看起来不错,但正如有人提到的那样,使用隐藏参数而不是查询字符串或 URL 段的一个缺点是用户无法添加书签或直接链接到特定页面。这可能是也可能不是问题,具体取决于应用程序,但只是需要注意的事情。
我之前使用过解决方案 3(我写了很多 django 应用程序)。而且我不认为这有什么问题。它与其他两个一样可生成(以防您需要进行大规模刮擦等),并且看起来更干净。另外,您的用户可以猜测 url(如果它是面向公众的应用程序),人们喜欢能够直接去他们想要的地方,并且 url-guessing 感觉授权。
我在我的项目中使用以下网址:
http://application/products?page=2&sort=+field1-field2
这意味着 - “给我页面的第二页按 field1 升序,然后按 field2 降序”。或者,如果我需要更大的灵活性,我会使用:
http://application/products?skip=20&limit=20&sort=+field1-field2
我使用以下模式来获取下一页记录。 http://application/products?lastRecordKey=?&pageSize=20&sort=ASC
RecordKey 是在 DB 中保存顺序值的表的列。这用于从 DB 一次仅获取一页数据。 pageSize 用于确定要获取多少条记录。 sort 用于对记录进行升序或降序排序。