基于类的视图

基于类的视图提供另一种将视图实现为 Python 对象而不是函数的方法。它们不能替代基于函数的视图,但与基于函数的视图相比,它们是有某些不同和优势的。

  • 与特定的 HTTP 方法(GET, POST, 等等)关联的代码组织能通过单独的方法替代条件分支来解决。
  • 面向对象技术(比如 mixins 多重继承)可用于将代码分解为可重用组件。

通用视图、基于类的视图和基于类的通用视图的关系和历史

一开始,这里只有视图函数,Django 传递 HttpRequest 函数并预期返回一个 HttpResponse 。这是 Django 能提供的范围。

早期人们就发现在视图开发过程中有常见的约定和模式。引入了基于函数的通用视图为这些常见情况抽象这些模式和简单视图的开发。

基于函数的通用视图的问题是即便它们可以很好的处理简单案例,但除了一些配置选项之外,没办法扩展或自定义它们,这样就限制了它们在实际应用中用途。

创建基于类的通用视图与基于函数的通用视图具有相同的目标,那就是使视图开发更容易。然而,通过使用 mixins 实现解决方案的方式提供了一个工具包,使基于类的通用视图比基于函数的通用视图更灵活,更有扩展性。

如果你之前有尝试过基于函数的通用视图并发现了它的不足之处,那么你不应该认为基于类的通用视图只是基于类的等效视图,而是作为一种新的方法来解决通用视图要解决的原始问题。

为了获得最大的灵活性,Django 使用基础类和mixins的工具包来构建通用视图,因此在默认方法实现和属性的形式中有很多钩子,你在最简单的用例中不太可能涉及到这些钩子。比如,不要将你限制为 form_class 的基于类的属性,使用 get_form 方法来实现,使用 get_form 方法,它调用 get_form_class 方法,在默认实现里只返回类的 form_class 属性。这给你一些选项来指定使用的表单,从简单属性到完全动态的可调用属性。这些选项看起来增加了复杂度,但没有它们,会限制更高级的设计。

使用基于类的视图

本质上来说,基于类的视图允许你使用不同的类实例方法响应不同 HTTP 请求方法,而不是在单个视图函数里使用有条件分支的代码。

因此在视图函数里处理 HTTP GET 的代码应该像下面这样:

from django.http import HttpResponse


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

而在基于类的视图里,会变成:

from django.http import HttpResponse
from django.views import View


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

因为 Django 的 URL 解析器期望发送请求和相关参数来调动函数而不是类,基于类的视图有一个 as_view() 类方法,当一个请求到达的 URL 被关联模式匹配时,这个类方法返回一个函数。这个函数创建一个类的实例,调用 setup() 初始化它的属性,然后调用 dispatch() 方法。 dispatch 观察请求并决定它是 GETPOST,等等。如果它被定义,那么依靠请求来匹配方法,否则会引发 HttpResponseNotAllowed

# urls.py
from django.urls import path
from myapp.views import MyView

urlpatterns = [
    path("about/", MyView.as_view()),
]

值得注意的是,你的方法返回值和基于函数的视图返回值是相同的,既某种形式的 HttpResponse 。这意味着 http 快捷函数TemplateResponse 对象可以使用基于类里的视图。

虽然基于类的最小视图不需要任何类属性来执行任务,类属性在很多基于类的始终很常见,这里有两种方法来配置或设置类属性。

第一种是子类化标准 Python 方式,并且在子类中覆盖属性和方法。所以如果父类有个像 greeting 这样的属性:

from django.http import HttpResponse
from django.views import View


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

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

你可以在子类中覆盖它:

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

另一个选择是在 URLconf 中将配置类属性作为参数来调用 as_view()

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

备注

当你的类为发送给它的每个请求实例化时,通过 as_view() 入口点设置的类属性在导入 URLs 的时候只配置一次。

使用 mixins

Mixins 是一个多继承表单,其中可组合多个父类的行为和属性。

举例,在通用基于类的视图中,名为 TemplateResponseMixin 的 mixin 的首要目的是定义方法 render_to_response()。当与视图的基类行为结合使用时,结果是一个 TemplateView 类,它将请求分派到适当的匹配方法(在视图基类中定义的行为),并且具有 render_to_response() 方法,该方法使用 template_name 属性返回一个 TemplateResponse 对象(在 TemplateResponseMixin 中定义的行为)。

Mixins 是在多个类中重用代码的绝佳方法,但它们需要一些代价。代码分散在 Mixins 中的越多,理解子类并知道它到底在做什么就越困难,而且如果你正在子类化具有深继承树的东西,那么就越难知道要从哪个 mixns 的方法中来覆盖它。

也需要注意你只能从一个通用视图继承——只有一个父类可以继承自 View ,剩余的(如果有的话)应该继承自 mixins 。试着从更多的继承自 View 的类继承的话——例如试着在列表顶部使用表单并组合 ListView ——将无法按照预期工作。

使用基于类的视图处理表单

处理表单的基于函数的基础视图如下所示:

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})

类似的基于类的视图可能看起来像这样:

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})

这是一个很小的案例,但你可以看到你可以选择通过覆盖类的任何属性来自定义这个视图,比如 form_class ,通过 URLconf 配置或者子类化和重写一个或多个方法(或者两种都可以)。

装饰基于类的视图

基于类的视图的扩展不仅限于使用 mixins ,你也可以使用装饰器。因为基于类的视图不是函数,所以根据你是使用 as_view() 还是创建子类,装饰它们的工作方式会有不同。

在 URLconf 中装饰

可以通过装饰 as_view() 方法的结果来调整基于类的视图。最简单的方法是在你部署视图的 URLconf 中执行此操作:

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())),
]

这个方式在每个基本实例上应用装饰器。如果你想装饰视图的每个实例,你需要采用不同方式。

装饰类

装饰基于类的视图的每个实例,你需要装饰类定义本身。为此,你可以将装饰器应用到类的 dispatch() 方法。

类上的方法与独立函数完全不同,因此你不能应用函数装饰器到方法上——你需要先将它转换为方法装饰器。method_decorator 装饰器转换函数装饰器为方法装饰器,这样它就被用在实例方法上。举例:

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)

或者,更简洁的说,你可以用装饰类来代替,并作为关键参数 name 传递要被装饰的方法名:

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

如果你在一些地方使用了常见的装饰器,你可以定义一个装饰器列表或元组,并使用它而不是多次调用 method_decorator() 。这两个类是等价的:

decorators = [never_cache, login_required]


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


@method_decorator(never_cache, name="dispatch")
@method_decorator(login_required, name="dispatch")
class ProtectedView(TemplateView):
    template_name = "secret.html"

装饰器将按照它们传递给装饰器的顺序来处理请求。在这个例子里,never_cache() 将在 login_required() 之前处理请求。

在这个例子里,ProtectedView 的每一个实例将被登录保护。尽管这些例子使用 login_required ,但可以使用 LoginRequiredMixin 获得同样的行为。

备注

method_decorator*args**kwargs 作为参数传递给类上的装饰方法。如果你的方法不接受兼容参数集合,它会引发 TypeError 错误。