類別式視圖簡介

類別式視圖提供了一種以 Python 物件而非函式來實作視圖的替代方法。它們並非取代函式式視圖,而是在與函式式視圖比較時,具有某些差異和優勢。

  • 與特定 HTTP 方法 (GETPOST 等) 相關的程式碼組織,可以透過獨立的方法而不是條件分支來處理。

  • 可以使用諸如 mixin (多重繼承) 等物件導向技術,將程式碼分解為可重複使用的元件。

通用視圖、類別式視圖和類別式通用視圖的關係和歷史

一開始只有視圖函式契約,Django 將一個 HttpRequest 傳遞給您的函式,並期望傳回一個 HttpResponse。這是 Django 所提供的全部內容。

早期人們就認識到,在視圖開發中存在一些常見的慣用語和模式。引入了函式式通用視圖,以抽象這些模式並簡化常見案例的視圖開發。

函式式通用視圖的問題在於,雖然它們很好地涵蓋了簡單的案例,但除了某些組態選項之外,沒有辦法擴充或自訂它們,這限制了它們在許多真實應用中的實用性。

建立類別式通用視圖的目標與函式式通用視圖相同,即簡化視圖開發。但是,該解決方案的實作方式,透過使用 mixin,提供了一個工具包,使得類別式通用視圖比其函式式對應物更具可擴充性和彈性。

如果您過去嘗試過函式式通用視圖並發現它們不足,則不應將類別式通用視圖視為類別式等效物,而應將其視為解決通用視圖原本旨在解決的原始問題的全新方法。

Django 用於建構類別式通用視圖的基礎類別和 mixin 工具包旨在實現最大的彈性,因此具有許多掛鉤,形式為您在最簡單的用例中不太可能關心的預設方法實作和屬性。例如,實作並非將您限制為 form_class 的類別式屬性,而是使用 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() 進入點設定的類別屬性僅在匯入您的 URL 時組態一次。

使用 mixin

Mixin 是一種多重繼承的形式,可以組合多個父類別的行為和屬性。

例如,在通用類別式視圖中,有一個名為 TemplateResponseMixin 的 mixin,其主要目的是定義方法 render_to_response()。當與 View 基礎類別的行為結合時,結果是一個 TemplateView 類別,它會將請求派送到適當的相符方法 (在 View 基礎類別中定義的行為),並且具有使用 template_name 屬性傳回 TemplateResponse 物件的 render_to_response() 方法 (在 TemplateResponseMixin 中定義的行為)。

Mixin 是跨多個類別重複使用程式碼的絕佳方式,但它們會帶來一些成本。您的程式碼分散在 mixin 中的越多,就越難讀取子類別並知道它確切在做什麼,並且如果您子類化具有深度繼承樹的內容,就越難知道要覆寫哪個 mixin 中的方法。

另請注意,您只能繼承一個通用視圖,也就是說,只有一個父類別可以繼承 View,其餘 (如果有的話) 應該是 mixin。嘗試從多個繼承自 View 的類別繼承 (例如,嘗試在清單頂端使用表單並結合 ProcessFormViewListView) 不會如預期般運作。

使用類別式視圖處理表單

處理表單的基本函式式視圖可能如下所示

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

這是一個最小的範例,但您可以看到,您然後可以透過 URLconf 組態覆寫任何類別屬性 (例如 form_class) 或子類化並覆寫一個或多個方法 (或兩者!) 來自訂此視圖。

裝飾類別式視圖

類別式視圖的擴充不僅限於使用 mixin。您也可以使用裝飾器。由於類別式視圖不是函式,因此裝飾它們的方式會因您使用 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 例外。

返回頂部