Fork me on GitHub

第 3 篇:实现博客首页文章列表 API

作者:HelloGitHub-追梦人物

文中所涉及的示例代码,已同步更新到 HelloGitHub-Team 仓库

此前在讨论基于模板引擎的开发方式和 django-rest-framework 开发的异同时说过,django-rest-framework 开发和传统的开发方式没有什么不同,区别仅在于返回的数据格式不同而已。

在基于模板引擎的开发方式中,博客首页文章列表的视图函数可能是这样的:

from django.shortcuts import render
from .models import Post

def index(request):
    post_list = Post.objects.all().order_by('-created_time')
    return render(request, 'blog/index.html', context={'post_list': post_list})

在 django-rest-framework,代码逻辑是一样的,只是在最后返回结果时,返回资源序列化后的结果。

from rest_framework.decorators import api_view
from rest_framework.response import Response
from rest_framework import status

from .models import Post
from .serializers import PostListSerializer

@api_view(http_method_names=["GET"])
def index(request):
    post_list = Post.objects.all().order_by('-created_time')
    serializer = PostListSerializer(post_list, many=True)
    return Response(serializer.data, status=status.200)

暂且忽略掉资源序列化器 PostListSerializer,我们接下来会实现它,先把注意力放在主体逻辑上。

首先,我们从 rest_framework.decorators 中导入了 api_view 装饰器,并用它装饰了 index 视图函数,使其成为一个 RESTful API 视图函数。

为什么需要这个视图函数装饰器呢?之前说过,django-rest-framework 为 API 的开发提供了丰富的功能,包括内容协商、认证和鉴权、限流等等。这些过程 django 默认的视图函数在处理 HTTP 请求时是没有提供的,而经过 api_view 装饰后的视图,则提供了上述全部功能。

不过我们这里并没有看到任何内容协商、认证和鉴权、限流代码逻辑和配置,这是为什么呢?原因隐藏在 Python 的装饰器魔法里,django-rest-framework 对于上述功能有一套默认的处理逻辑,因此我们不需要进行任何配置,仅需使用 api_view 装饰一个 django 视图函数,所有功能全部自动开启。

视图函数里我们先从数据库获取文章列表资源,然后使用序列化器对其进行序列化,序列化后的数据存在 data 属性里,我们把它传递给 HTTP 响应类 Response,并将这个响应返回。

注意这个 Response 是从 rest_framework.response 中导入的,它类似于 django 的 HTTPResponse 响应类。实际上,这个类是 django-rest-framework 对 django 的模板响应类(SimpleTemplateResponse)的拓展(具体的细节可以不用了解,只要知道 django 使用它来渲染模板并构造 HTTP 响应即可),通常在 RESTful API 的视图函数中我们都会返回这个类,而不是 django 的 HTTP 响应类。此外,通过传入 status 参数,指定 HTTP 响应的状态码。

小贴士

请了解常用的 HTTP 状态码。在 RESTful 架构中,客户端通过 HTTP 请求动词表征对资源的操作意图,而服务端则使用 HTTP 状态码表示资源操作的结果。常用状态码及其含义如下:

200:通常表示请求成功。

201:表示资源创建成功。

400:表示客户端请求错误。

401:没有提供身份认证信息

403:没有操作权限

404 :访问的资源不存在

405:不支持的 HTTP 请求方法

500:服务器内部错误

HTTP 请求和响应过程,django-rest-framework 已经帮我们处理。但是资源的序列化,框架是无法自动化完成的,框架提供了基本的序列化器,我们需要自定义序列化逻辑。所以,让我们来定义 PostListSerializer 序列化器,用它来序列化文章列表。

序列化器由一系列的序列化字段(Field)组成,序列化字段的作用是,在序列化资源时,将 Python 数据类型转为原始数据类型(通常为字符类型或者二进制类型),以便在客户端和服务端之间传递;反序列化时,将原始数据类型转为 Python 数据类型。在转换过程中,还会进行数据合法性的校验。

先来看一个简单的例子(摘自 django-rest-framework 官网示例),理解序列化器的工作原理和功能。假设我们有一个 Python 类 Comment

from datetime import datetime

class Comment(object):
    def __init__(self, email, content, created=None):
        self.email = email
        self.content = content
        self.created = created or datetime.now()

comment = Comment(email='leila@example.com', content='foo bar')

根据 Comment 类 3 个属性的类型,定义一个序列化器,用于数据序列化和反序列化。我们在上一步教程的 交流的桥梁:评论功能 中介绍过表单(Form)的定义。实际上,django-rest-framework 序列化器的设计参考了 django 表单的设计。序列化器和表单也有很多相似功能,比如对输入数据进行校验等。序列化器的代码如下:

from rest_framework import serializers

class CommentSerializer(serializers.Serializer):
    email = serializers.EmailField()
    content = serializers.CharField(max_length=200)
    created = serializers.DateTimeField()

自定义的序列化器都要继承 serializers.Serializer 基类,基类提供了数据序列化和反序列化的逻辑。根据被序列化对象的属性的数据类型,需要指定相应的序列化字段(Serializer Field)。django-rest-framework 提供了很多常用的序列化字段,例如本例中用于序列化 email 数据格式的 EmailField,用于序列化字符型数据格式的 CharField,用于序列化日期格式的 DateTimeField。在实际项目中,应该根据数据类型,选择合适的序列化字段。全部序列化字段,可以参考官方文档 Serializer fields

有了序列化器,就可以将 Comment 对象序列化了,序列化器用法如下:

>>> serializer = CommentSerializer(comment)
>>> serializer.data
# 输出:
{'email': 'leila@example.com', 'content': 'foo bar', 'created': '2016-01-27T15:17:10.375877'}

首先将需要序列化的对象(comment)传入序列化器(CommentSerializer),构造一个序列化器对象(serializer),访问序列化器对象的 data 属性,就可以得到序列化后的数据。

被序列化对象序列化后的数据是一个扁平的 Python 字典,字典中的数据描述了这个对象资源。有了序列化生成的 Python 字典,我们就可以将字典数据进一步格式化为 JSON 字符串或者 XML 文档字符串,在客户端和服务端之间传输。试想,客户端服务端通常都通过 HTTP 协议传输数据,传输的数据只能是字符串或者二进制数据,不可能将一个 Python 的对象直接传递,这就是为什么要序列化的原因。一端接收到序列化的数据后,如果有需要,可以对数据进行反序列化,重新恢复为 Python 对象。

以上就是一个标准序列化器的定义。其关键点在于,根据被序列化对象属性的数据类型,选择合适的序列化字段。回顾我们在上一步教程的 交流的桥梁:评论功能 中对评论表单的定义,我们通过继承 ModelForm 定义了表单,而并没有显示地指定表单字段的类型。原因在于,对于 django 中的模型(Model),已经有了定义其数据类型的模型字段,因此 django 表单可以根据关联的模型,自动推测需要使用的表单字段,在背后帮我们完成表单字段的选择,简化了表单的定义。

和表单类似,django-rest-framework 的序列化器也可以根据关联的模型,自动检测被序列化模型各个属性的数据类型,推测需要使用的序列化字段,无需我们显示定义。此时,自定义的序列化器不再继承标准的 Serializer,而是继承其子类,ModelSerializer

我们来编写文章(Post)模型的序列化器代码。按照习惯,序列化器的代码位于相应应用的 serializers.py 模块中,因此在 blog 应用下新建一个 serializers.py 文件,写上如下代码:

from rest_framework import serializers
from .models import Post

class PostListSerializer(serializers.ModelSerializer):
    category = CategorySerializer()
    author = UserSerializer()
    
    class Meta:
        model = Post
        fields = [
            'id',
          	'title',
          	'created_time',
          	'excerpt',
          	'category',
          	'author',
          	'views',
        ]

使用 ModelSerializer 时,只需要在序列化器的内部类 Meta 中指定关联的模型,以及需要序列化的模型属性,django-rest-framework 就会根据各个属性的数据类型,自动推测需要使用的系列化字段,从而生成标准的序列化器。事实上,我们可以来看一下 django-rest-framework 最终生成的序列化器长什么样子:

class PostListSerializer():
    id = IntegerField(label='ID', read_only=True)
    title = CharField(label='标题', max_length=70)
    created_time = DateTimeField(label='创建时间', required=False)
    excerpt = CharField(allow_blank=True, label='摘要', max_length=200, required=False)
    category = CategorySerializer()
    author = UserSerializer()

还需要注意一点,titlecreated_timeviews 这些属性都是原始的数据类型(字符型、日期型、整数类型)。而对于文章关联的 categoryauthor,它们本身也是一个对象,django-rest-framework 就无法推测该使用什么类型的系列化字段来序列化它们了。所以这里我们按照标准序列化器的定义方式,将这两个属性的系列化字段分别定义为 CategorySerializerUserSerializer,意思是告诉 django-rest-framework,请使用 CategorySerializerUserSerializer 来序列化关联的 categoryauthor。实际上,序列化器本身也是一个序列化字段。当然,CategorySerializerUserSerializer 目前还不存在,我们来定义他们:

from django.contrib.auth.models import User
from .models import Category, Post

class CategorySerializer(serializers.ModelSerializer):
    class Meta:
        model = Category
        fields = [
            'id',
          	'name',
        ]
        
class UserSerializer(serializers.ModelSerializer):
    class Meta:
        model = User
        fields = [
            'id',
          	'username',
        ]
        
class PostListSerializer(serializers.ModelSerializer):
	# ...

再来回顾一下我们的 API 视图函数代码:

@api_view(http_method_names=["GET"])
def index(request):
    post_list = Post.objects.all().order_by('-created_time')
    serializer = PostListSerializer(post_list, many=True)
    return Response(serializer.data, status=status.HTTP_200_OK)

注意这里 PostListSerializer 的用法,构造序列化器时可以传入单个对象,序列化器会将其序列化为一个字典;也可以传入包含多个对象的可迭代类型(这里的 post_list 是一个 django 的 QuerySet),此时需要设置 many 参数为 True 序列化器会依次序列化每一项,返回一个列表。

api_view 装饰器传入 http_method_names 参数指定允许访问该 API 视图的 HTTP 方法。

现在我们已经有了视图函数,最后,我们需要给这个视图函数绑定 URL,在 blog 应用下的 urls.py 中加入绑定的代码:

path('api/index/', views.index)

启动开发服务器,打开浏览器访问 http://127.0.0.1:8000/api/index/ ,可以看到接口返回了文章列表 JSON 格式的数据(默认为 JSON)。

目前来说,这个接口其实作用不大。不过在后续的教程中,我们学习前端框架 Vue,那个时候,RESTful API 就有了它的用武之地了。

回顾一下 index API 视图函数的基本逻辑:

  1. 从数据库取数据
  2. 构造序列化器并将取出的数据序列化
  3. 返回响应

这其实是访问序列型的资源比较常见的逻辑,我们知道,django 专门为这种在 Web 开发中常用的逻辑提供了一系列基于类的通用视图,以提高代码的复用性和减少代码量。只是 django 的通用视图适用于基于模板引擎的开发方式,同样的,django-rest-framework 也提供了专门针对 RESTful API 开发过程中常用逻辑的类视图通用函数。接下来,让我们使用 django-rest-framework 提供的通用类视图,将首页 API 的视图函数改为类视图。


关注公众号加入交流群

posted @ 2020-05-01 08:44  削微寒  阅读(911)  评论(0编辑  收藏  举报