Django 的 CBV 最佳實踐


Django 的 CBV 最佳實踐

Django 視圖本質是一個函數:接受 HttpRequest 對象作為參數,返回一個 HttpResponse 對象作為返回。FBV 直接就是這樣一個函數,而 CBV 類的方法 as_view(),它的返回也是這樣一個函數。


Django 視圖本質是一個函數:接受 HttpRequest 對象作為參數,返回一個 HttpResponse 對象作為返回。FBV 直接就是這樣一個函數,而 CBV 類的方法 as_view(),它的返回也是這樣一個函數。

Django 提供了一些通用視圖, generic class-based views (GCBV),可以加快開發。

django.views.generic 中提供的這些 GCBV,或者 Mixin 還不夠完善,沒有包括認證等功能。因此,可采用 django-braces 這個第三方庫來彌補空缺。

CBV 代碼編寫指南:

  • 視圖代碼越少越好
  • 視圖代碼不能重復
  • 視圖應該只處理呈現邏輯。業務邏輯應放在數據模型中,或者表單對象中
  • 保持視圖代碼簡單
  • 不要用 CBV 來實現自定義的 403, 404 和 500 等錯誤處理器,應使用 FBV 實現
  • 保持 Mixins 簡潔

在 CBV 中使用 Mixins

子類通過多重繼承 Mixin,可以將 Mixin 中的功能和行為包含進自身。

因此,我們可以利用 Mixin 的功能來組裝我們的視圖類。

使用 Mixin 時,推薦遵循 kenneth Love 的繼承規則,該規則也是從左到右進行處理的,和 Python 的方法解析規則類似:

  • Django 提供的基類移到右邊
  • Mixin 放在左邊
  • Mixin 應該繼承自 object

一個簡單的例子如下:

from django.views.generic import TemplateView class FreshFruitMixin(object): def get_context_data(self, **kwargs): context = super(FreshFruitMixin, self).get_context_data(**kwargs) context["has_fresh_fruit"] = True return context class FruityFlavorView(FreshFruitMixin, TemplateView): template_name = "fruity_flavor.html" 

哪個 Django GCBV 應該用於哪個任務?

GCBV 的可重用性是以犧牲易用性為代價的。GCBV 有復雜的繼承關系鏈。

下表列出了 django.views.generic 中的各 GCBV 的用途:

名稱 目的 例子
View 視圖基礎類 使用 django.views.generic.View
RedirectView 重定向到 URL 如重定向到 ‘/login/’
TemplateView 顯示 HTML 模板 如 ‘/about/’ 頁
ListView 列出對象  
DetailView 對象的詳細信息  
FormView 提交表單  
CreateView 創建對象  
UpdateView 更新對象  
DeleteView 刪除對象  
通用時間視圖 顯示某個時間段內的對象  

如何利用 Django CBV/GCBV 的三種觀點:

  • 盡量利用 Django 提供的所有通用視圖。推薦這種觀點
  • 只使用 django.views.generic.View
  • 盡量避免使用 CBV,先都有 FBV,只在必要時改用 CBV

關於 Django CBV 的通用建議

如何限制 CBV/GCBV 只能由認證用戶訪問

django.contrib.auth.decorators.login_required 裝飾器應用到 CBV 比較麻煩,應使用 django-braces 提供的 LoginRequiredMixin,例如:

# flavors/views.py
from django.views.generic import DetailView

from braces.views import LoginRequiredMixin

from .models import Flavor

class FlavorDetailView(LoginRequiredMixin, DetailView):
    model = Flavor

 

表單有效時在視圖的 form_valid() 中進行后續處理

在調用 form_valid() 時,表單內的所有數據都已驗證過,並且都有效。 form_valid() 應該返回一個 django.http.HttpResponseRedirect 對象。

例如:

from django.views.generic import CreateView

from braces.views import LoginRequiredMixin

from .models import Flavor

class FlavorCreateView(LoginRequiredMixin, CreateView):
    model = Flavor
    fields = ('title', 'slug', 'scoops_remaining')

    def form_valid(self, form):
        # Do custom logic here
        return super(FlavorCreateView, self).form_valid(form)

 

表單無效時在視圖的 form_invalid() 中進行后續處理

如果表單的數據在驗證時無效,會調用該方法,該方法應該返回一個 django.http.HttpResponse 對象。

例如:

from django.views.generic import CreateView

from braces.views import LoginRequiredMixin

from .models import Flavor

class FlavorCreateView(LoginRequiredMixin, CreateView):
    model = Flavor

    def form_invalid(self, form):
        # Do custom logic here
        return super(FlavorCreateView, self).form_invalid(form)

 

在模板中引用視圖 view 對象

可以在模板代碼中,通過 view 對象變量,調用相關的屬性和方法。

例如,定義的視圖如下:

from django.utils.functional import cached_property
from django.views.generic import UpdateView, TemplateView

from braces.views import LoginRequiredMixin

from .models import Flavor
from .tasks import update_users_who_favorited

class FavoriteMixin(object):

    @cached_property
    def likes_and_favorites(self):
        """Returns a dictionary of likes and favorites"""
        likes = self.object.likes()
        favorites = self.object.favorites()
        return {
            "likes": likes,
            "favorites": favorites,
            "favorites_count": favorites.count(),
        }

class FlavorUpdateView(LoginRequiredMixin, FavoriteMixin, UpdateView):
    model = Flavor
    fields = ('title', 'slug', 'scoops_remaining')

    def form_valid(self, form):
        update_users_who_favorited(
            instance=self.object,
            favorites=self.likes_and_favorites['favorites']
        )
        return super(FlavorCreateView, self).form_valid(form)

class FlavorDetailView(LoginRequiredMixin, FavoriteMixin, TemplateView):
    model = Flavor

 

然后在模板代碼中,訪問視圖對象:

{# flavors/base.html #}
{% extends "base.html" %}

{% block likes_and_favorites %}
<ul>
  <li>Likes: {{ view.likes_and_favorites.likes }}</li>
  <li>Favorites: {{ view.likes_and_favorites.favorites_count }}</li>
</ul>
{% endblock likes_and_favorites %}

 

GCBV 和表單如何結合使用

以下例子中使用的數據模型定義如下:

# flavors/models.py
from django.core.urlresolvers import reverse
from django.db import models

STATUS = (
    (0, "zero"),
    (1, "one"),
)

class Flavor(models.Model):
    title = models.CharField(max_length=255)
    slug = models.SlugField(unique=True)
    scoops_remaining = models.IntegerField(default=0, choices=STATUS)

    def get_absolute_url(self):
        return reverse("flavors:detail", kwargs={"slug": self.slug})

 

下面是使用表單的幾種場景:

1、Views + ModelForm

這是最簡單最常見的表單場景。當創建數據模型后,通常需要能夠添加一條新記錄、更新記錄。

以下例子將創建一些視圖來對 Flavor 記錄進行創建、更新和顯示。同時演示如何向用戶提供消息提醒。

  • FlavorCreateView 對應創建新記錄的表單
  • FlavorUpdateView 對應更新記錄的表單
  • FlavorDetailView 顯示記錄詳情,並作為創建和更新操作的確認頁顯示

視圖代碼如下:

# flavors/views.py
from django.views.generic import CreateView, UpdateView, DetailView

from braces.views import LoginRequiredMixin

from .models import Flavor

class FlavorCreateView(LoginRequiredMixin, CreateView):
    model = Flavor
    fields = ('title', 'slug', 'scoops_remaining')

class FlavorUpdateView(LoginRequiredMixin, UpdateView):
    model = Flavor
    fields = ('title', 'slug', 'scoops_remaining')

class FlavorDetailView(DetailView):
    model = Flavor

 

由於 FlavorDetailView 要作為操作確認界面,需要對不同的操作提醒不同的消息。可以使用 django.contrib.messages 的相關功能完成。

下面將重載 FlavorCreateView 和 FlavorUpdateView 的 form_valid() 方法,實現當操作完成后推送不同的消息。可以將重復的代碼提取出來,放在一個 Mixin 中,如下:

# flavors/views.py

from django.contrib import messages
from django.views.generic import CreateView, UpdateView, DetailView

from braces.views import LoginRequiredMixin

from .models import Flavor

class FlavorActionMixin(object):

    fields = ('title', 'slug', 'scoops_remaining')

    @property
    def success_msg(self):
        return NotImplemented

    def form_valid(self, form):
        messages.info(self.request, self.success_msg)
        return super(FlavorActionMixin, self).form_valid(form)

class FlavorCreateView(LoginRequiredMixin, FlavorActionMixin,
                        CreateView):
    model = Flavor
    success_msg = "Flavor created!"

class FlavorUpdateView(LoginRequiredMixin, FlavorActionMixin,
                        UpdateView):
    model = Flavor
    success_msg = "Flavor updated!"

class FlavorDetailView(DetailView):
    model = Flavor

 

當 FlavorCreateView 或 FlavorUpdateView 操作完成后,FlavorDetailView 的模板代碼就可以通過訪問 messages 變量來獲取相關推送消息了,如下:

{# templates/flavors/flavor_detail.html #}
{% if messages %}
  <ul class="messages">
    {% for message in messages %}
    <li id="message_{{ forloop.counter }}"
      {% if message.tags %} class="{{ message.tags }}"
        {% endif %}>
      {{ message }}
    </li>
    {% endfor %}
  </ul>
{% endif %}

 

以上的模板代碼可以放在項目的 BASE 模板中。

2、Views + Form

以查詢表單為例,先顯示一個查詢表單頁,提交后通過 ORM 查詢,將查詢結果列表顯示出來。

在本例中,只實現一個 FlavorListView,將查詢表單和查詢結果全部都顯示在該頁中。

由於查詢沒有修改數據,因此表單方法用 GET。要正確顯示匹配的查詢列表,需要重載 ListView 的 get_queryset() 方法,視圖代碼如下:

from django.views.generic import ListView

from .models import Flavor

class FlavorListView(ListView):
    model = Flavor

    def get_queryset(self):
        # Fetch the queryset from the parent get_queryset
        queryset = super(FlavorListView, self).get_queryset()

        # Get the q GET parameter
        q = self.request.GET.get("q")
        if q:
            # Return a filtered queryset
            return queryset.filter(title__icontains=q)
        # Return the base queryset
        return queryset

 

由於查詢框表單可能會出現在多個頁面中,因此將這部分代碼片段保存在 _flavor_search.html,方便在其它模板文件中導入,代碼如下:

{# templates/flavors/_flavor_search.html #}
{% comment %}
    Usage: {% include "flavors/_flavor_search.html" %}
{% endcomment %}
<form action="{% url "flavor_list" %}"  method="GET">
    <input type="text" name="q" />
    <button type="submit">search</button>
</form>

 

只使用 django.views.generic.View

FBV 如果要區別不同的 HTTP 方法,需要用 if 塊,而 CBV 只需定義 get(),post() 方法即可,比較清晰明了。

如下面的代碼所示,繼承 View 類后,CBV 只需定義 get(),post() 就能完成相應的 HTTP 請求。

from braces.views import LoginRequiredMixin

from .forms import FlavorForm
from .models import Flavor

class FlavorView(LoginRequiredMixin, View):

    def get(self, request, *args, **kwargs):
        # Handles display of the Flavor object
        flavor = get_object_or_404(Flavor, slug=kwargs['slug'])
        return render(request,
            "flavors/flavor_detail.html",
                {"flavor": flavor}
            )

    def post(self, request, *args, **kwargs):
        # Handles updates of the Flavor object
        flavor = get_object_or_404(Flavor, slug=kwargs['slug'])
        form = FlavorForm(request.POST)
        if form.is_valid():
            form.save()
        return redirect("flavors:detail", flavor.slug)

 

這種寫法和 FBV 類似,但是更加清晰,而且也可以加入 Mixin。

它最適合用來輸出 JSON、PDF、Excel 等非 HTML 內容。如下例如下:

from django.http import HttpResponse
from django.shortcuts import get_object_or_404
from django.views.generic import View

from braces.views import LoginRequiredMixin

from .models import Flavor
from .reports import make_flavor_pdf

class PDFFlavorView(LoginRequiredMixin, View):

    def get(self, request, *args, **kwargs):
        # Get the flavor
        flavor = get_object_or_404(Flavor, slug=kwargs['slug'])

        # create the response
        response = HttpResponse(content_type='application/pdf')

        # generate the PDF stream and attach to the response
        response = make_flavor_pdf(response, flavor)

        return response

 

實際上,這種方式即保持了 FBV 的簡單,又具有了 CBV 的繼承優勢。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM