最近开始学习django REST framework,以下简称drf

前后端分离,是在实际工作中是常用的,于是在慕课网购买了一个教程,388心痛

UTOOLS1589274318565.png

哈哈开玩笑,只要学到了知识388算什么

写本文的目的不在于唠嗑日常,而是我想总结下学习笔记《drf的Apiview、GenericView、Viewset和router的原理分析》

数据序列化

通过前面Django基础的学习,我们知道通过views视图函数,将数据传入到templates模板层,进行模板渲染

再将渲染好的数据发送给用户,大概就是下面这个过程

UTOOLS1589274757015.png

上方这个在实际开发的时候,具有高耦合性

之前,在练习个人博客项目的时候,就需要经常结合前端页面进行调试,离开前端我们就不能直观的看到数据

这样如此,后端开发人员就要掌握部分前端知识(实际上也是如此),不过这样反复调试实在恼火

而且以后想要更换网站模板,又只能反复调试,不断适配?

于是,就有前后端分离,后端就只做提供数据的工作,前端就做使用数据的工作

UTOOLS1589274988691.png

前端请求接口,拿到对应数据,再进行渲染

编写views.py

import json
from django.views import View
from django.http import JsonResponse
from goods.models import Goods


class GoodsListView(View):

    def get(self, request):
        """通过django的view实现商品页"""
        json_list = []
        goods = Goods.objects.all()[:10]

        # 方法一:
        # for good in goods:
        #     json_dict = {}
        #     json_dict['name'] = good.name
        #     json_dict['category'] = good.category.name
        #     json_list.append(json_dict)

        # 方法二:
        # from django.forms.models import model_to_dict
        # for good in goods:
        #     json_dict = model_to_dict(good)
        #     json_list.append(json_dict)

        # 方法三:
        from django.core import serializers
        json_data = serializers.serialize('json', goods)
        json_data = json.loads(json_data)
        return JsonResponse(json_data, safe=False)

通过上方代码演示,就可以通过视图函数获取其数据了

1589275571368

这里使用到了类视图,推荐下文,打开有点慢

https://www.zmrenwu.com/courses/hellodjango-blog-tutorial/materials/79/

既然如此,Django原本就能够实现了json数据的发送,为什么我们还要学习drf呢?

作为初学者,我理解得也不是特别透彻,学了几课,感觉就是drf功能集成更好,实现更为方便

原生Django,post数据只支持urlencode,不支持application/jsonform-data

drf这三种均是可以支持的,话说这么多,我也表述不清楚,接下往下看

安装drf及其依赖,这里就跳过...

第一版(首次使用序列化器)

views同目录下创建GoodsSerializer.py序列化器`

from rest_framework import serializers


class GoodsSerializer(serializers.Serializer):
    # 序列化的字段
    name = serializers.CharField(required=True, max_length=100)
    click_num = serializers.IntegerField(default=0)
    goods_front_image = serializers.ImageField()
    # ...
    # 模型类中还有很多字段,做测试用,只写了三个

序列化器的编写,是不是和form表单的编写极其相似呢?

下面是编写表单的示例

UTOOLS1589276437164.png

编写views.py

from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import status
from .models import Goods
from .serializers import GoodsSerializer

# 第一版
class GoodsListView(APIView):
    """这是一段描述"""
    def get(self, request, format=None):
        goods = Goods.objects.all()[:10]
        goods_serializer = GoodsSerializer(goods, many=True)
        return Response(goods_serializer.data, status=status.HTTP_200_OK)

views中使用,也和表单类似哦!

from .serializers import GoodsSerializer导入序列化器类

goods_serializer = GoodsSerializer(goods, many=True)创建序列化器对象,传入参数goods(Goods模型实例),有多个数据就需要many=True

return Response(goods_serializer.data, status=status.HTTP_200_OK)返回json数据及状态码

第二版(Model序列化类)

了解了序列化器的基本使用,现在来改进这个序列化器

一个模型可能拥有很多个字段,全部我们手来写,实在麻烦

所以drf提供了和form中类似forms.ModelForm的功能——serializers.ModelSerializer

就像Django提供Form类和ModelForm类一样,REST框架同时包含Serializer类和ModelSerializer类。

官方详解:

https://www.django-rest-framework.org/tutorial/1-serialization/#using-modelserializers

改写serializer.py

from rest_framework import serializers
from goods.models import Goods


class GoodsSerializer(serializers.ModelSerializer):
    category = CategorySerializer()
    class Meta:
        model = Goods
        # fields = ['category', 'goods_sn', ...]
        fields = '__all__'

fields用于自定义需要展示的字段

fields = '__all__'即展示模型类中所有的字段

第三版(显示外键内容)

UTOOLS1589277530454.png

在本例获取到的json数据中 category是分类表中的主键id,这里如果不想展示主键id,而是展示分类的详细内容,那么可以这样写

改写serializer.py

from rest_framework import serializers
from goods.models import Goods, GoodsCategory


class CategorySerializer(serializers.ModelSerializer):
    """商品分类序列化器"""
    class Meta:
        model = GoodsCategory
        fields = '__all__'


class GoodsSerializer(serializers.ModelSerializer):
    """商品序列化器"""
    category = CategorySerializer()
    class Meta:
        model = Goods
        # fields = ['category', 'goods_sn', ...]
        fields = '__all__'

先定义好我们的商品分类序列化器

然后在商品序列化器里面,去创建这个分类序列化器的对象就可以了

category = CategorySerializer()

category这个变量名是要和,商品表字段所对应哦,不能改成其他的

效果如下:

UTOOLS1589277818948.png

这样就展示了,当前商品所属分类的详细信息

视图函数之路

先来看下刚开始我们的写法

class GoodsListView(APIView):
    """这是一端描述"""
    def get(self, request, format=None):
        goods = Goods.objects.all()[:10]
        goods_serializer = GoodsSerializer(goods, many=True)
        return Response(goods_serializer.data)

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

继承关系:继承至rest_framework.views.APIView

通用视图

好的,现在来改进一下

# 第二版
class GoodsListView(mixins.ListModelMixin, generics.GenericAPIView):
    """商品列表"""
    # 指定查询集
    queryset = Goods.objects.all()[:10]
    # 指定序列化器 类名
    serializer_class = GoodsSerializer

    def get(self, request, *arg, **kwargs):
        return self.list(request, *arg, **kwargs)

继承关系:继承至mixins.ListModelMixin, generics.GenericAPIView

先了解下继承这两个东西是什么,有什么作用?

mixins

mixins.py说明

Basic building blocks for generic class based views.

We don't bind behaviour to http method handlers yet,

which allows mixin classes to be composed in interesting ways.

基于通用类的视图的基本构建块。

我们还没有将行为绑定到http方法处理程序,

这允许以有趣的方式组合mixin类。

它有下面这几个类

UTOOLS1589278660634.png

上方例子中,所继承的ListModelMixin (列出一个model实例的查询集)

其他的暂时没有学到,应该可以大概猜出是什么功能,这里也不做详细介绍了(我还没学到,嘿嘿)

ListModelMixin

class ListModelMixin:
    """
    List a queryset.
    """
    def list(self, request, *args, **kwargs):
        # 获取查询集数据
        queryset = self.filter_queryset(self.get_queryset())
        # 分页
        page = self.paginate_queryset(queryset)
        if page is not None:
            serializer = self.get_serializer(page, many=True)
            return self.get_paginated_response(serializer.data)
        # 序列化
        serializer = self.get_serializer(queryset, many=True)
        # 返回数据
        return Response(serializer.data)

对比来看看

class GoodsListView(APIView):
    """这是一端描述"""
    def get(self, request, format=None):
        # 获取查询集数据
        goods = Goods.objects.all()[:10]
        # 序列化
        goods_serializer = GoodsSerializer(goods, many=True)
        # 返回数据
        return Response(goods_serializer.data)

怎么样,是不是执行步骤一模一样,所以上方代码,完全可以使用ListModelMixin.list取代

def get(self, request, *arg, **kwargs):
        return self.list(request, *arg, **kwargs)

现在应该明白了继承ListModelMixin的作用了吧

generics

再来理一理generics.GenericAPIView

generics.py

Generic views that provide commonly needed behaviour.

提供常见行为的通用视图。

这个和Django自身的通用视图有异曲同工之妙

上方例子所继承的GenericAPIView是这个drf通用视图的基类,其他的显示视图都需要继承它

还有很多其他的通用视图,暂时不做详细介绍,用到了再做学习笔记

现在主要理清楚在本例中GenericAPIView的作用

`GenericAPIView类中就有我们设置的两个属性

queryset = None
serializer_class = None

还有其对应的两个方法

get_queryset()
get_serializer_class()

ListModelMixin.list方法中,也用到了这两个方法,获取查询集结果获取序列化对象

queryset = self.filter_queryset(self.get_queryset())
serializer = self.get_serializer(queryset, many=True)

好的,最后再来梳理一下

执行ListModelMixin.list方法,这个方法中需要querysetserializer ,所以在执行到的时候,又要去执行GenericAPIView中与之对应的方法获得查询集序列化器对象,最后返回数据

列表视图(继续精简)

先上代码

class GoodsListView(generics.ListAPIView):
    """商品列表"""
    queryset = Goods.objects.all()
    serializer_class = GoodsSerializer

继承关系:继承至generics.ListAPIView

UTOOLS1589295833379.gif

get方法都没了,就这两句,就能实现与上面一样的功能?

我们看一下generics.ListAPIView,只要看到generics就要想着一句话

提供常见行为的通用视图。

这里举例的获取数据列表,嘿,就是属于常见行为,所以继承generics.ListAPIView

查看generics.ListAPIView

class ListAPIView(mixins.ListModelMixin, GenericAPIView):
    """
    Concrete view for listing a queryset.
    """
    def get(self, request, *args, **kwargs):
        return self.list(request, *args, **kwargs)

ListAPIView所继承的不就是刚才我们所写的吗

mixins.ListModelMixin, GenericAPIView

ListAPIView类帮我们实现了get方法,所以也就不必再写了

视图集(再次精简)

What Fuck? 我觉得上方已经很精简了,还能再简?

UTOOLS1589295147241.gif

引入视图集(ViewSets)

默认情况下,基本ViewSet类不提供任何操作。

先上代码

class GoodsListViewSet(mixins.ListModelMixin, viewsets.GenericViewSet):
    """商品列表"""
    queryset = Goods.objects.all()
    serializer_class = GoodsSerializer

继承关系:继承至mixins.ListModelMixin, viewsets.GenericViewSet

GenericViewSet 这个就是视图集,来查看下其源码

class GenericViewSet(ViewSetMixin, generics.GenericAPIView):
    """
    The GenericViewSet class does not provide any actions by default,
    but does include the base set of generic view behavior, such as
    the `get_object` and `get_queryset` methods.
    """
    pass

继承关系: 继承至ViewSetMixin, generics.GenericAPIView

查看ViewSetMixin, 代码有点长,就不贴出来了,可以自己去看下,我这里就描述下其功能

重写 ".as_view()",以便它采用一个"actions"关键字,该关键字将 HTTP 方法绑定到资源上的操作。

例如,要创建将"GET"和"POST"方法绑定到"list"和"create"操作的具体视图...

view = MyViewSet.as_view({'get': 'list', 'post': 'create'})

看完是不是云里雾里的,不要慌,慢慢来,继续看

总之,现在我们知道class GoodsListViewSet(mixins.ListModelMixin, viewsets.GenericViewSet): 视图类,通过继承这两个类,可以实现其具体功能,不管怎么变化了,还是继承至

mixins.ListModelMixingenerics.GenericAPIView

也是我们在刚开始实现的功能(上方 通用视图里有介绍)

好了,功能是没有任何问题,那么这个视图集到底有啥用呢?

我的个人理解如下: (理解不是特别清楚)

一个请求链接,可能有很多请求方式,例如get put post 等等,每一个请求方式后面所对应有不同的处理方法

例如:

get请求,如果是获得列表数据,则是mixins.ListModelMixin.list方法,列出数据的查询集

put请求,如果是更新指定数据,则是mixins.UpdateModelMixin.update方法,更新一个模型实例对象

如果一个接口有很多请求方式,那么就会继承很多对应的mixins类,写起来颇为繁琐,不美观

于是出现了视图集,理解为一个集合,将所有我们所需的操作方法,集中起来

集中起来,又该如何使用呢?

编写urls.py

# 导入视图集GoodsListViewSet
from goods.views import GoodsListViewSet

# 通过视图类得到视图函数
goods_list = GoodsListViewSet.as_view({
    'get': 'list',
    'post': 'create',
})

urlpatterns = [
    # ...
    path('goods/', goods_list, name='goods-list'),
]

那么,通过get请求goods/,则会返回数据,通过post请求goods/,则会新建一个数据

看到这里,你可能想说,这也没啥特别的嘛,就是规定了哪个请求方式,执行什么方法而已嘛

好的,接着往下看

# 导入视图集GoodsListViewSet
from goods.views import GoodsListViewSet

# 通过视图类得到视图函数
goods_list = GoodsListViewSet.as_view({
    'get': 'list',
    'post': 'create',
})
goods_detail = GoodsListViewSet.as_view({
    'get': 'retrieve',
    'delete': 'destroy',
})

urlpatterns = [
    # ...
    path('goods/', goods_list, name='goods-list'),
    path('goods/<int:pk>/', goods_detail, name='goods-detail')
]

我们用同一个视图集,实现两个功能不同的视图函数goods_listgoods_detail都具有get请求

一个是获取商品列表,另一个是获取商品详情,就是因为get所对应的处理方法不同。

想一想,如果不这样写,那么就要做商品列表一个视图类,商品详情一个视图类,精简程度远没有现在这么好

ViewSetsRouter一般是绑定着用

示例:

from django.urls import path, include
# 导入视图集GoodsListViewSet
from goods.views import GoodsListViewSet
# 导入路由器
from rest_framework.routers import DefaultRouter

# 新建路由器对象
router = DefaultRouter()
# 将视图集注册到路由器对象
router.register('goods', GoodsListViewSet)
router.register('goods/<int:pk>', GoodsListViewSet)


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

这样写起来,路由就更为精炼了,而我们发现和上面的方法比起来,我们没有定义请求方式与处理方法的对应关系

UTOOLS1589300037645.gif

这里使用了路由器规则默认的配置DefaultRouter,关于路由器更多介绍

https://www.django-rest-framework.org/api-guide/routers/

数据分页

在上方查看ListModelMixin.list方法中,我们发现除了获取查询集和获取序列化器对象之外,还有一段分页的代码块:

page = self.paginate_queryset(queryset)
if page is not None:
    serializer = self.get_serializer(page, many=True)
    return self.get_paginated_response(serializer.data)

下面是我以查看源码的心态,啰嗦的,如果是直接使用的话,建议看文档最快

分页官方文档:

https://www.django-rest-framework.org/api-guide/pagination/

解读代码:

page = self.paginate_queryset(queryset)根据上下文,猜测是获取以页码分类的查询集

ListModelMixin没有这个paginate_queryset所以,只能是在generic.GenericAPIView

查看paginate_queryset

def paginate_queryset(self, queryset):
    if self.paginator is None:
        return None
    return self.paginator.paginate_queryset(queryset, self.request, view=self)

Return a single page of results, or None if pagination is disabled.

返回单页结果,如果禁用分形,则返回"无"。

用到了 self.paginator,就在这个函数的上面,继续查看

查看self.paginator

    @property
    def paginator(self):
        if not hasattr(self, '_paginator'):
            if self.pagination_class is None:
                self._paginator = None
            else:
                self._paginator = self.pagination_class()
        return self._paginator

The paginator instance associated with the view, or None.

与视图关联的 paginator 实例,或"无"。

查看self.pagination_class

# The style to use for queryset pagination.
pagination_class = api_settings.DEFAULT_PAGINATION_CLASS

至此在drf模块下的settings.py找到了配置DEFAULT_PAGINATION_CLASS

'DEFAULT_PAGINATION_CLASS': None, # 分页样式
'PAGE_SIZE': None, # 分页数(一页有多少数据)

配置settings.py

REST_FRAMEWORK = {
    'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.LimitOffsetPagination',
    'PAGE_SIZE': 10,
}

配置好后,再次请求接口,就可以看到每一页是10个数据了,并且有分页按钮

UTOOLS1589282069072.png

请求带上参数,就可以获取指定页面的数据了,例如http://127.0.0.1/goods/?page=10获取第10页的数据

UTOOLS1589282161908.png

数据分页(自定义)

官方一共有三种模式:

  • LimitOffsetPagination
  • PageNumberPagination
  • CursorPagination

参考:https://www.django-rest-framework.org/api-guide/pagination/#pagenumberpagination

也可以自定也分页类

要创建自定义的分页序列化程序类,您应该继承pagination.BasePagination并覆盖paginate_queryset(self, queryset, request, view=None)and get_paginated_response(self, data)方法:

  • paginate_queryset方法将传递给初始查询集,并且应返回仅包含所请求页面中数据的可迭代对象。
  • get_paginated_response方法将传递序列化的页面数据,并且应返回一个Response实例。

注意,该paginate_queryset方法可以在分页实例上设置状态,以后可以由get_paginated_response方法使用

我这里继承的是PageNumberPagination,本质上依然是继承至pagination.BasePagination

class StandardResultsSetPagination(PageNumberPagination):
    page_size = 10
    # 设置参数关键词
    # 请求一页的数据数量
    page_size_query_param = 'page_size'
    # 请求第几页的参数
    page_query_param = 'p'
    # 设置请求一页最大数据量
    max_page_size = 100

接着下在视图类中使用这个分页类就可以了

class GoodsListViewSet(viewsets.ReadOnlyModelViewSet):
    """商品列表"""
    queryset = Goods.objects.all()
    serializer_class = GoodsSerializer
    # 指定分页类
    pagination_class = StandardResultsSetPagination

小结

好了,最后再对以上内容进行一个小结

serializers: 将数据序列化,操作方法和form类似

mixins: 操作数据并返回数据,例如list, create, delete 等等

generic: 包含了在视图中常见操作的套路,例如获取查询集,获取序列化器,获取分页器

推荐文章

[django rest framework apiview、viewset总结分析]https://juejin.im/post/5a66d262f265da3e317e4cc5#heading-4

Last modification:May 13th, 2020 at 08:48 am