Django 的类视图

前面第9节中我们简单介绍了 Django FBV 和 CBV,分别表示以函数形式定义的视图和以类形式定义的视图。函数视图便于理解,但是如果一个视图函数对应的 URL 路径支持多种不同的 HTTP 请求方式时,如 GET, POST, PUT 等,需要在一个函数中写不同的业务逻辑,这样导致写出的函数视图可读性不好。此外,函数视图的复用性不高,大量使用函数视图,导致的一个结果就是大量重复逻辑和代码,严重影响项目质量。而 Django 提供的 CBV 正是要解决这个问题而出现的,这也是官方强烈推荐使用的方式。

1. Django 类视图使用介绍

1.1 CBV 的基本使用

前面我们已经介绍了 CBV 的基本使用方法,其基本流程如下:

定义视图类 (TestView)

该类继承视图基类 View,然后实现对应 HTTP 请求的方法。Django 在 View 类的基础上又封装了许多视图类,如专门返回模板的 TemplateView 视图类、用于显示列表数据的 ListView 视图类等等。这些封装的是图能够进一步减少大家的重复代码,后面我会详细介绍这些封装的视图类的使用以及其源码实现。

# 代码路径 hello_app/views.py
# ...

class TestView(View):
    def get(self, request, *args, **kwargs):
        return HttpResponse('hello, get\n')

    def post(self, request, *args, **kwargs):
        return HttpResponse('hello, post\n')

    def put(self, request, *args, **kwargs):
        return HttpResponse('hello, put\n')

    def delete(self, request, *args, **kwargs):
        return HttpResponse('hello, delete\n')

    @csrf_exempt
    def dispatch(self, request, *args, **kwargs):
        return super(TestView, self).dispatch(request, *args, **kwargs)

配置 URLConf,如下

# 代码路径 hello_app/urls.py
# ...

urlpatterns = [
    path('test-cbv/', views.TestView.as_view(), name="test-cbv")
]

注意:不是直接写视图类,而是要调用视图类的 as_view() 方法,这个 as_view() 方法返回的也是一个函数。

启动 Django 工程,测试

# 启动django服务
(django-manual) [root@server first_django_app]# python manage.py runserver 0.0.0.0:8888
Watching for file changes with StatReloader
Performing system checks...

System check identified no issues (0 silenced).
April 15, 2020 - 07:08:32
Django version 2.2.11, using settings 'first_django_app.settings'
Starting development server at http://0.0.0.0:8888/
Quit the server with CONTROL-C

# 打开另一个xshell窗口,发送如下请求
[root@server ~]# curl -XGET http://127.0.0.1:8888/hello/test-cbv/
hello, get
[root@server ~]# curl -XPOST http://127.0.0.1:8888/hello/test-cbv/
hello, post
[root@server ~]# curl -XPUT http://127.0.0.1:8888/hello/test-cbv/
hello, put
[root@server ~]# curl -XDELETE http://127.0.0.1:8888/hello/test-cbv/
hello, delete

1.2 Django 中使用 Mixin

首先需要了解一下 Mixin 的概念,这里有一篇介绍 Python 中 Mixin 的文章:<<多重继承>> ,可以认真看下,加深对 Mixin 的理解。在我的理解中,Mixin 其实就是单独的一块功能类。假设 Django 中提供了 A、B、C 三个视图类,又有 X、Y、Z三个 Mixin 类。如果我们想要视图 A,同时需要额外的 X、Y功能,那么使用 Python 中的多重继承即可达到目的:

class NewView(A, X, Y):
    """
    定义新的视图
    """
    pass

我们来看看 Django 的官方文档是如何引出 Mixin 的:

Django’s built-in class-based views provide a lot of functionality, but some of it you may want to use separately. For instance, you may want to write a view that renders a template to make the HTTP response, but you can’t use TemplateView;perhaps you need to render a template only on POST, with GET doing something else entirely. While you could use TemplateResponse directly, this will likely result in duplicate code.

For this reason, Django also provides a number of mixins that provide more discrete functionality. Template rendering, for instance, is encapsulated in the TemplateResponseMixin.

翻译过来就是: Django 内置的类视图提供了许多功能,但是我们可能只需要其中的一部分功能。例如我想写一个视图,该视图使用由模板文件渲染后的 HTML 来响应客户端的 HTTP 请求,但是我们又不能使用 TemplateView 来实现,因为我只想在 POST 请求上使用这个模板渲染的功能,而在 GET 请求时做其他事情。当然,可以直接使用 TemplateResponse 来完成,这样就会导致代码重复。基于这个原因, Django 内部提供了许多离散功能的 mixins。

可以看到,这里的 mixins 就是一些单独功能的类,配合视图类一起使用,用于组合出各种功能的视图。接下来,我们结合前面的 Member 表来使用下 mixin 功能。具体的步骤如下:

改造原来的视图类-TestView。我们给原来的视图类多继承一个 mixin,用于实现单个对象查找查找功能;

from django.shortcuts import render
from django.http import HttpResponse
from django.views.decorators.csrf import csrf_exempt
from django.views.generic import View
from django.views.generic.detail import SingleObjectMixin

from .models import Member

# Create your views here.
class TestView(SingleObjectMixin, View):
    model = Member

    def get(self, request, *args, **kwargs):
        return HttpResponse('hello, get\n')

    def post(self, request, *args, **kwargs):
        self.object = self.get_object()
        return HttpResponse('hello, {}\n'.format(self.object.name))

    def put(self, request, *args, **kwargs):
        return HttpResponse('hello, put\n')

    def delete(self, request, *args, **kwargs):
        return HttpResponse('hello, delete\n')

    @csrf_exempt
    def dispatch(self, request, *args, **kwargs):
        return super(TestView, self).dispatch(request, *args, **kwargs)

修改 URLConf 配置,传递一个动态参数,用于查找表中记录:

urlpatterns = [
    path('test-cbv/<int:pk>/', views.TestView.as_view(), name="test-cbv")
]

启动服务器,然后进行测试:

[root@server first_django_app]# curl -XPOST http://127.0.0.1:8888/hello/test-cbv/2/
hello, 会员2
[root@server first_django_app]# curl -XPOST http://127.0.0.1:8888/hello/test-cbv/4/
hello, spyinx-0
[root@server first_django_app]# curl -XPOST http://127.0.0.1:8888/hello/test-cbv/9/
hello, spyinx-5
[root@server first_django_app]# curl -XGET http://127.0.0.1:8888/hello/test-cbv/9/
hello, get
[root@server first_django_app]# curl -XPUT http://127.0.0.1:8888/hello/test-cbv/9/
hello, put
[root@server first_django_app]# curl -XDELETE http://127.0.0.1:8888/hello/test-cbv/9/
hello, delete

可以看到在 POST 请求中,我们通过传递主键值,就能返回 Member 表中对应记录中的 name 字段值,这一功能正是由SingleObjectMixin 中的 get_object() 方法提供的。通过继承这个查询功能,我们就不用再使用 ORM 模型进行查找了,这简化了我们的代码。当然,这只能满足一小部分的场景,对于更多复杂的场景,我们还是需要实现自己的逻辑,我们也可以把复杂的功能拆成各种 mixin,然后相关组合继承,这样可以很好的复用代码,这是一种良好的编码方式。

2. 深入理解 Django 类视图

这里在介绍完类视图的基本使用后,我们来深入学习下 Django 的源代码,看看 Django 是如何将对应的 HTTP 请求映射到对应的函数上。这里我们使用的是 Django 2.2.10 的源代码进行说明。我们使用 VSCode 打开 Django 源码,定位到 django/views/generic 目录下,这里是和视图相关的源代码。

图片描述

首先看 __init__.py 文件,内容非常少,主要是将该目录下的常用视图类导入到这里,简化开发者导入这些常用的类。其中最重要的当属 base.py 文件中定义的 view 类,它是其他所有视图类的基类。

# base.py中常用的三个view类
from django.views.generic.base import RedirectView, TemplateView, View

# dates.py中定义了许多和时间相关的视图类
from django.views.generic.dates import (
    ArchiveIndexView, DateDetailView, DayArchiveView, MonthArchiveView,
    TodayArchiveView, WeekArchiveView, YearArchiveView,
)
# 导入DetailView类
from django.views.generic.detail import DetailView
# 导入增删改相关的视图类
from django.views.generic.edit import (
    CreateView, DeleteView, FormView, UpdateView,
)
# 导入list.py中定义的显示列表的视图类
from django.views.generic.list import ListView

__all__ = [
    'View', 'TemplateView', 'RedirectView', 'ArchiveIndexView',
    'YearArchiveView', 'MonthArchiveView', 'WeekArchiveView', 'DayArchiveView',
    'TodayArchiveView', 'DateDetailView', 'DetailView', 'FormView',
    'CreateView', 'UpdateView', 'DeleteView', 'ListView', 'GenericViewError',
]

# 定义一个通用的视图异常类
class GenericViewError(Exception):
    """A problem in a generic view."""
    pass

接下来,我们查看 base.py 文件,重点分析模块中定义的 View 类:

# 源码路径 django/views/generic/base.py

# 忽略导入
# ...

class View:

    http_method_names = ['get', 'post', 'put', 'patch', 'delete', 'head', 'options', 'trace']
    
    def __init__(self, **kwargs):
        # 忽略
        # ...
            
    @classonlymethod
    def as_view(cls, **initkwargs):
        """Main entry point for a request-response process."""
        for key in initkwargs:
            if key in cls.http_method_names:
                raise TypeError("You tried to pass in the %s method name as a "
                                "keyword argument to %s(). Don't do that."
                                % (key, cls.__name__))
            if not hasattr(cls, key):
                raise TypeError("%s() received an invalid keyword %r. as_view "
                                "only accepts arguments that are already "
                                "attributes of the class." % (cls.__name__, key))

        def view(request, *args, **kwargs):
            self = cls(**initkwargs)
            if hasattr(self, 'get') and not hasattr(self, 'head'):
                self.head = self.get
            self.setup(request, *args, **kwargs)
            if not hasattr(self, 'request'):
                raise AttributeError(
                    "%s instance has no 'request' attribute. Did you override "
                    "setup() and forget to call super()?" % cls.__name__
                )
            return self.dispatch(request, *args, **kwargs)
        view.view_class = cls
        view.view_initkwargs = initkwargs

        # take name and docstring from class
        update_wrapper(view, cls, updated=())

        # and possible attributes set by decorators
        # like csrf_exempt from dispatch
        update_wrapper(view, cls.dispatch, assigned=())
        return view

    # ...

    def dispatch(self, request, *args, **kwargs):
        # Try to dispatch to the right method; if a method doesn't exist,
        # defer to the error handler. Also defer to the error handler if the
        # request method isn't on the approved list.
        if request.method.lower() in self.http_method_names:
            handler = getattr(self, request.method.lower(), self.http_method_not_allowed)
        else:
            handler = self.http_method_not_allowed
        return handler(request, *args, **kwargs)

    def http_method_not_allowed(self, request, *args, **kwargs):
        logger.warning(
            'Method Not Allowed (%s): %s', request.method, request.path,
            extra={'status_code': 405, 'request': request}
        )
        return HttpResponseNotAllowed(self._allowed_methods())
    
    # 忽略其他函数
    # ...

# ...

我们来仔细分析 view 类中的这部分代码。view 类首先定义了一个属性 http_method_names,表示其支持的 HTTP 请求方法。接下来最重要的是 as_view() 方法和 dispatch() 方法。在上面使用视图类的示例中,我们定义的 URLConf 如下:

# first_django_app/hello_app/urls.py

from . import views

urlpatterns = [
    # 类视图
    url(r'test-cbv/', views.TestView.as_view(), name='test-cbv'),
]

这里结合源码可以看到,views.TestView.as_view() 返回的结果同样是一个函数:view(),它的定义和前面的视图函数一样。as_view() 函数可以接收一些参数,函数调用会先对接收的参数进行检查:

for key in initkwargs:
    if key in cls.http_method_names:
        raise TypeError("You tried to pass in the %s method name as a "
                        "keyword argument to %s(). Don't do that."
                        % (key, cls.__name__))
    if not hasattr(cls, key):
        raise TypeError("%s() received an invalid keyword %r. as_view "
                         "only accepts arguments that are already "
                         "attributes of the class." % (cls.__name__, key))

上面的代码会对 as_view() 函数传递的参数做两方面检查:

首先确保传入的参数不能有 get、post 这样的 key 值,否则会覆盖 view 类中的对应方法,这样对应的请求就无法正确找到函数进行处理。覆盖的代码逻辑如下:

class View:
    # ...
    def __init__(self, **kwargs):
        # 这里会将所有的传入的参数通过setattr()方法给属性类赋值
        for key, value in kwargs.items():
            setattr(self, key, value)
    # ...
    @classonlymethod
    def as_view(cls, **initkwargs):
        # ...

        def view(request, *args, **kwargs):
            # 调用视图函数时,会将这些参数传给View类来实例化
            self = cls(**initkwargs)
            # ...
       
        # ...
    # ...

此外,不可以传递类中不存在的属性值。假设我们将上面的 URLConf 进行略微修改,如下:

from . import views

urlpatterns = [
    # 类视图
    url(r'test-cbv/', views.TestView.as_view(no_key='hello'), name='test-cbv'),
]

启动后,可以发现 Django 报错如下,这正是由本处代码抛出的异常。

图片描述

接下来看下