Django 的类视图

背景

练习Django的时候发现它的类视图很神奇,很少的代码就能实现功能,研究了一下,发现这个教程,写得比较详细,转载一下,加深印象。

类视图与函数视图

Django的视图可以分为:

  • 函数视图FBV:def index(request):
  • 类视图CBV:class AboutView(TemplateView):

早期,人们在视图开发中发现了一些常见的习语和句式,也就是重复性代码和工作。于是引入了基于函数的视图来抽象这些模式,便于一般情况下的视图开发。

基于函数的视图的问题在于,尽管它们覆盖了简单的情况,但是除了一些简单的配置选项之外,没有办法扩展或定制它们,限制了它们在许多现实应用中的实用性。

基于类的通用视图与基于函数的视图的目标相同,都是想让视图开发更容易。但由于类视图可以使用MIXIN等一些面向对象的方法和工具包,使得基于类的视图比基于函数的视图更具扩展性和灵活性。

基于类的视图:

  • 通过HTTP请求方法的不同,将代码分隔在不同的类方法中,比如GET和POST,而不是类函数中的条件判断。
  • 可以使用面向对象的技巧,比如混入。
  • 类具有封装和继承的特性,方便代码复用、分发和重构。

两种视图可以实现同样的功能,本质上是一个东西,没有谁好谁坏之分,只是适用场景不同而已:

  • 简单逻辑、快速处理,请用FBV
  • 代码复用、功能封装,请用CBV

用法

Django 提供了很多适用于各种应用场景的基本视图类,我们一般不从头自己写起,这些类视图都继承django.views.generic.base.View类。比如RedirectView 用于 HTTP 重定向,TemplateView 用于渲染模板。

类视图有很多简单的用法,甚至不需要去views.py中编写代码,比如下面的例子:

1
2
3
4
5
6
from django.urls import path
from django.views.generic import TemplateView

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

重点关注:

  • TemplateView类视图
  • as_view()方法
  • template_name参数

更通用的使用方法是继承Django提供的各种视图类,所以上面的例子的一般性写法如下:

1
2
3
4
5
# some_app/views.py
from django.views.generic import TemplateView

class AboutView(TemplateView):
template_name = "about.html"

但是,由于类不是函数,所以需要在URLconf中使用as_view()这个类方法将一个基于类的视图转换成函数形式的接口。

1
2
3
4
5
6
7
# urls.py
from django.urls import path
from some_app.views import AboutView

urlpatterns = [
path('about/', AboutView.as_view()),
]

上面的AboutView视图不涉及模型的访问,比较简单。让我们看一个书籍列表视图的例子:
首先是路由:

1
2
3
4
5
6
from django.urls import path
from books.views import BookListView

urlpatterns = [
path('books/', BookListView.as_view()),
]

BookListView视图:

1
2
3
4
5
6
7
8
9
10
11
12
13
from django.http import HttpResponse
from django.views.generic import ListView
from books.models import Book

class BookListView(ListView):
model = Book # 指定模型

def head(self, request, *args, **kwargs):
last_book = self.get_queryset().latest('publication_date')
response = HttpResponse()
# RFC 1123 date format
response['Last-Modified'] = last_book.publication_date.strftime('%a, %d %b %Y %H:%M:%S GMT')
return response

这个例子中,如果用户通过GET请求数据,那么将正常的返回响应数据(此处省略)。而如果通过HEAD请求,将使用我们写的head方法中的业务逻辑。

一、使用基于类的视图

本质上来说,基于类的视图允许你使用不同的实例方法响应不同的HTTP 请求,而不是在单个视图函数里使用if/else代码。
在函数视图里处理 GET请求的代码像下面这样:

1
2
3
4
5
6
from django.http import HttpResponse

def my_view(request):
if request.method == 'GET':
# <view logic>
return HttpResponse('result')

而在类视图中,则通过不同过的实例方法来处理:

1
2
3
4
5
6
7
from django.http import HttpResponse
from django.views import View

class MyView(View):
def get(self, request):
# <view logic>
return HttpResponse('result')

上面的继承关系非常重要,不能从头写一个新类,或者随便自己瞎继承一个类,否则你无法和Django系统勾连。
每个类视图都有一个as_view()方法,用于在urlconf中进行dispatch。这个方法会创建一个类视图的实例,并调用它的dispatch()方法。dispatch方法会在类中查找类似GET\POST之类的类方法,然后与请求request中的HTTP方法匹配。匹配上了就调用对应的代码,匹配不上就弹出异常HttpResponseNotAllowed,也就是不允许当前的请求类型。

1
2
3
4
5
6
7
# urls.py
from django.urls import path
from myapp.views import MyView

urlpatterns = [
path('about/', MyView.as_view()), # 注意as_view要加括号进行调用
]

至于return返回的什么,和函数视图是一样样的。

as_view()方法可以传递参数,例如:

urlpatterns = [ path('view/', MyView.as_view(size=42)), ]

传递给视图的参数会在视图的每个实例之间共享。这意味着你不应使用列表、字典或任何其他可变对象作为视图的参数。如果你这样做并且共享对象被修改,则会导致一个请求对另外一个请求产生影响,这显然存在极大的风险,完全不可接受。

每个MyView视图的实例都可以使用 self.size接收传入的参数值42,但该参数必须已经在类中定义了。

基于类的视图不强制你添加任何类属性,当需要的时候,有两种方法来配置或设置类属性。

第一种是继承父类,在子类中重写父类的属性,如下所示:

父类:

1
2
3
4
5
6
7
8
from django.http import HttpResponse
from django.views import View

class GreetingView(View):
greeting = "Good Day"

def get(self, request):
return HttpResponse(self.greeting)

子类:

1
2
class MorningGreetingView(GreetingView):
greeting = "Morning to ya"

注意其中的greeting类属性。
另一种就是简单粗暴的在URLconf路由条目中修改as_view()方法的参数。当然,参数名必须是存在的类属性,你不能随便瞎写!如下所示:

1
2
3
urlpatterns = [
path('about/', GreetingView.as_view(greeting="G'day")),
]

但是这种方式有很大的弊端,那就是虽然每次匹配上了url,你的类视图都会被实例化一次,但是你的URLs却是在被导入的时候才配置一次,也就是as_view()方法的参数的配置只有一次。也就是说这么做,就不能再改了!所以,不要偷懒,使用子类吧。

二、使用mixin混入

混入是一种多父类继承的形式,其基础理论知识请参考我的Python教程中多继承相关章节。
在编写子类的时候,一个父类负责类的主体结构、主要行为和主要属性,其它的功能都通过各种MIXin父类提供。
MIXIN是跨多个类重用代码的一个很好的方法,但是它们会带来一些代价。你的代码散布在MIXIN中越多,阅读子类就越难,很难知道它到底在做什么。如果你在对具有多级继承树的子类进行分类,就更难以判断子类的方法到底继承的是哪个先祖,俗称‘家谱乱了’。
需要注意的是你的父类中只有一个类可以继承最顶级的View类,其它的必须以mixin方法混入。

三、使用类视图处理表单

一个用来处理表单的函数视图通常是下面这样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from django.http import HttpResponseRedirect
from django.shortcuts import render

from .forms import MyForm

def myview(request):
if request.method == "POST":
form = MyForm(request.POST)
if form.is_valid():
# <process form cleaned data>
return HttpResponseRedirect('/success/')
else:
form = MyForm(initial={'key': 'value'})

return render(request, 'form_template.html', {'form': form})

而如果用类视图来实现,是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
from django.http import HttpResponseRedirect
from django.shortcuts import render
from django.views import View

from .forms import MyForm

class MyFormView(View):
form_class = MyForm
initial = {'key': 'value'}
template_name = 'form_template.html'

def get(self, request, *args, **kwargs):
form = self.form_class(initial=self.initial)
return render(request, self.template_name, {'form': form})

def post(self, request, *args, **kwargs):
form = self.form_class(request.POST)
if form.is_valid():
# <process form cleaned data>
return HttpResponseRedirect('/success/')

return render(request, self.template_name, {'form': form})

看起来类视图好像比函数视图代码多了很多,复杂了很多,貌似没啥优点啊。但是如果你有多个类似的视图需要编写,那么你就可以发挥子类的继承和复写神操作了,分分钟整出个新的视图来。或者直接在URLConf中修改参数!或者两种操作同时使用!

其实,到现在你应该理解,类视图适用于大量重复性的视图编写工作,在简单的场景下,没几个视图需要编写,或者各个视图差别很大的情况时,还是函数视图更有效!所以,不要认为类视图是多么高大上的东西,人云亦云!

四、装饰类视图

除了mixin,还可以使用装饰器扩展类视图。装饰器的工作方式取决于你是在创建子类还是使用as_view()。

用法一,在URLConf中直接装饰:

1
2
3
4
5
6
7
8
9
from django.contrib.auth.decorators import login_required, permission_required
from django.views.generic import TemplateView

from .views import VoteView

urlpatterns = [
path('about/', login_required(TemplateView.as_view(template_name="secret.html"))),
path('vote/', permission_required('polls.can_vote')(VoteView.as_view())),
]

用法二,在类视图中装饰指定的方法:

1
2
3
4
5
6
7
8
9
10
from django.contrib.auth.decorators import login_required
from django.utils.decorators import method_decorator
from django.views.generic import TemplateView

class ProtectedView(TemplateView):
template_name = 'secret.html'

@method_decorator(login_required)
def dispatch(self, *args, **kwargs):
return super().dispatch(*args, **kwargs)

注意:

  • 上面要把装饰器用在dispatch这个方法上,才能在每次请求到达URL时,实例化类视图时都运行这个装饰器的功能。
  • 不是每个装饰器都能直接运用在类方法上,需要使用method_decorator这个装饰器的装饰器方法将装饰器运用在类方法上。感觉很绕?其实就是说,我们有很多很多的装饰器,但其中有一些不能直接装饰dispatch这种类方法。那怎么办呢?套层壳!用method_decorator装饰器包裹起来,假装成一个能用的。

有时候,简单地用一下,可以写成下面的精简版:

1
2
3
@method_decorator(login_required, name='dispatch')
class ProtectedView(TemplateView):
template_name = 'secret.html'

有时候,可能你需要对一个对象应用多个装饰器,正常做法是:

1
2
3
4
@method_decorator(never_cache, name='dispatch')
@method_decorator(login_required, name='dispatch')
class ProtectedView(TemplateView):
template_name = 'secret.html'

为了偷懒,我们可以这么做:

1
2
3
4
5
decorators = [never_cache, login_required]

@method_decorator(decorators, name='dispatch')
class ProtectedView(TemplateView):
template_name = 'secret.html'

唯一需要注意地是装饰器是有先后顺序的。上面的例子中,never_cache就要先于login_required被调用。

最后,使用method_decorator有时会导致TypeError异常,因为参数传递的原因。

请我喝杯咖啡吧~

支付宝
微信