CBV高级类视图

在Django中,视图负责处理HTTP请求并返回响应。Django提供了两种实现视图的方式,函数视图(Function-Based Views,FBV)和类视图(Class-Based Views,CBV)。函数视图前面我们已经详细介绍过了,相较于函数视图,类视图通过面向对象编程(OOP)的方式来组织视图,能够起到提高代码可复用性的目的。

当然,类视图也有很多缺点,它最大的问题就是过度抽象和高度耦合,类视图代码可读性相对较低,使用类视图心智负担远高于函数视图,能灵活使用类视图的前提是你必须对各种内置类视图的方法和Mixin了如指掌;此外,类视图也没有函数视图灵活,一些特殊的场景我们依然必须使用最基础的函数视图才能实现。实际开发时尤其是团队合作的开发模式中,使用CBV通常弊大于利,写出一系列抽象的代码之前你一定要知道自己在干什么,否则不要埋这个坑!

这篇笔记我们对类视图的使用进行简单介绍。

创建类视图

我们可以通过继承View类来创建自定义的类视图,该基类包含了一些最基础的功能并提供了和路由系统整合的相关逻辑。下面例子演示了如何创建一个自定义类视图。

from django.shortcuts import render
from django.views.generic import View

from demo01.forms import StudentForm


class StudentFormView(View):
    def get(self, request):
        form = StudentForm()
        return render(request, 'index.html', {'form': form})

    def post(self, request):
        form = StudentForm(request.POST)
        if form.is_valid():
            print(form.cleaned_data.get('stu_code'))
            print(form.cleaned_data.get('name'))
            print(form.cleaned_data.get('age'))
            print(form.cleaned_data.get('class_room').room_code)
            return render(request, 'success.html')
        else:
            return render(request, 'index.html', {'form': form})

上面代码和以下函数视图等价。

from django.shortcuts import render

from demo01.forms import StudentForm


def index(request):
    if request.method == 'POST':
        form = StudentForm(request.POST)
        if form.is_valid():
            print(form.cleaned_data.get('stu_code'))
            print(form.cleaned_data.get('name'))
            print(form.cleaned_data.get('age'))
            print(form.cleaned_data.get('class_room').room_code)
            return render(request, 'success.html')
        else:
            return render(request, 'index.html', {
                'form': form
            })
    else:
        form = StudentForm()
        return render(request, 'index.html', {
            'form': form
        })

可以看到类视图中,GET、POST等不同请求类型被封装成了不同的方法,Django框架会通过判断请求类型来调用对应的方法,而函数视图中则通过request.method来获取请求类型,然后通过if判断的方式执行不同的逻辑。View类中有一个dispatch()方法,这些判断逻辑其实就被封装在该方法中,它会根据请求类型来调用对应的方法,因此我们也可以通过重写dispatch()方法来实现自定义的逻辑。

类视图配置到路由需要调用as_view()方法,这个方法由View基类提供,用于将类视图转换为函数视图,以便路由系统可以调用它。

from django.urls import path
from demo01.views import StudentFormView

urlpatterns = [
    path('index/', StudentFormView.as_view())

setup()和dispatch()

View基类中有两个方法setup()dispatch(),这两个方法在类视图的执行过程中起到了至关重要的作用。

setup():首先我们要知道,Django的视图类是多实例的,每次有请求到达视图时,Django会为每个请求创建一个新的视图类实例。setup()方法初始化视图类时调用,它可以用于在视图类初始化阶段插入一些额外逻辑。

dispatch():该方法对应请求真正执行的阶段,dispatch()方法根据请求类型调用对应的处理方法,比如GET请求调用get()方法,POST请求调用post()方法,我们可以在请求执行前和执行后插入额外逻辑。

下面例子代码中,我们在视图类初始化时打印了一些信息,在视图逻辑执行后打印执行时间。

import time

from django.views.generic import TemplateView


class IndexView(TemplateView):
    template_name = 'index.html'

    def setup(self, request, *args, **kwargs):
        print('IndexView setup!')
        super().setup(request, *args, **kwargs)

    def dispatch(self, request, *args, **kwargs):
        print('Begin processing!')
        start_time = time.time() * 1000
        response = super().dispatch(request, *args, **kwargs)
        end_time = time.time() * 1000
        print(f'Processing time: {end_time - start_time} ms!')
        return response

    def get_context_data(self, **kwargs):
        super().get_context_data(**kwargs)
        return {'msg': 'Hello, world!'}

封装类视图

前面我们创建的类视图实际上可以进一步封装,将表单对象和模板抽离出来,作为基础类供其它功能复用。

import time

from django.shortcuts import render
from django.views.generic import View

from demo01.forms import StudentForm


class MyFormView(View):
    form_class = None
    template_name = None

    def setup(self, request, *args, **kwargs):
        print('IndexView setup!')
        super().setup(request, *args, **kwargs)

    def dispatch(self, request, *args, **kwargs):
        print('Begin processing!')
        start_time = time.time() * 1000
        response = super().dispatch(request, *args, **kwargs)
        end_time = time.time() * 1000
        print(f'Processing time: {end_time - start_time} ms!')
        return response

    def get(self, request):
        form = self.form_class()
        return render(request, 'index.html', {'form': form})

    def post(self, request):
        form = self.form_class(request.POST)
        if form.is_valid():
            self.handle_form_submission(form)
            return render(request, 'success.html')
        else:
            return render(request, 'index.html', {'form': form})

    def handle_form_submission(self, form):
        pass


class StudentFormView(MyFormView):
    form_class = StudentForm
    template_name = 'index.html'

    def handle_form_submission(self, form):
        print(form.cleaned_data.get('stu_code'))
        print(form.cleaned_data.get('name'))
        print(form.cleaned_data.get('age'))
        print(form.cleaned_data.get('class_room').room_code)

代码中,我们定义了基类MyFormView,它支持通过form_classtemplate_name属性来指定表单类和模板,handle_form_submission()方法用于处理表单提交后的逻辑,此外还对视图类的初始化阶段和执行阶段插入了一些自定义逻辑。子类StudentFormView中我们覆盖了类变量form_classtemplate_name,并重写handle_form_submission()方法来实现自己的业务逻辑。

实际上Django有许多类似的内置视图,像上面的表单视图Django其实已经内置了类似的FormView类,灵活使用内置视图能够极大提高我们的开发效率。

使用内置视图

Django提供了一些内置的基本视图类,不过和我们自定义的视图类似,所有内置视图类也都最终继承View基类,TemplateView就是这样一种内置子类视图,它用于渲染模板,我们这里就以TemplateView为例进行介绍。

TemplateView最简单的用法我们甚至不需要扩展它,直接在配置路由时指定template_name参数实例化即可。

from django.urls import path
from django.views.generic import TemplateView

urlpatterns = [
    path('index/', TemplateView.as_view(template_name='index.html'))
]

如果我们要添加额外功能而不是简单显示模板页面,则需要继承TemplateView定义自己的类视图,实际开发中通常都是这样编写代码。下面代码我们重写了TemplateView类的get_context_data()方法为视图添加上下文数据,在路由配置中仍使用as_view()方法进行配置。

from django.views.generic import TemplateView


class IndexView(TemplateView):
    template_name = 'index.html'

    def get_context_data(self, **kwargs):
        return {
            'msg': 'Hello, world!'
        }

可以看到,其实使用这些内置类视图和我们自定义的类视图基类是类似的,因此只要掌握了类视图的基本用法,使用这些内置类视图就非常简单了。下面表格列举了Django中常用的内置类视图。

类视图名称 说明 主要方法 示例
ListView 显示模型的对象列表 get_queryset 用于显示数据库中的一个模型的所有对象。
DetailView 显示某个特定对象的详细信息 get_object 用于显示数据库中某个特定模型对象的详情。
CreateView 用于处理创建对象的表单并保存新对象 form_valid 用于创建新的数据库对象,并显示一个表单。
UpdateView 用于处理更新现有对象的表单并保存更改 form_valid 用于更新现有数据库对象,并显示一个表单。
DeleteView 用于删除现有对象并跳转到删除后显示的页面 get_object 用于删除特定的对象并跳转到一个页面,通常是列表页面。
TemplateView 渲染指定的模板,并可以传递上下文数据 get_context_data 用于渲染指定的模板。
RedirectView 执行URL重定向 get_redirect_url 用于执行简单的重定向,常用于URL重定向。
ArchiveIndexView 用于展示某个模型的归档视图,通常按日期归档 get_queryset 用于显示某个模型对象的归档列表(按年、月、日分组)。
YearArchiveView 展示某个模型的特定年份归档视图 get_queryset 用于按年份展示某个模型的归档数据。
MonthArchiveView 展示某个模型的特定月份归档视图 get_queryset 用于按月份展示某个模型的归档数据。
DayArchiveView 展示某个模型的特定日期归档视图 get_queryset 用于按日期展示某个模型的归档数据。
DateDetailView 展示某个模型的某个具体日期的对象详细信息 get_object 用于展示特定日期的模型对象详情。
FormView 用于渲染和处理表单的视图 form_valid 用于呈现和处理表单提交。

关于这些内置类视图我们不可能逐一列举介绍,使用时参考文档即可。

自定义Mixin类

实际上,Django的类视图还是基于多继承的Mixin模式设计的,这种设计进一步提高了代码的可复用性。前面我们使用过的TemplateView中,get_context_data()其实就来自内置的ContextMixin类。实际开发中,我们可能经常需要在多个类视图中复用功能,Django提供了很多内置的Mixin类,对于常用功能我们也可以封装成自己的Mixin类,通过继承这些Mixin类就能实现代码逻辑的复用。

所谓的Mixin类其实没什么特别的,它就是一个普通的类,只不过它的方法通常都是用来扩展其他类视图的功能的。下面我们通过一个简单的例子来演示如何自定义Mixin类。

from django.views.generic import TemplateView


class MyMixin:
    def gen_hello_msg(self):
        return {
            'msg': 'Hello, world!'
        }


class IndexView(TemplateView, MyMixin):
    template_name = 'index.html'

    def get_context_data(self, **kwargs):
        return self.gen_hello_msg()

代码中,我们定义了一个MyMixin类,其中gen_hello_msg()方法返回一个字典,用于生成上下文数据。IndexView类视图中,同时继承TemplateViewMyMixin,它在get_context_data()方法中调用MyMixin中的gen_hello_msg()方法生成上下文数据。上述代码达到了将部分逻辑抽离的目的。

内置Mixin类

使用内置Mixin非常简单,只要继承它即可,下面例子我们使用内置的LoginRequiredMixin实现登录检查(该Mixin是auth模块提供)。

from django.views.generic import TemplateView
from django.contrib.auth.mixins import LoginRequiredMixin


class IndexView(LoginRequiredMixin, TemplateView):
    template_name = 'index.html'

代码中,我们的IndexView类同时继承了LoginRequiredMixinTemplateView,这样IndexView就具备了登录检查的功能,只有登录的用户才能访问该视图。这里要注意多继承中的继承顺序,LoginRequiredMixin底层是通过覆盖dispatch()实现的,因此它必须放在前面,否则不会生效,多继承顺序问题也是使用CBV的坑之一。

下面表格列举了一些内置的Mixin类,这些Mixin类可以用于扩展类视图的功能。

Mixin名称 描述 适用场景
LoginRequiredMixin 确保用户已登录。继承此Mixin后,只有登录的用户才能访问视图,否则会重定向到登录页面。 需要限制访问权限的视图,例如用户中心或管理后台视图。
PermissionRequiredMixin 确保用户拥有特定权限。继承此Mixin后,用户必须具备指定权限才能访问该视图。 需要权限控制的视图,例如只有管理员或特定角色用户可以访问的页面。
UserPassesTestMixin 允许根据自定义条件来限制访问。继承此Mixin后,可以定义test_func方法来指定访问条件。 需要通过自定义条件控制访问的视图,例如根据用户属性或其他逻辑进行访问控制。
FormMixin 自动将表单处理逻辑与视图结合。继承此Mixin后,可以在视图中处理表单验证和提交。 用于需要处理表单提交的视图,通常与 POST 请求一起使用。
ModelFormMixin 用于处理与模型相关的表单。继承此Mixin后,可以自动生成基于模型的表单,并处理提交逻辑。 处理模型数据的表单,例如创建和编辑对象的表单。
MultipleObjectMixin 处理多个对象的查询和显示,通常用于显示列表。 显示模型对象列表或分页的视图。
SingleObjectMixin 处理单个对象的查询,通常用于显示单个模型实例的视图。 显示单个模型对象的详细信息,通常用于查看视图。
RedirectViewMixin 简化重定向操作。继承此Mixin后,可以很方便地设置视图重定向到指定的 URL。 在视图中需要进行重定向时,例如将请求重定向到另一个页面。
TemplateResponseMixin 自动返回渲染的模板响应。继承此Mixin后,自动将模板渲染为响应对象。 需要返回模板渲染的视图,例如展示 HTML 页面的视图。
ContextMixin 为模板提供额外的上下文数据。继承此Mixin后,可以向模板传递额外的上下文变量。 在视图中需要向模板传递一些额外的数据,例如显示网站通知或动态标题等。
BaseDetailViewMixin 用于基于视图提供详细信息的通用逻辑。通常与SingleObjectMixin配合使用。 显示模型对象的详细信息,可以处理显示和操作单个对象的请求。
作者:Gacfox
版权声明:本网站为非盈利性质,文章如非特殊说明均为原创,版权遵循知识共享协议CC BY-NC-ND 4.0进行授权,转载必须署名,禁止用于商业目的或演绎修改后转载。