Django Views:Class-Based Views 類別視圖深入解析 | Django 教學
在前面的文章中,我們使用 函數式視圖(Function-Based Views,FBV) 來處理各種請求。FBV 直觀好懂,但當專案規模成長,你會發現大量的視圖函數充斥著重複的邏輯——取得物件、渲染模板、處理表單、驗證權限,每個 view 都在做類似的事。這正是 Class-Based Views(CBV)類別視圖 登場的時機。Django 內建了一套強大的 通用視圖(Generic Views),讓你用最少的程式碼完成最常見的 Web 開發模式。本篇文章將從 CBV 的核心概念出發,逐步帶你掌握每一個內建通用視圖,並在最後以完整的 CRUD 範例收尾。
為什麼需要 CBV?FBV 的局限性
先回顧一下典型的 FBV 寫法。假設我們要建立一個文章列表頁面:
# views.py — FBV 寫法
from django.shortcuts import render
from .models import Post
def post_list(request):
posts = Post.objects.filter(is_published=True).order_by('-created_at')
return render(request, 'blog/post_list.html', {'posts': posts})
看起來很簡潔,但如果你有十幾個 Model 都需要列表頁面呢?每個 view 都要寫幾乎一樣的程式碼:查詢資料庫、傳入模板、渲染回應。當需要加上分頁(Pagination)、權限檢查時,重複的程式碼會快速膨脹。
FBV 的主要局限性包括:
- 程式碼重複:多個視圖有相似邏輯時,需要手動抽取共用函數
- HTTP 方法處理繁瑣:需要用
if request.method == 'POST'手動判斷 - 複用性不佳:無法透過繼承來擴展行為,只能用裝飾器(Decorator)或輔助函數
- 擴展困難:新增功能時往往需要修改原有函數,不符合開放封閉原則(Open-Closed Principle)
CBV 透過 物件導向(OOP) 的繼承與 Mixin 機制,優雅地解決了這些問題。
View 基礎類別與 as_view() 方法
所有 CBV 的根基都是 django.views.View 類別。它提供了兩個核心機制:
as_view():類別方法,將 CBV 轉換為可以放在 URLconf 中的視圖函數dispatch():根據 HTTP 方法自動分派請求到對應的處理方法
# views.py — 最基本的 CBV
from django.http import HttpResponse
from django.views import View
class HelloView(View):
def get(self, request):
return HttpResponse("Hello, this is a GET request!")
def post(self, request):
return HttpResponse("Hello, this is a POST request!")
# urls.py — 使用 as_view() 註冊路由
from django.urls import path
from .views import HelloView
urlpatterns = [
path('hello/', HelloView.as_view(), name='hello'),
]
as_view() 的內部運作流程如下:
- 建立類別的實例(Instance)
- 呼叫
dispatch()方法 dispatch()檢查request.method,將請求轉給get()、post()等對應方法- 如果請求的 HTTP 方法不在
http_method_names列表中,回傳405 Method Not Allowed
這就是為什麼 CBV 不需要 if request.method == 'POST' 這種判斷——方法分派(Method Dispatch) 機制已經幫你處理好了。
TemplateView:最簡單的 CBV
TemplateView 是最基本的通用視圖,專門用來渲染一個模板。適合靜態頁面或只需要傳入少量上下文資料的頁面:
# views.py
from django.views.generic import TemplateView
class AboutView(TemplateView):
template_name = 'blog/about.html'
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['page_title'] = '關於我們'
context['version'] = '2.0'
return context
# urls.py — 也可以直接在 URLconf 中使用
from django.views.generic import TemplateView
urlpatterns = [
path('about/', TemplateView.as_view(template_name='blog/about.html'), name='about'),
]
get_context_data() 是 CBV 中最常覆寫的方法之一,用來向模板傳入額外的上下文資料。記得一定要呼叫 super().get_context_data(**kwargs) 保留父類別的上下文。
ListView:列表頁面
ListView 用來顯示某個 Model 的物件列表,內建分頁功能:
# views.py
from django.views.generic import ListView
from .models import Post
class PostListView(ListView):
model = Post
template_name = 'blog/post_list.html' # 預設為 <app>/<model>_list.html
context_object_name = 'posts' # 預設為 object_list
paginate_by = 10 # 每頁顯示 10 筆
ordering = ['-created_at'] # 排序方式
自訂 get_queryset()
如果需要過濾查詢結果,覆寫 get_queryset() 方法:
class PostListView(ListView):
model = Post
template_name = 'blog/post_list.html'
context_object_name = 'posts'
paginate_by = 10
def get_queryset(self):
# 只顯示已發布的文章
queryset = super().get_queryset().filter(is_published=True)
# 支援搜尋功能
keyword = self.request.GET.get('q')
if keyword:
queryset = queryset.filter(title__icontains=keyword)
return queryset.order_by('-created_at')
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['keyword'] = self.request.GET.get('q', '')
return context
在模板中使用分頁:
<!-- blog/post_list.html -->
{% for post in posts %}
<article>
<h2>{{ post.title }}</h2>
<p>{{ post.content|truncatewords:30 }}</p>
</article>
{% endfor %}
<!-- 分頁導航 -->
{% if is_paginated %}
<nav>
{% if page_obj.has_previous %}
<a href="?page={{ page_obj.previous_page_number }}">上一頁</a>
{% endif %}
<span>第 {{ page_obj.number }} / {{ page_obj.paginator.num_pages }} 頁</span>
{% if page_obj.has_next %}
<a href="?page={{ page_obj.next_page_number }}">下一頁</a>
{% endif %}
</nav>
{% endif %}
DetailView:詳細頁面
DetailView 用來顯示單一物件的詳細資訊,預設會根據 URL 中的 pk 或 slug 參數查詢物件:
# views.py
from django.views.generic import DetailView
from .models import Post
class PostDetailView(DetailView):
model = Post
template_name = 'blog/post_detail.html' # 預設為 <app>/<model>_detail.html
context_object_name = 'post' # 預設為 object
# urls.py
urlpatterns = [
path('post/<int:pk>/', PostDetailView.as_view(), name='post_detail'),
# 也可以用 slug
path('post/<slug:slug>/', PostDetailView.as_view(), name='post_detail_by_slug'),
]
如果使用 slug,需要指定 slug_field 和 slug_url_kwarg:
class PostDetailView(DetailView):
model = Post
template_name = 'blog/post_detail.html'
context_object_name = 'post'
slug_field = 'slug' # Model 中的欄位名稱
slug_url_kwarg = 'slug' # URL 中的參數名稱
CreateView:建立表單頁面
CreateView 整合了表單顯示與物件建立的邏輯,處理 GET 請求時顯示空白表單,處理 POST 請求時驗證並儲存資料:
# views.py
from django.views.generic import CreateView
from django.urls import reverse_lazy
from .models import Post
from .forms import PostForm
class PostCreateView(CreateView):
model = Post
form_class = PostForm # 使用自訂表單
template_name = 'blog/post_form.html'
success_url = reverse_lazy('post_list') # 建立成功後導向列表頁
def form_valid(self, form):
# 在儲存前設定作者為當前使用者
form.instance.author = self.request.user
return super().form_valid(form)
如果不需要自訂表單,也可以直接用 fields 屬性指定欄位:
class PostCreateView(CreateView):
model = Post
fields = ['title', 'content', 'category'] # 自動產生 ModelForm
template_name = 'blog/post_form.html'
success_url = reverse_lazy('post_list')
注意:
success_url使用reverse_lazy()而非reverse(),因為 URLconf 在類別定義時可能尚未載入完成,reverse_lazy()會延遲到實際需要時才解析 URL。
UpdateView:編輯表單頁面
UpdateView 與 CreateView 類似,但它會預先填入既有物件的資料:
# views.py
from django.views.generic import UpdateView
class PostUpdateView(UpdateView):
model = Post
form_class = PostForm
template_name = 'blog/post_form.html'
def get_success_url(self):
# 更新成功後導向該文章的詳細頁面
return reverse_lazy('post_detail', kwargs={'pk': self.object.pk})
CreateView 和 UpdateView 可以共用同一個模板,模板中透過判斷 object 是否存在來區分建立與編輯:
<!-- blog/post_form.html -->
<h2>{% if object %}編輯文章{% else %}新增文章{% endif %}</h2>
<form method="post">
{% csrf_token %}
{{ form.as_p }}
<button type="submit">{% if object %}更新{% else %}建立{% endif %}</button>
</form>
DeleteView:刪除確認頁面
DeleteView 處理物件刪除,GET 請求顯示確認頁面,POST 請求執行刪除:
# views.py
from django.views.generic import DeleteView
class PostDeleteView(DeleteView):
model = Post
template_name = 'blog/post_confirm_delete.html'
success_url = reverse_lazy('post_list')
<!-- blog/post_confirm_delete.html -->
<h2>確認刪除</h2>
<p>你確定要刪除「{{ object.title }}」嗎?此操作無法復原。</p>
<form method="post">
{% csrf_token %}
<button type="submit">確認刪除</button>
<a href="{% url 'post_detail' object.pk %}">取消</a>
</form>
Mixin 設計模式
Mixin 是 Python 多重繼承的一種設計模式,透過將小型、可複用的功能拆分成獨立的類別,再組合到目標類別中。Django 提供了多個內建 Mixin,最常用的是認證與權限相關的 Mixin。
LoginRequiredMixin
要求使用者必須登入才能存取視圖:
from django.contrib.auth.mixins import LoginRequiredMixin
from django.views.generic import CreateView
class PostCreateView(LoginRequiredMixin, CreateView):
model = Post
form_class = PostForm
template_name = 'blog/post_form.html'
login_url = '/accounts/login/' # 未登入時導向的頁面(可選)
redirect_field_name = 'next' # 登入後返回原頁面的參數名稱
PermissionRequiredMixin
要求使用者具備特定權限:
from django.contrib.auth.mixins import PermissionRequiredMixin
class PostCreateView(PermissionRequiredMixin, CreateView):
model = Post
form_class = PostForm
template_name = 'blog/post_form.html'
permission_required = 'blog.add_post' # <app_label>.<permission_codename>
UserPassesTestMixin
自訂測試條件,例如限制只有作者本人才能編輯文章:
from django.contrib.auth.mixins import UserPassesTestMixin
class PostUpdateView(LoginRequiredMixin, UserPassesTestMixin, UpdateView):
model = Post
form_class = PostForm
template_name = 'blog/post_form.html'
def test_func(self):
post = self.get_object()
return self.request.user == post.author
Mixin 的繼承順序
Mixin 必須放在繼承列表的最左側,這是因為 Python 的 MRO(Method Resolution Order)方法解析順序 由左至右查找方法:
# 正確:LoginRequiredMixin 在最左側,會先執行權限檢查
class PostCreateView(LoginRequiredMixin, CreateView):
pass
# 錯誤:LoginRequiredMixin 在右側,權限檢查可能被跳過
class PostCreateView(CreateView, LoginRequiredMixin):
pass
自訂 get_queryset() 與 get_context_data()
這兩個方法是客製化 CBV 行為最常用的切入點。
get_queryset():控制資料查詢
class MyPostListView(LoginRequiredMixin, ListView):
model = Post
template_name = 'blog/my_posts.html'
context_object_name = 'posts'
def get_queryset(self):
# 只顯示當前使用者的文章
return super().get_queryset().filter(author=self.request.user)
get_context_data():傳入額外資料
class PostDetailView(DetailView):
model = Post
template_name = 'blog/post_detail.html'
context_object_name = 'post'
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
# 傳入相關文章
context['related_posts'] = Post.objects.filter(
category=self.object.category
).exclude(pk=self.object.pk)[:5]
# 傳入留言表單
context['comment_form'] = CommentForm()
return context
FBV vs CBV 選擇指南
| 比較面向 | FBV(Function-Based Views) | CBV(Class-Based Views) |
|---|---|---|
| 可讀性 | 直觀、線性流程,新手友善 | 結構化但需了解繼承與 MRO |
| 複用性 | 需手動抽取共用邏輯 | Mixin 機制天然支援複用 |
| HTTP 方法處理 | 需 if/elif 手動判斷 | get()、post() 自動分派 |
| 適用場景 | 邏輯簡單、一次性需求、高度客製化 | 標準 CRUD、通用模板需求 |
| 裝飾器 | 直接套用 @decorator | 需使用 method_decorator 包裝 |
| 程式碼量 | 簡單場景較少,複雜場景較多 | 標準場景極少,非標準場景可能較多 |
| 學習曲線 | 低,Python 函數基礎即可 | 中,需了解 OOP、繼承、Mixin |
| 測試難度 | 直接呼叫函數 | 需透過 Client 或 RequestFactory |
選擇原則:
- 邏輯簡單或高度客製化 –> FBV
- 標準 CRUD 操作 –> CBV(Generic Views)
- 多個視圖共用相同邏輯 –> CBV + Mixin
- API 端點 –> 考慮 Django REST Framework 的 CBV
- 不確定時 –> 先用 FBV,需要複用時再重構為 CBV
完整 CRUD 範例:以 Post Model 為例
以下是一個完整的部落格文章 CRUD 實作,整合前面所有知識點。
Model 定義
# models.py
from django.db import models
from django.contrib.auth.models import User
from django.urls import reverse
class Post(models.Model):
title = models.CharField('標題', max_length=200)
content = models.TextField('內容')
author = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name='作者')
category = models.CharField('分類', max_length=50, blank=True)
is_published = models.BooleanField('已發布', default=False)
created_at = models.DateTimeField('建立時間', auto_now_add=True)
updated_at = models.DateTimeField('更新時間', auto_now=True)
class Meta:
ordering = ['-created_at']
verbose_name = '文章'
verbose_name_plural = '文章'
def __str__(self):
return self.title
def get_absolute_url(self):
return reverse('post_detail', kwargs={'pk': self.pk})
Form 定義
# forms.py
from django import forms
from .models import Post
class PostForm(forms.ModelForm):
class Meta:
model = Post
fields = ['title', 'content', 'category', 'is_published']
widgets = {
'title': forms.TextInput(attrs={'class': 'form-control'}),
'content': forms.Textarea(attrs={'class': 'form-control', 'rows': 10}),
'category': forms.TextInput(attrs={'class': 'form-control'}),
}
CBV 視圖
# views.py
from django.views.generic import (
ListView, DetailView, CreateView, UpdateView, DeleteView
)
from django.contrib.auth.mixins import LoginRequiredMixin, UserPassesTestMixin
from django.urls import reverse_lazy
from .models import Post
from .forms import PostForm
class PostListView(ListView):
"""文章列表頁面"""
model = Post
template_name = 'blog/post_list.html'
context_object_name = 'posts'
paginate_by = 10
def get_queryset(self):
return super().get_queryset().filter(is_published=True)
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['page_title'] = '文章列表'
return context
class PostDetailView(DetailView):
"""文章詳細頁面"""
model = Post
template_name = 'blog/post_detail.html'
context_object_name = 'post'
class PostCreateView(LoginRequiredMixin, CreateView):
"""新增文章頁面(需登入)"""
model = Post
form_class = PostForm
template_name = 'blog/post_form.html'
success_url = reverse_lazy('post_list')
def form_valid(self, form):
# 自動設定作者為當前登入使用者
form.instance.author = self.request.user
return super().form_valid(form)
class PostUpdateView(LoginRequiredMixin, UserPassesTestMixin, UpdateView):
"""編輯文章頁面(需登入且為作者本人)"""
model = Post
form_class = PostForm
template_name = 'blog/post_form.html'
def test_func(self):
post = self.get_object()
return self.request.user == post.author
def get_success_url(self):
return reverse_lazy('post_detail', kwargs={'pk': self.object.pk})
class PostDeleteView(LoginRequiredMixin, UserPassesTestMixin, DeleteView):
"""刪除文章頁面(需登入且為作者本人)"""
model = Post
template_name = 'blog/post_confirm_delete.html'
success_url = reverse_lazy('post_list')
def test_func(self):
post = self.get_object()
return self.request.user == post.author
URL 路由
# urls.py
from django.urls import path
from . import views
urlpatterns = [
path('', views.PostListView.as_view(), name='post_list'),
path('post/<int:pk>/', views.PostDetailView.as_view(), name='post_detail'),
path('post/new/', views.PostCreateView.as_view(), name='post_create'),
path('post/<int:pk>/edit/', views.PostUpdateView.as_view(), name='post_update'),
path('post/<int:pk>/delete/', views.PostDeleteView.as_view(), name='post_delete'),
]
這個範例展示了 CBV 的威力:五個視圖類別,每個都不超過 15 行程式碼,就完成了一個具備權限控制的完整 CRUD 功能。相較於同等功能的 FBV 版本,程式碼量大幅減少,邏輯也更加清晰。
總結
本篇文章深入解析了 Django Class-Based Views(CBV) 的核心概念與實戰用法:
- View 基礎類別 透過
as_view()與dispatch()實現了自動的 HTTP 方法分派機制 - TemplateView 適合簡單的模板渲染頁面
- ListView 與 DetailView 處理最常見的列表與詳細頁面需求,內建分頁功能
- CreateView、UpdateView、DeleteView 三劍客完整覆蓋了表單處理與 CRUD 操作
- Mixin 設計模式 讓你以組合的方式加入認證、權限等橫切關注點(Cross-Cutting Concerns),且繼承順序至關重要
- get_queryset() 與 get_context_data() 是客製化 CBV 行為最常用的兩個方法
- FBV 與 CBV 各有適用場景,根據需求選擇適合的寫法才是正確的做法
掌握 CBV 之後,你會發現 Django 開發變得更加高效——大量的樣板程式碼被通用視圖吸收,你只需要專注在真正與業務相關的客製化邏輯上。在下一篇文章中,我們將探索 Django 的 Middleware 中介層,了解請求在到達視圖之前與離開視圖之後經歷了什麼處理流程。