Django Rest Framework 5:关系和超链接API

目前我们的 API 中的关系是用主键表示的。我们将通过使用超链接来提高我们 API 的内部联系。

文章目录

一,内容协商

(一)内容协商概述

1,内容协商的基本原则

内容协商是当有资源有多种表示可用时,为给定响应选择最佳表示的过程:

  • 一份特定的文件称为一项资源。整个资源,连同它的各种表现形式,共享一个特定的 URL 。
  • 当客户端想要获取资源的时候,会使用其对应的 URL 发送请求。
  • 服务器通过这个 URL 来选择这个请求指向的资源的某一变体——每一个变体就是资源的一种表现形式——然后将这个选定的表现形式返回给客户端。

2,为什么会有内容协商

同一项资源可以有不同的表现形式,简单来说就是不同的数据格式.

举个比较具体的例子,比如使用这样一个 URL 时:http://127.0.0.1:8000/books/v1,客户端既可以显式地通过指定后缀的方式指定所需要的格式:http://127.0.0.1:8000/books/v1/1.pdf 或者http://127.0.0.1:8000/books/v1/1.html等,也可以通过显式地通过指定关键字的方式指定所需要的格式:http://127.0.0.1:8000/snippets/v1/1?format=xml 或者 http://127.0.0.1:8000/snippets/v1/1?format=json等。

这样显得非常直观,但却并不符合 RESTful URL的定义(RESTful URL的定义本身就是对 HTTP 提供的基础设施的充分利用),也并不一定能让客户端的请求得到真正的满足:万一服务器无法返回该资源的某种特定表示呢?

基于这两点,客户端与服务器在与资源实体的相关的信息交互中,应该做这样几件事:

  • 客户端剥离 URL 中除资源本身之外的格式信息,交由 HTTP 中的基础设施进行实现。
  • 服务器以默认(或者说最佳)的表现形式在响应中返回资源实体。

总之,有这样一些关键看法:

  • 应该充分利用 HTTP 协议提供的特性,减少不必要的自定义行为。
  • 请求不是命令,要什么,就双方协商着来,这样能够屏蔽客户端之间的差异,从而实现长期的 URL 功能。
  • 之所以叫内容协商而不叫格式协商,是因为内容可被视为格式的超集,而不仅仅是具体的媒体类型media_type

Content Negotiation in RFC 2616
Understanding Content Negotiation in Web API
Why would HTTP content negotiation be preferred to explicit parameters in an API scenario?
Content Negotiation For Web API Longevity

3,内容协商机制

客户端和服务器端之间存在多种协商方式,有些已经被逐渐淘汰了。这里只介绍两种最主要的方式:

  • 如果响应的最佳表示是由位于服务器的逻辑进行的,则称为服务器驱动的内容协商。
  • 如果响应的最佳表示是在代理或客户端进行的,则称为代理驱动的内容协商。
(1)服务器驱动的内容协商

1,当客户端发送 HTTP 请求时,它可以通过请求头中的 Accept 指定它可以接受的媒体类型。举个例子🌰:

Accept: text/xml, application/xml, application/json

客户端也能指定特殊供应商的媒体类型。举个例子🌰:

application/vnd.ms-excel
application/vnd.sif.v2.studentpersonal+json

客户端可以通过AcceptAccept-CharsetAccept-EncodingAccept-Language指定详细的响应要求。

2,服务器接收请求后,根据内容协商规则选择最合适的类型,在设置 Content-Type 以包含实际发送的资源类型、使用的字符集、编码方式、内容语言等信息后返回。
如果客户端发发送的请求不包含 Accept ,则服务器可以自由地提供它认为最好的任何表示。
总之,就是服务器根据客户端的请求头的情况,决定:

  • 在 HTTP 响应中返回一种表示
  • 重定向到包含请求的表示之一的不同 URL
  • 发送 415错误(不支持的媒体类型)

3,客户端接收到响应后,就根据 Content-Type 对响应进行解析。

在这里插入图片描述
即便服务驱动型内容协商机制相对直观,但它存在如下几个缺点:

  • 实际上,服务器可返回它认为合理的一切,而并不完全遵守于客户端的要求。
  • 因为给定的资源需要返回不同的展现形式,共享缓存的效率会降低,而服务器端的实现会越来越复杂。
  • 面对不同的资源请求,可能会导致客户端需要使用越来越复杂的请求头内容。
(2)客户端/代理驱动的内容协商

1,客户端首先通过 URL 发送请求。
2,服务器会在遇到模棱两可的请求时返回一个页面,该页面包含指向该资源所有可用表示的链接。
3,客户端合适的链接,再次发送请求。
4,服务器解析请求,设置 Content-Type 指定请求所需要的表示,并返回。
5,客户端接收到响应后,就根据 Content-Type 对响应进行解析。
在这里插入图片描述
在确保内容协商成功率时,这也导致一些问题:

  • HTTP 标准没有指定用于在可用资源之间进行选择的页面格式,、除了回退到服务器驱动的协商之外,这种方法几乎总是与额外的脚本一起使用:在检查协商标准后, JavaScript 脚本执行重定向。
  • 需要一个额外的请求来获取真正的资源,减慢了资源对用户的可用性。

Content negotiation
Accept 与 Content-Type
Apache HTTP Server Version 2.4 Content Negotiation
Content Negotiation
Content Negotiation in REST
HTTP协议的内容协商

(二)REST framework中的内容协商机制

1,确定可接受的渲染器

REST framework使用一种简单的内容协商方式:根据可用的渲染器、每个渲染器的优先级以及客户端的 Accept 请求头来确定应该将哪个媒体类型返回给客户端。

  • 更具体的媒体类型优先于不太具体的媒体类型。
  • 如果多个媒体类型具有相同的特异性,则根据为给定视图配置的渲染器的顺序优先选择。

如果客户端的请求头包括:

Accept: application/json; indent=4, application/json, application/yaml, text/html, */*

给定媒体类型的优先级为:

  • application/json; indent=4
  • application/json, application/yaml, text/html
  • /

如果被请求的视图只配置了 YAMLHTML 的渲染器,那么 REST framework 将选择 renderer_classes 属性或默认的 DEFAULT_RENDERER_CLASSES 配置中最先列出的渲染器。

  • REST framework 在确定优先级时不考虑“q”值。“q”值的使用会对缓存产生负面影响,在作者看来,它们是一种不必要的、过于复杂的内容协商方法。

2,自定义内容协商

REST framework 作者不太建议为 REST framework 提供一个定制的内容协商方案,默认的DefaultContentNegotiation已经能够很好的完成工作。但如果需要,则请重写BaseContentNegotiation类。

REST framework 的内容协商类应该对请求的适当解析器和响应的适当渲染器进行选择,因此需要重写 .select_parser(request, parsers).select_renderer(request, renderers, format_suffix) 方法。举个例子🌰:

  • select_parser() 方法应该从可用解析器列表中返回一个解析器实例,如果没有解析器能够处理传入的请求,则返回 None。
  • select_renderer() 方法应返回一个双元组(渲染器实例,媒体类型),或抛出 NotAcceptable 异常。
from rest_framework.negotiation import BaseContentNegotiation

class IgnoreClientContentNegotiation(BaseContentNegotiation):
    def select_parser(self, request, parsers):
        """
        Select the first parser in the `.parser_classes` list.
        """
        return parsers[0]

    def select_renderer(self, request, renderers, format_suffix):
        """
        Select the first renderer in the `.renderer_classes` list.
        """
        return (renderers[0], renderers[0].media_type)

具体可参考DefaultContentNegotiation的实现。

3,设置内容协商

可以使用 DEFAULT_CONTENT_NEGOTIATION_CLASS 设置来全局设置默认的内容协商类。举个例子🌰:

REST_FRAMEWORK = {
    'DEFAULT_CONTENT_NEGOTIATION_CLASS': 'myapp.negotiation.IgnoreClientContentNegotiation',
}

也可以通过 content_negotiation_class 属性设置用于单个类视图或视图集的内容协商。举个例子🌰:

from myapp.negotiation import IgnoreClientContentNegotiation
from rest_framework.response import Response
from rest_framework.views import APIView

class NoNegotiationView(APIView):
    """
    An example view that does not perform content negotiation.
    """
    content_negotiation_class = IgnoreClientContentNegotiation

    def get(self, request, format=None):
        return Response({
            'accepted media type': request.accepted_renderer.media_type
        })

二,解析器

REST 框架包括一些内置的Parser类,允许你接受各种媒体类型的请求。还支持定义自己的自定义解析器,这使你可以灵活地设计 API 能接受的媒体类型。

一组视图有效的解析器总是被定义为一个类的列表。当访问request.data获取请求数据时时,REST框架将检查传入请求中的Content-Type请求头,并自动确定用于解析请求内容的解析器。

  • 客户端应用发出的请求中,务必i指定Content-Type请求头,比如使用 jQuery 的 .ajax() 方法发送 json 编码数据,则应确保设置了 contentType: 'application/json'
  • 否则大多数客户端将它的值默认使用'application/x-www-form-urlencoded'

(一)设置解析器

可以使用DEFAULT_PARSER_CLASSES设置全局默认的解析器集。举个例子🌰:

REST_FRAMEWORK = {
    'DEFAULT_PARSER_CLASSES': (
        'rest_framework.parsers.JSONParser',
    )
}

也可以通过parser_classes属性设置用于单个类视图或视图集的解析器。举个例子🌰:

from rest_framework.parsers import JSONParser
from rest_framework.response import Response
from rest_framework.views import APIView

class ExampleView(APIView):
    """
    A view that can accept POST requests with JSON content.
    """
    parser_classes = [JSONParser]

    def post(self, request, format=None):
        return Response({'received data': request.data})

也可以通过@parser_classes装饰器设置用于单个函数视图的解析器。举个例子🌰:

from rest_framework.decorators import api_view
from rest_framework.decorators import parser_classes
from rest_framework.parsers import JSONParser

@api_view(['POST'])
@parser_classes([JSONParser])
def example_view(request, format=None):
    """
    A view that can accept POST requests with JSON content.
    """
    return Response({'received data': request.data})

(二)API 参考

1,JSONParser

解析 media_type: application/json 的JSON请求内容。

2,FormParser

解析.media_type: application/x-www-form-urlencoded 的 HTML 表单内容。

  • request.data将被填充为 QueryDict 格式的数据。

通常需要使用 FormParserMultiPartParser 两者,以便完全支持 HTML 表单数据。

3,MultiPartParser

解析.media_type: multipart/form-data的 HTML 表单内容,支持文件上传。

  • request.data将被填充为 QueryDict 格式的数据。

通常需要使用FormParserMultiPartParser两者,以便完全支持HTML多中文件上传表单的数据。

4,FileUploadParser

解析.media_type: */*的原始文件上传的内容。

  • request.data 属性将是有单个 key ‘file’ 的包含上传文件的字典。

如果与 FileUploadParser 一起使用的视图使用 filename URL关键字参数调用,则该参数将用作文件名。
否则客户端必须在Content-Disposition 请求头中设置文件名。例如 Content-Disposition: attachment; filename=upload.jpg. 举个例子🌰:

# views.py
class FileUploadView(views.APIView):
    parser_classes = [FileUploadParser]

    def put(self, request, filename, format=None):
        file_obj = request.data['file']
        # ...
        # do some stuff with uploaded file
        # ...
        return Response(status=204)

# urls.py
urlpatterns = [
    # ...
    re_path(r'^upload/(?P<filename>[^/]+)$', FileUploadView.as_view())
]

(三)自定义解析器

要实现一个自定义解析器,应该重写BaseParser类,设置.media_type属性,并实现.parse(self,stream,media_type,parser_context)方法:

class BaseParser:
    """
    All parsers should extend `BaseParser`, specifying a `media_type`
    attribute, and overriding the `.parse()` method.
    """
    media_type = None

    def parse(self, stream, media_type=None, parser_context=None):
        """
        Given a stream to read from, return the parsed representation.
        Should return parsed data, or a `DataAndFiles` object consisting of the
        parsed data and files.
        """
        raise NotImplementedError(".parse() must be overridden.")
  • stream:请求体的数据流。
  • media_type:可选的。如果提供,这是传入请求内容的媒体类型。根据请求的Content-Type:获取媒体类型,比渲染器的media_type属性更具体,因为前者可以包括媒体类型参数。例如 “text/plain; charset=utf-8”。
  • parser_context:可选的。如果提供,该参数将是一个包含解析请求内容可能需要的任何附加上下文的字典。默认情况下将包含以下keys: view, request, args, kwargs

举个例子🌰:一个示例纯文本解析器:使用表示请求正文的字符串填充request.data属性。

class PlainTextParser(BaseParser):
    """
    Plain text parser.
    """
    media_type = 'text/plain'

    def parse(self, stream, media_type=None, parser_context=None):
        """
        Simply return a string representing the body of the request.
        """
        return stream.read()

三,渲染器

REST框架包括许多内置的Renderer类,它们允许使用各种媒体类型返回响应。还支持定义你自己的自定义渲染器,这样可以灵活地设计你自己的媒体类型。

一组视图有效的渲染器总是被定义为一个类的列表。当请求流程进入视图时,REST框架将对传入的请求执行内容协商,并自动确定最适合的渲染器来满足请求。

内容协商的基本过程包括检查请求头的Accept,以确定响应中期望的媒体类型。URL 上可选的格式后缀可以用于显式地告知请求特定表示方式。例如 http://example.com/api/users_count.json 就是始终返回 JSON 数据。

(一)设置渲染器

可以使用DEFAULT_RENDERER_CLASSES设置全局默认的渲染器集。举个例子🌰:

REST_FRAMEWORK = {
    'DEFAULT_RENDERER_CLASSES': (
        'rest_framework.renderers.JSONRenderer',
        'rest_framework.renderers.BrowsableAPIRenderer',
    )
}

也可以通过renderer_classes属性设置用于单个类视图或视图集的渲染器。举个例子🌰:

from django.contrib.auth.models import User
from rest_framework.renderers import JSONRenderer
from rest_framework.response import Response
from rest_framework.views import APIView

class UserCountView(APIView):
    """
    返回JSON格式活动用户数的视图。
    """
    renderer_classes = (JSONRenderer, )

    def get(self, request, format=None):
        user_count = User.objects.filter(active=True).count()
        content = {'user_count': user_count}
        return Response(content)

也可以通过@renderer_classes装饰器设置用于单个函数视图的渲染器。举个例子🌰:

@api_view(['GET'])
@renderer_classes((JSONRenderer,))
def user_count_view(request, format=None):
    """
    返回JSON格式活动用户数的视图。
    """
    user_count = User.objects.filter(active=True).count()
    content = {'user_count': user_count}
    return Response(content)

(二)渲染器类的使用优先级

指定渲染器类时要考虑到每个媒体类型要分配哪些优先级,这一点非常重要。

如果一个客户端不能指定它可以接受的表示形式,例如发送 Accept: */*,或者无法包含 Accept ,那么REST框架将选择渲染器列表中的第一个渲染器。

如果你的 API 提供 JSON 响应和 browsable HTML API,则可能需要将 JSONRenderer 设置为你的默认渲染器,以便向不能指定 Accept 的客户端发送 JSON 响应。

如果你的 API 包含可以根据请求同时提供常规网页和 API 响应的视图,那么您可能会考虑TemplateHTMLRenderer使用默认渲染器,以便与无法发送的 Accept 的旧浏览器配合工作。

(三)API 参考

1,JSONRenderer

使用 utf-8 编码将请求的数据渲染成 JSON格式。

  • .media_type: application/json
  • .format: '.json'
  • .charset: None

默认样式包括 unicode 字符,并使用紧凑样式渲染响应:

{"unicode black star":"★","value":999}

客户端还可以使用包含indent媒体类型参数的Accept,的 JSON 缩进。例如Accept: application/json; indent=4

{
    "unicode black star": "★",
    "value": 999
}

2,TemplateHTMLRenderer

使用 Django 的标准模板将数据渲染成 HTML。

  • .media_type: text/html
  • .format: '.html'
  • .charset: utf-8

TemplateHTMLRenderer与其他渲染器不同:

  • 传递给 Response 的数据不需要序列化,可直接使用字典格式。
  • 可以在创建 Response 时包含一个 template_name 参数。
  • 如果正在构建TemplateHTMLRenderer与其他渲染器类一起使用的网站,则应该它列为renderer_classes中的第一个类,这样即使对于发送格式不正确的ACCEPT:请求头的浏览器,它都会被优先考虑。

TemplateHTMLRenderer将创建一个 RequestContext ,使用 response.data 作为上下文字典,并在指定的模板中渲染它。

  • 当与使用序列化器的视图一起使用时, response.data 在返回之前包装在字典中以允许 TemplateHTMLRenderer 渲染它。
response.data = {'results': response.data}

模板由以下优先级确定:

  1. 显式传递给 Responsetemplate_name参数。
  2. 显式定义的.template_name属性。
  3. view.get_template_names方法的返回结果。

举个例子🌰:

class UserDetail(generics.RetrieveAPIView):
    """
    A view that returns a templated HTML representation of a given user.
    """
    queryset = User.objects.all()
    renderer_classes = [TemplateHTMLRenderer]

    def get(self, request, *args, **kwargs):
        self.object = self.get_object()
        return Response({'user': self.object}, template_name='user_detail.html')

3,StaticHTMLRenderer

一个简单的渲染器,只需返回预渲染的HTML。

  • .media_type: text/html
  • .format: '.html'
  • .charset: utf-8

与其他渲染器不同的是,传递给 Response 对象的数据应该是要返回的内容的字符串格式。举个例子🌰:

@api_view(('GET',))
@renderer_classes((StaticHTMLRenderer,))
def simple_html_view(request):
    data = '<html><body><h1>Hello, world</h1></body></html>'
    return Response(data)

回到教程👨‍💻。
既然已经添加了代码高亮功能,马上要做的就是将它们进行合理的渲染,为此,我们打算专门用一个类视图来处理它们。

首先要考虑的就是使用哪种渲染器。由于我们并不打算把高亮代码渲染为 JSON 而只需要普通的 HTML 格式就行,同时由于我们也不打算专门用一个模板来渲染,所以 StaticHTMLRenderer 渲染器就能满足将 models.TextField 格式的字符串代码片段渲染为 HTML 格式的内容。

然后就是具体的执行逻辑。显然通用类视图就足够方便了,配置一下querysetrenderer_classes ,并重写一下 get 方法来返回 Snippet 对象实例的 highlighted 属性。

from rest_framework import renderers
from rest_framework.response import Response

class SnippetHighlight(generics.GenericAPIView):
    queryset = Snippet.objects.all()
    renderer_classes = (renderers.StaticHTMLRenderer,)

    def get(self, request, *args, **kwargs):
        snippet = self.get_object()
        return Response(snippet.highlighted)

和其他的继承通用类视图的方法差不多,只不过这里专门指定了渲染器。

当然还要记得添加路由:

# snippet/urls.py
urlpatterns = [
    ...
    path('snippets/<int:pk>/highlight/', views.SnippetHighlight.as_view()),
]

4,BrowsableAPIRenderer

将数据渲染成可浏览的API。
在这里插入图片描述

  • .media_type: text/html
  • .format: '.html'
  • .charset: utf-8
  • .template: 'rest_framework/api.html'

默认情况下,响应内容将以与BrowsableAPIRenderer不同的最高优先级渲染器渲染。如果你需要自定义此行为,例如使用 HTML 作为默认返回格式,但在可浏览的 API 中使用 JSON,则可以通过重写get_default_renderer()方法来实现。举个例子🌰:

class CustomBrowsableAPIRenderer(BrowsableAPIRenderer):
    def get_default_renderer(self, view):
        return JSONRenderer()

5,AdminRenderer

将数据渲染给 HTML 以进行类似管理界面的显示:
在这里插入图片描述

  • .media_type: text/html
  • .format: '.html'
  • .charset: utf-8
  • .template: 'rest_framework/api.html'

包含嵌套序列化或ListSerilizer的输入视图时,AdminRenderer将无法正常工作,因为HTML表单无法正确支持它们。

当数据中存在正确配置的URL_FIELD_NAME(缺省url)属性时,AdminRenderer才能够包含指向详细页面的链接,对于HyperlinkedModelSerializer就是这种情况。但是对于ModelSerializer 或者简单的Serializer类,则需要确保明确地包含该字段,比如使用模型get_absolute_url方法:

class AccountSerializer(serializers.ModelSerializer):
    url = serializers.CharField(source='get_absolute_url', read_only=True)

    class Meta:
        model = Account

6,HTMLFormRenderer

将序列化程序返回的数据渲染为HTML表单数据。

  • .media_type: text/html
  • .format: '.html'
  • .charset: utf-8
  • .template: rest_framework/horizontal/form.html'

此渲染器不是直接使用,而是可以通过将序列化器实例传递给render_form模板标记来替代模板。

{% load rest_framework %}

<form action="/submit-report/" method="post">
    {% csrf_token %}
    {% render_form serializer %}
    <input type="submit" value="Save" />
</form>

7,MultiPartRenderer

此渲染器用于渲染HTML multipart表单数据。

  • .media_type: multipart/form-data; boundary=BoUnDaRyStRiNg
  • .format: '.multipart'
  • .charset: utf-8

它不适合作为响应渲染器,而是用于创建测试请求,参考测试客户端和测试请求工厂

(四)自定义渲染器

要实现自定义渲染器,应该重写BaseRenderer类.
要配置.media_type.format等属性。

  • media_type:表示响应的媒体类型。
  • format:表示响应的格式,
  • charset:表示响应的编码方式。默认使用 UTF-8 编码。
  • render_style=‘binary’:确保可浏览的API不会尝试将二进制内容显示为字符串。

还要实现.render(self, data, media_type=None, renderer_context=None)方法。

  • data:请求数据,由 Response() 实例化设置。
  • media_type=None:可选的。如果提供,这是由内容协商阶段确定的所接受的媒体类型。根据客户端的 Accept: 头,这可能比渲染器的 media_type 属性更具体,可能包括媒体类型参数。例如 “application/json; nested=true”。
  • renderer_context=None:可选的。如果提供,这是一个由视图提供的上下文信息的字典。默认情况下这个字典会包括以下键:view, request, response, args, kwargs。

举个例子🌰:一个明文渲染器,它将使用参数作为响应 data 的内容返回响应。

from django.utils.encoding import smart_unicode
from rest_framework import renderers

class PlainTextRenderer(renderers.BaseRenderer):
    media_type = 'text/plain'
    format = 'txt'
    charset = 'iso-8859-1'

    def render(self, data, media_type=None, renderer_context=None):
        return data.encode(self.charset)

(五)高级渲染器使用

你可以使用REST framework的渲染器做一些非常灵活的事情。一些例子:

  • 根据请求的媒体类型,从同一个路径提供单独的或者嵌套的表示。
  • 提供来自相同路径的常规 HTML 网页和基于 JSON 的 API 响应。
  • 为 API 客户端指定要使用的多种类型的 HTML 表示形式。
  • 未指定渲染器的媒体类型,例如使用 media_type = 'image/*',可使用 Accept 头来更改响应的编码。

1,因媒体类型而异的行为

如果希望视图根据接受的媒体类型使用不同的序列化样式,则需要访问request.accepted_renderer以确定将用于响应的协商渲染器。举个例子🌰:

@api_view(['GET'])
@renderer_classes([TemplateHTMLRenderer, JSONRenderer])
def list_users(request):
    """
    A view that can return JSON or HTML representations
    of the users in the system.
    """
    queryset = Users.objects.filter(active=True)

    if request.accepted_renderer.format == 'html':
        # TemplateHTMLRenderer takes a context dict,
        # and additionally requires a 'template_name'.
        # It does not require serialization.
        data = {'users': queryset}
        return Response(data, template_name='list_users.html')

    # JSONRenderer requires serialized data as normal.
    serializer = UserSerializer(instance=queryset)
    data = serializer.data
    return Response(data)

2,未指定媒体类型

如果希望渲染器提供一些列媒体类型,则需要通过为 media_type 设置诸如 image/**/*这样的值来指定应该响应的媒体类型。

如果确定了渲染器的媒体类型,就应该确保在返回响应时使用 content_type 属性明确指定媒体类型。举个例子🌰:

return Response(data, content_type='image/png')

3,设计媒体类型

许多Web API的目标,简单的具有超链接的 JSON 响应可能就已经足够了。如果你想完全拥抱RESTful设计和HATEOAS则需要更详细地考虑媒体类型的设计和使用。

Roy Fielding的话来说,“REST API 应该花费所有的描述性努力来定义用于表示资源和驱动应用程序状态的媒体类型(们),或者为现有的标准媒体类型定义扩展关系名称和/或超文本启用标记。”。

有关自定义媒体类型的优秀示例,请参阅GitHub关于自定义 application/vnd.github+json 媒体类型的应用以及 Mike Amundsen的IANA认可的 application/vnd.collection+json JSON超媒体

四,版本控制

(一)什么是 API 版本控制

在心中默认一百遍:需求总是变化的

通常来说,API 应该指的是 API 服务,出于需求变化的原因,一个 API 服务可能提供多个 API 接口。在新接口不断出现的情况下,为了向后兼容仍然使用旧需求的客户端,就必然出现 API 接口的版本化。

Google API Design Guide 中文版
When and How Do You Version Your API?
RESTful API如何进行版本控制
怎么做 Web API 版本控制?

(二)版本号应该出现在哪里

实际上,关于API版本号应不应该出现在 URL 中是有争议的:Best practices for API versioning?

RESTful 原教旨主义者认为既然 URL 就是资源的代表,那么它就应该完全是关于资源的描述,而版本控制的相关信息,应该放到 HTTP 协议请求头的 Accept 选项中去,通过内容协商的方式处理版本号,比如:

# 内容协商:
Accept: application/vnd.example.v1+json
# 指定版本:
Accept: application/vnd.example+json;version=1.0
# 自定义请求头:
Accept-version: v2

但从实践的角度来说,非常多的企业、服务、平台都将版本号加入到了 URL 中,比如:

# 使用命名空间:
http://www.example.com/api/v3.0/customers/1234

# 使用关键字参数:
http://www.tianqiapi.com/api?version=v9&appid=23035354&appsecret=8YvlPNrz

简单来说的话,如果仅通过 URL 中的一部分字符分流出旧服务而让它得到继续的支持,那么将是比较划算的。

How are REST APIs versioned?
RESTful API 设计最佳实践
RESTful API版本控制策略
深入开放API版本策略
版本号规则
Restful服务应不应该在URI中加入版本号

(三)使用REST framework进行版本控制

简单来说,就是选择不同方案来根据不同版本提供不同的序列化样式。

1,配置版本控制方案

启用 API 版本控制后,request.version属性将包含与客户端传入的请求中请求的版本相对应的字符串。举个例子🌰:

	def get_serializer_class(self):
	    if self.request.version == 'v1':
	        return AccountSerializerVersion1
	    return AccountSerializer
  • 在类视图中根据版本不同而选择不同的序列化器。

全局版本控制方案由DEFAULT_VERSIONING_CLASS进行定义。举个例子🌰:

REST_FRAMEWORK = {
    'DEFAULT_VERSIONING_CLASS': 'rest_framework.versioning.NamespaceVersioning'
}
  • 默认情况下,版本控制是关闭的。DEFAULT_VERSIONING_CLASS值为
    None。在这种情况下,request.version属性将始终返回 None。

还可以在单​​个视图上设置版本控制方案,需要配置versioning_class属性。举个例子🌰:

class ProfileList(APIView):
    versioning_class = versioning.QueryParameterVersioning
  • 通常来说,在全局范围内使用单一版本控制方案更有意义。

还有一些更加细节的版本控制配置:

  • DEFAULT_VERSION:不存在版本控制信息时request.version应使用的值,默认为 None。
  • ALLOWED_VERSIONS.:限制为版本控制方案的子集,否则会引发错误。默认为None。DEFAULT_VERSION的值始终被视为ALLOWED_VERSIONS集合的一部分(除非它是None)。
  • VERSION_PARAM:应该用于任何版本控制参数的字符串,例如在媒体类型或 URL 查询参数中。默认为 ‘version’。
REST_FRAMEWORK = {
            ……
			'DEFAULT_VERSIONING_CLASS':'rest_framework.versioning.URLPathVersioning',
            'ALLOWED_VERSIONS':['v1','v2'], # 允许的版本
            'VERSION_PARAM':'version', # 参数
            'DEFAULT_VERSION':'v1', # 默认版本
            ....
      }

2,API参考

(1)AcceptHeaderVersioning

此方案要求客户端在请求头的 Accept 中将版本指定为媒体类型参数的一部分,通过内容协商的方法解析版本。举个例子🌰:

GET /bookings/ HTTP/1.1
Host: example.com
Accept: application/json; version=1.0
  • request.version属性将返回字符串 ‘1.0’。

严格地说 json 媒体类型并不包含其他参数。如果要构建明确指定的公共 API,则需要使用vendor media type,只需将渲染器配置为使用基于 JSON 的渲染器和自定义媒体类型。举个例子🌰:

class BookingsAPIRenderer(JSONRenderer):
    media_type = 'application/vnd.megacorp.bookings+json'

此时的请求现在会是这样的:

GET /bookings/ HTTP/1.1
Host: example.com
Accept: application/vnd.megacorp.bookings+json; version=1.0
(2)URLPathVersioning

此方案要求客户端将版本指定为URL路径的一部分。举个例子🌰:

GET /v1/bookings/ HTTP/1.1
Host: example.com
Accept: application/json

此时要求URL路由必须包含与带有 ‘version’ 关键字参数,以便版本控制方案可以使用版本信息。举个例子🌰:

  • request.version属性值是根据正则表达式匹配到的。
urlpatterns = [
    re_path(
        r'^(?P<version>(v1|v2))/bookings/$',
        bookings_list,
        name='bookings-list'
    ),
    re_path(
        r'^(?P<version>(v1|v2))/bookings/(?P<pk>[0-9]+)/$',
        bookings_detail,
        name='bookings-detail'
    )
]

举个例子🌰:

# urls.py
urlpatterns = [
    path('', include(('snippet.urls')),
]

# app urls.py
urlpatterns = [
    path('', views.api_root),	# 

    re_path('^snippets/(?P<version>(v1|v2))/(?P<pk>[0-9]+)/$', views.SnippetDetail.as_view(), name='snippet-detail'),
]


# views.py
@api_view(['GET'])
def api_root(request, format=None):
    return Response({
        'snippets': reverse('snippet-list', request=request, format=format)	
    })

class SnippetList(generics.GenericAPIView):
	...
    versioning_class = versioning.URLPathVersioning
    ...
(3)NamespaceVersioning

对于客户端,此方案与URLPathVersioning相同。举个例子🌰:

GET /v1/something/ HTTP/1.1
Host: example.com
Accept: application/json

唯一的区别是,前者使用 URL 路由中的命名空间而不是键字参数。举个例子🌰:

  • request.version属性值是根据 namespace 参数确定的。
# urls.py
urlpatterns = [
    re_path(r'^v1/bookings/', include('bookings.urls', namespace='v1')),
    re_path(r'^v2/bookings/', include('bookings.urls', namespace='v2'))
]

# app/urls.py
urlpatterns = [
    re_path(r'^$', bookings_list, name='bookings-list'),
    re_path(r'^(?P<pk>[0-9]+)/$', bookings_detail, name='bookings-detail')
]

如果你只需要一个简单的版本控制方案,那么URLPathVersioningNamespaceVersioning都是合适的。
URLPathVersioning 可能更适合小型项目,对于更大的项目来说NamespaceVersioning可能更容易管理。

从规范化 API 版本的角度来说,版本号后置更好一些:

# urls.py
urlpatterns = [
    re_path(r'^bookings/v1/', include('bookings.urls', namespace='v1')),
    path('', include(('snippet.urls', 'snippet'), namespace='v1')),
]

# app.urls.py
urlpatterns = [
    re_path('^users/(?P<pk>[0-9]+)/$', views.UserDetail.as_view(),name='user-detail'),
]

在实际开发中需要考虑许多问题:

  • 什么时候需要更新 API 版本?
  • 如何创建灵活的 API 版本?

因为对于公开的 API 来说,需求的多样性远大于不公开的 API,也就意味着前者一旦有某些客户端需要从 API 获取特定的内容,就不要更新 API,而且这种更新往往伴随着多个功能模块的变更,但也存在没有发生变更的功能模块,此时 /v1/something/ 这种格式就不太适合。同时为让 URL 尽量 RESTful,/something/v1/ 这种格式就更加适合。

回到教程👨‍💻。
出于对 API 以后的扩展性的考虑,我们决定为它添加版本控制功能。

出于压缩名称减少命名冲突的考虑,我们打算选择配置 NamespaceVersioning 作为全局的版本控制方案(通常来说,并不建议为单独的视图配置版本控制方案):

# settings.py
REST_FRAMEWORK = {
    'DEFAULT_VERSIONING_CLASS': 'rest_framework.versioning.NamespaceVersioning',
}

显然还要在路由中添加命名空间,出于灵活的版本控制考虑,我们将版本号放在每个功能名称的后面,并使用正则表达式来匹配:

# tutorial/urls.py
urlpatterns = [
    path('', include(('snippet.urls', 'snippet'), namespace='v1')),
    path('api-auth/', include('rest_framework.urls', namespace='rest_framework')),
]

# snippet/urls.py
urlpatterns = [
    path('', views.api_root),

    re_path(r'^snippets/v1/$', views.SnippetList.as_view(), name='snippet-list'),
    re_path(r'^snippets/v1/(?P<pk>[0-9]+)/$', views.SnippetDetail.as_view(), name='snippet-detail'),
    re_path(r'^v1/snippets/(?P<pk>[0-9]+)/highlight/$', views.SnippetHighlight.as_view(), name='snippet-highlight'),

    re_path(r'^users/v1/$', views.UserList.as_view(), name='user-list'),
    re_path(r'^users/v1/(?P<pk>[0-9]+)/$', views.UserDetail.as_view(), name='user-detail'),
]
(4)HostNameVersioning

次方案要求客户端将请求的版本指定为 URL 中主机名的一部分。举个例子🌰:

  • request.version属性值是根据对 ^([a-zA-Z0-9]+)\.[a-zA-Z0-9]+\.[a-zA-Z0-9]+$ 的匹配情况来确定的。
GET /bookings/ HTTP/1.1
Host: v1.example.com
Accept: application/json

因为可以为不同的 API 版本配置不同的 DNS 记录,所以如果需要根据版本将传入的请求路由到不同的服务器,则HostNameVersioning可能特别有用,

(5)QueryParameterVersioning

此方案是一种在 URL 中包含版本信息作为查询参数的简单方案。举个例子🌰:

GET /something/?version=0.1 HTTP/1.1
Host: example.com
Accept: application/json

此时需解析请求中的 version 参数以获取版本信息。

3,自定义版本控制方案

要实现自定义版本控制方案,请子类化BaseVersioning并重写.determine_version方法。举个例子🌰:使用自定义 X-API-Version 请求头来确定请求的版本。

class XAPIVersionScheme(versioning.BaseVersioning):
    def determine_version(self, request, *args, **kwargs):
        return request.META.get('HTTP_X_API_VERSION', None)

如果版本控制方案基于请求 URL,则还需要更改版本控制 URL 的确定方式,还应该重写.reverse()方法。有关示例,请参见源代码。

4,版本控制的一些细节

(1)反向解析 URL 版本化的 API

REST framework 包含的 reverse 函数与版本控制方案紧密关联。这个函数将应用任何适用于请求的 URL 的版本的转换。举个例子🌰:

  • 需要确保将当前 request 作为关键字参数传递进去,如下所示。
from rest_framework.reverse import reverse

reverse('bookings-list', request=request)
  • 如果使用NamespacedVersioning,并且 API 的版本是 ‘v1’,那么将会查找 ‘v1:bookings-list’,可能反向解析为类似 http://example.org/v1/bookings/ 这个URL。
  • 如果使用QueryParameterVersioning,并且 API 的版本是 1.0,那么可能反向解析为类似http://example.org/bookings/?version=1.0

回到教程

(2)版本化 API 和超链接序列化器

HyperlinkedModelSerializer与基于 URL 的版本控制方案一起使用时,请确保将请求本身作为上下文包含在序列化程序中。举个例子🌰:

  • 这样做将会在任何返回的URL中包含适当的版本控制。
	def get(self, request):
	    queryset = Booking.objects.all()
	    serializer = BookingsSerializer(queryset, many=True, context={'request': request})
	    return Response({'all_bookings': serializer.data})

(四)内容协商与版本控制

REST framework 作者认为,因为 HTTP 协议未明确说明服务器应该如何权衡两种主要协商机制中的头部选项,所以REST framework 就实现了这两种机制的混用方式。

如果严格按照 RESTful 的要求,就应该在 Accpet 中既指定媒体类型又指定版本号。

最终还是应该选择更合适的内容机制。

Why I Consistently Reach for Server-Driven Content Negotiation (For Versioning)

五,格式后缀

Web API 的一个常见模式是在 URL 上使用文件扩展名来为给定的媒体类型提供格式后缀。例如, “http://example.com/api/users.json” 提供 JSON 表示。

尽管我们通常使用内容协商机制来让客户端和浏览器“猜测”彼此需要什么类型,但 URL 中的格式后缀似乎更加直观。

而直接将格式后缀模式添加到 API 的 URLconf 中的每个单独条目是容易出错且非 DRY 的,因此 REST 框架提供了将这些模式添加到 URLConf 的快捷方式。

(一)一般使用方式

urlpatterns = format_suffix_patterns(urlpatterns, suffix_required=False, allowed=None) 获得一个包含格式后缀的 URL 模式列表。

其中:

  • urlpatterns:必需。URL 模式列表。
  • suffix_required:可选。一个布尔值,指示 URL 中的后缀是可选的还是强制的。默认为 False,这意味着默认情况下后缀是可选的。
  • allowed:可选。有效格式后缀的列表或元组。如果未提供,则使用通配符格式后缀模式。

举个例子🌰:

from rest_framework.urlpatterns import format_suffix_patterns
from blog import views

urlpatterns = [
    path('', views.apt_root),
    path('comments/', views.comment_list),
    path('comments/<int:pk>/', views.comment_detail)
]

urlpatterns = format_suffix_patterns(urlpatterns, allowed=['json', 'html'])

使用format_suffix_patterns时,必须确保将 format 关键字参数添加到相应的视图中:

@api_view(['GET', 'POST'])
def comment_list(request, format=None):
    # do stuff...

或者

class CommentList(APIView):
    def get(self, request, format=None):
        # do stuff...

    def post(self, request, format=None):
        # do stuff...

(二)通过查询参数指定格式后缀

一般格式后缀的替代方法是将请求的格式包含在请求的查询参数中。REST 框架默认提供此选项,并在可浏览 API 中用于在不同的可用表示之间的切换。

具体就是使用format查询参数。例如:http://example.com/organizations/?format=csv

可以使用URL_FORMAT_OVERRIDE配置项来修改此查询参数的名称。将值设置为 None 以禁用此行为。

(三)内容协商与格式后缀

一些 Web 社区似乎认为文件扩展名不是 RESTful 模式,而应始终使用 HTTP Accept

这实际上是一种误解。例如,引用 Roy Fielding 的以下引述,讨论查询参数媒体类型指示符与文件扩展名媒体类型指示符的相对优点:

“这就是为什么我总是喜欢扩展。这两种选择都与 REST 无关。” — Roy Fielding,REST 讨论邮件列表

引用没有提到 Accept ,但它明确表明格式后缀应被视为可接受的模式。

六,超链接序列化器HyperlinkedModelSerializer

我们在Django Rest Framework 1:序列化与反序列化重点介绍了两种序列化器:SerializerModelSerializer ,实际上,也经常使用HyperlinkedModelSerializer,它类似于ModelSerializer类,不同之处在于前者用一个 url 字段表示的超链接代替主键字段来表示关联关系。

对于这个 url 字段,一般来说应该使用HyperlinkedIdentityField字段来表示,而模型间的任何关联则应该使用HyperlinkedRelatedField字段来表示。

使用这个序列化器时,应该将这个 url 字段添加到 fields 选项中来显式地包含。举个例子🌰:

class AccountSerializer(serializers.HyperlinkedModelSerializer):
    class Meta:
        model = Account
        fields = ('url', 'id', 'account_name', 'users', 'created')

(一)绝对和相对URL

当实例化一个HyperlinkedModelSerializer时,你通常应该在序列化器的上下文中包含当前的request值。举个例子🌰:

serializer = AccountSerializer(queryset, context={'request': request})

只有这样做,才能确保超链接是一个绝对路径:

http://api.example.com/accounts/1/

如果确实要使用不常见的相对路径,则应该使用 None 代替传入当前的 request,得到:

/accounts/1/

(二)如何确定超链接视图

与其他序列化器不同的一点是,超链接序列化器定义好之后,下一步就是将它与正确的视图匹配起来,它这个视图有两点要求:

  1. 超链接化的外键字段应连接到路由中与 name 参数为 {basename}-detail 格式的URL匹配的视图。普通超链接化的字段直接连接到视图名。
  2. 能通过 pk 关键字参数查找模型实例。

一种方法时在类视图 Meta 类的 extra_kwargs 属性中设置view_namelookup_field中的一个或两个来重写 URL 字段的目标名称和待查询的字段。举个例子🌰:

class AccountSerializer(serializers.HyperlinkedModelSerializer):
    class Meta:
        model = Account
        fields = ('account_url', 'account_name', 'users', 'created')
        extra_kwargs = {
            'url': {'view_name': 'accounts', 'lookup_field': 'account_name'},	# url 是普通超链接字段,直接连接到名为accounts的视图
            'users': {'lookup_field': 'username'}	# users 是外键字段,默认通过名为 {model_name}-detail} 的路由连接到指定的视图。
        }

更好的方法是显式地设置序列化器上的字段的同名参数。举个例子🌰:

class AccountSerializer(serializers.HyperlinkedModelSerializer):
    url = serializers.HyperlinkedIdentityField(
        view_name='accounts',
        lookup_field='slug'
    )
    users = serializers.HyperlinkedRelatedField(
        view_name='user-detail',	# 显示地设置
        lookup_field='username',
        many=True,
        read_only=True
    )

    class Meta:
        model = Account
        fields = ('url', 'account_name', 'users', 'created')

具体可以通过打印HyperlinkedModelSerializer实例的repr来检查关联关系映射的那些视图名称和查询字段是否是你想要的。

(三)更改URL字段名称

URL字段的名称默认为’url’,可以通过使用URL_FIELD_NAME设置进行全局配置。

回到教程👨‍💻:
我们将要超链接化我们的API,其中包括一个普通字段和一个外键字段。

处理好实体之间的关系是 Web API 设计中相当又挑战性的方面。我们可以选择几种不同的方式来代表一种关系:

  • 使用主键。
  • 在实体之间使用超链接。
  • 在相关实体上使用唯一的标识字段。
  • 使用相关实体的默认字符串表示形式。
  • 将相关实体嵌套在父表示中。
  • 一些其他自定义表示。

REST框架支持所有这些方式,并且可以将它们应用于正向或反向关系,也可以在诸如通用外键之类的自定义管理器上应用。

既然我们希望在实体之间使用超链接的方式,那么就需要使用HyperlinkedModelSerializer序列化器来完成序列化工作。

第一个需要求改的序列化器就是 SnippetSerializer,因为我们要通过超链接的方式提供指向高亮代码的链接:

class SnippetSerializer(serializers.HyperlinkedModelSerializer):
    owner = serializers.ReadOnlyField(source='owner.username')
    highlight = serializers.HyperlinkedIdentityField(view_name='v1:snippet-highlight', format='html')

    class Meta:
        model = Snippet
        fields = ('url', 'id', 'highlight', 'owner',
                  'title', 'code', 'linenos', 'language', 'style')
  • 通过HyperlinkedIdentityField字段序列化高亮代码。
  • 通过 view_name 指定路由name,为了配合NamespaceVersioning实现版本控制,使用的是命名空间的格式。
  • 需要将链接字段加入 fields

还有一个要修改的是 UserSerializer。因为修改后的 SnippetSerializer 只有在Snippet列表或详情中才显示高亮超链接,为了简化跳转到Snippet列表或详情的这个动作,我们将与 Snippet 关联的用户 UserSerializer 也超链接化:

class UserSerializer(serializers.HyperlinkedModelSerializer):
    snippets = serializers.HyperlinkedRelatedField(many=True, view_name='v1:snippet-detail', read_only=True)

    class Meta:
        model = User
        fields = ('url', 'id', 'username', 'snippets')
  • 通过HyperlinkedIdentityField字段序列化snippets代码。
  • 同时通过 read_only 参数将关联字段设为只读。

超链接序列完成之后,就需要马上命名路由了,完成之后才能实现正确的视图匹配:

# snippet/urls.py
urlpatterns = [
    path('', views.api_root),
	...
    re_path(r'^snippets/v1/(?P<pk>[0-9]+)/$', views.SnippetDetail.as_view(), name='snippet-detail'),
    re_path(r'^snippets/v1/(?P<pk>[0-9]+)/highlight/$', views.SnippetHighlight.as_view(), name='snippet-highlight'),
    re_path(r'^users/v1/(?P<pk>[0-9]+)/$', views.UserDetail.as_view(), name='user-detail'),
]

但我们还有两个关联到视图 SnippetList 和 UserList 的路由没修改,为了实现路由的一致性,并提供完整的全链路版本控制,我们还要修改这两个路由:通过rest_framework.reverse.reverse函数实现反向解析 URL 版本化的 API,并用一个函数视图来将这个逻辑封装为整个 API 的入口。
先来封装入口逻辑:

# snippet/views.py
from rest_framework.decorators import api_view
from rest_framework.response import Response
from rest_framework.reverse import reverse


@api_view(['GET'])
def api_root(request, format=None):
    return Response({
        'users': reverse('user-list', request=request, format=format),
        'snippets': reverse('snippet-list', request=request, format=format)
    })

再来修改路由:

# snippet/urls.py
urlpatterns = [
    ...
    re_path(r'^snippets/v1/$', views.SnippetList.as_view(), name='snippet-list'),
    re_path(r'^users/v1/$', views.UserList.as_view(), name='user-list'),
]

至此,基本上已经完成 API 的超链接化。

七,分页

(一)django中的分页机制

django DOC: Pagination
django:分页

(二)REST framework 的分页概述

REST framework 可定制分页样式,这意味着这允许开发者将大型结果集拆分到单个数据页面中。

分页 API 可以支持:

  • 作为响应内容的一部分提供的分页链接。
  • 响应头中包含的分页链接(例如Content-RangeLink)。默认。

仅当使用通用类视图或视图集时,才会自动执行分页。如果使用的是常规APIView,则需要手动调用分页 API 以确保返回分页响应。

有关示例,请参见mixins.ListModelMixingenerics.GenericAPIView类的源码。

可以通过将分页类设置为 None 来关闭分页。

1,设置分页样式

通常需要同时使用 DEFAULT_PAGINATION_CLASSPAGE_SIZE 来设置全局分页样式。举个例子🌰:

REST_FRAMEWORK = {
    'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.LimitOffsetPagination',	# 指定分页类。默认为 None 
    'PAGE_SIZE': 100	# 页面大小。默认为 None 
}

也可以在类视图上设置使用 pagination_class 属性来指定分页类。

  • 通常,应该在整个 API 中使用相同的分页样式。

2,修改分页样式

如果要修改分页样式的特定方面,则需要覆盖其中一个分页类,并设置要修改的属性。举个例子🌰:

class LargeResultsSetPagination(PageNumberPagination):
    page_size = 1000
    page_size_query_param = 'page_size'
    max_page_size = 10000

class StandardResultsSetPagination(PageNumberPagination):
    page_size = 100
    page_size_query_param = 'page_size'
    max_page_size = 1000

(三)API 参考

1,PageNumberPagination

此分页样式接受请求查询参数中的单个数字页码:

GET https://api.example.org/accounts/?page=4

HTTP 200 OK
{
    "count": 1023
    "next": "https://api.example.org/accounts/?page=5",
    "previous": "https://api.example.org/accounts/?page=3",
    "results": []
}

PageNumberPagination 类包含许多属性,通过在继承它的自定义分页类中重写其中的一些属性可以修改分页样式:

  • django_paginator_class —— 要使用的分页类。默认为 django.core.paginator.Paginator
  • page_size —— 表示页面大小的整数值。如果设置,则覆盖 PAGE_SIZE 设置。默认为 PAGE_SIZE 的键值。
  • page_query_param —— 一个字符串值,指示用于分页控件的查询参数的名称。默认为 ‘page’
  • page_size_query_param —— 一个字符串值,指示允许客户端根据每个请求设置页面大小的查询参数的名称。默认值为 None。
  • max_page_size —— 一个整数值,指示允许的最大请求页面大小。此属性仅在设置了 page_size_query_param 时有效。
  • last_page_strings —— 一个字符串值的列表或元组,指示可与page_query_param 一起使用的值,以请求集合中的最后一页。默认为 ‘last’ 。
  • template —— 在可浏览 API 中呈现分页控件时要使用的模板名称。可以重写以修改呈现样式,或设置为 None 以完全禁用 HTML 分页控件。默认为 “rest_framework/pagination/numbers.html”。

回到教程👨‍💻。
我们只需要使用内置的这种简单的分页方式就行。

# settins.py
...
REST_FRAMEWORK = {
    ...
    'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination',
    'PAGE_SIZE': 2
}

看一下效果:
在这里插入图片描述

2,LimitOffsetPagination

此分页样式映射在数据库查找多个记录时使用的分页查询语法。客户端包括 “limit” 和 “offset” 查询参数:

  • limit 指示要返回的项目的最大数目,这相当于其他样式中的 page_size。如果全局配置中指定了 PAGE_SIZE 键值,那么 limit 参数将是可选的。
  • offset指示查询相对于完整的未分页项的起始位置。
GET https://api.example.org/accounts/?limit=100&offset=400

HTTP 200 OK
{
    "count": 1023
    "next": "https://api.example.org/accounts/?limit=100&offset=500",
    "previous": "https://api.example.org/accounts/?limit=100&offset=300",
    "results": []
}

LimitOffsetPagination 类包含许多属性,通过在继承它的自定义分页类中重写其中的一些属性可以修改分页样式:

  • default_limit —— 一个整数值,指示在客户端未在查询参数中提供的代替 limit 数值的值。默认为与 PAGE_SIZE 设置键相同的值。
  • limit_query_param —— 一个字符串值,指示 limit 查询参数的名称。默认为 ‘limit’。
  • offset_query_param ——一个字符串值,指示 offset 查询参数的名称。默认为 ‘offset’。
  • max_limit —— 一个整数值,指示客户端可能请求的最大允许的 limit 数值的值。默认为 None。
  • template —— 在可浏览 API 中呈现分页控件时要使用的模板名称。可以重写以修改呈现样式,或设置为 None 以完全禁用 HTML 分页控件。默认为 “rest_framework/pagination/numbers.html”。

MySQL分页查询方法及优化

3,CursorPagination

CursorPagination 分页类的实现与使用要求比较复杂,在实际中也不常用,这里就忽略,具体可参考官方文档:CursorPagination

(四)自定义分页样式

为了创建自定义分页序列化功能,可以将 pagination.BasePagination 子类化,并重写 paginate_queryset(self, queryset, request, view=None)get_paginated_response(self, data) 方法:

  • paginate_queryset 方法被传递给初始的queryset,它应该返回一个iterable对象,该对象只包含请求页中的数据。
  • get_paginated_response 方法传递序列化的页数据,并应返回一个 Response

具体可以参考内置的几个分页类的实现过程。

(五)HTML分页控件

默认情况下,使用分页类时,会在可浏览的 API 页面中显示 HTML分页控件。

有两种内置的显示样式:

  • PageNumberPaginationLimitOffsetPagination 类显示带有上一页和下一页控件的页码列表。
  • CursorPagination 类显示的样式更简单,仅显示上一页和下一页控件。
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值