Django Views:Class-Based Views 類別視圖深入解析 | Django 教學

2026/06/02 2026/05/25
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 類別。它提供了兩個核心機制:

  1. as_view():類別方法,將 CBV 轉換為可以放在 URLconf 中的視圖函數
  2. 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() 的內部運作流程如下:

  1. 建立類別的實例(Instance)
  2. 呼叫 dispatch() 方法
  3. dispatch() 檢查 request.method,將請求轉給 get()post() 等對應方法
  4. 如果請求的 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 中的 pkslug 參數查詢物件:

# 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_fieldslug_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
測試難度直接呼叫函數需透過 ClientRequestFactory

選擇原則

  • 邏輯簡單或高度客製化 –> 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 適合簡單的模板渲染頁面
  • ListViewDetailView 處理最常見的列表與詳細頁面需求,內建分頁功能
  • CreateViewUpdateViewDeleteView 三劍客完整覆蓋了表單處理與 CRUD 操作
  • Mixin 設計模式 讓你以組合的方式加入認證、權限等橫切關注點(Cross-Cutting Concerns),且繼承順序至關重要
  • get_queryset()get_context_data() 是客製化 CBV 行為最常用的兩個方法
  • FBV 與 CBV 各有適用場景,根據需求選擇適合的寫法才是正確的做法

掌握 CBV 之後,你會發現 Django 開發變得更加高效——大量的樣板程式碼被通用視圖吸收,你只需要專注在真正與業務相關的客製化邏輯上。在下一篇文章中,我們將探索 Django 的 Middleware 中介層,了解請求在到達視圖之前與離開視圖之後經歷了什麼處理流程。

BenZ Software Developer

熱愛技術的軟體開發者,在這裡分享程式開發經驗與學習筆記。