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 的繼承優勢。