Django Views:Function-Based Views 函數視圖入門 | Django 教學
在 Django 的 MTV(Model-Template-View)架構中, View 扮演著承上啟下的核心角色——它接收使用者的 HTTP 請求(Request),執行商業邏輯,最後回傳 HTTP 回應(Response)。本篇教學將聚焦於 Function-Based Views(FBV,函數視圖),這是 Django 中最直觀、最容易上手的 View 撰寫方式。我們將從 HttpRequest 與 HttpResponse 物件開始,逐步學會
render()、redirect()、 JsonResponse 等常用工具,最後完成一個完整的 CRUD 範例。
View 在 MTV 架構中的角色
在前幾篇教學中,我們認識了 Django 的 MTV 架構:Model 負責資料層、Template 負責呈現層、View 負責邏輯層。View 就像是整個應用的「交通指揮中心」,它的工作流程如下:
- 接收來自使用者的 HttpRequest 物件(包含請求方法、參數、表單資料等)
- 執行對應的商業邏輯(查詢資料庫、處理表單、計算結果等)
- 回傳一個 HttpResponse 物件(HTML 頁面、JSON 資料、重導向等)
┌──────────┐ HttpRequest ┌──────────┐ 查詢/寫入 ┌──────────┐
│ │ ──────────────────→ │ │ ──────────────→ │ │
│ 使用者 │ │ View │ │ Model │
│ │ ←────────────────── │ │ ←────────────── │ │
└──────────┘ HttpResponse └──────────┘ 資料結果 └──────────┘
│
│ 渲染模板
↓
┌──────────┐
│ Template │
└──────────┘
Django 的 View 最小定義非常簡潔——只要是一個接收 request 參數並回傳 HttpResponse(或其子類別)的 Python 函式,就是一個合法的 FBV。
HttpRequest 物件
當使用者發送請求到 Django 應用時,Django 會自動將原始的 HTTP 請求封裝成一個 HttpRequest 物件,並作為第一個參數傳入 View 函式。這個物件包含了請求的所有資訊。
常用屬性一覽
def example_view(request):
# HTTP 請求方法:'GET'、'POST'、'PUT'、'DELETE' 等
print(request.method)
# GET 參數(QueryDict 物件),例如 ?page=2&sort=name
print(request.GET)
print(request.GET.get('page', '1')) # 取得 page 參數,預設值為 '1'
# POST 表單資料(QueryDict 物件)
print(request.POST)
print(request.POST.get('username', ''))
# 請求的原始內容(bytes),常用於處理 JSON 請求
print(request.body)
# 當前登入的使用者(需啟用 AuthenticationMiddleware)
print(request.user)
# Session 物件(dict-like),用於儲存跨請求的資料
print(request.session)
# 請求路徑
print(request.path) # 例如 '/articles/1/'
print(request.get_full_path()) # 例如 '/articles/1/?page=2'
request.GET 和 request.POST 都是 QueryDict 物件,它是 Python 字典(dict)的子類別,支援同一個 key 對應多個值(例如多選的 checkbox)。使用 .get() 方法取值時可以指定預設值,避免 key 不存在時拋出例外(Exception)。
HttpResponse 物件
View 函式必須回傳一個 HttpResponse 物件(或其子類別)。HttpResponse 代表伺服器要回傳給瀏覽器的回應內容。
from django.http import HttpResponse
def hello_view(request):
# 最基本的回應:回傳純文字
response = HttpResponse("Hello, Django!")
print(response.status_code) # 200(預設狀態碼)
print(response.content) # b'Hello, Django!'
return response
你也可以自訂狀態碼(Status Code)和回應標頭(Headers):
from django.http import HttpResponse
def custom_response(request):
response = HttpResponse("找不到頁面", status=404)
response['X-Custom-Header'] = 'my-value'
return response
Django 也提供了多個 HttpResponse 的子類別,對應不同的 HTTP 狀態碼:
| 類別 | 狀態碼 | 用途 |
|---|---|---|
HttpResponse | 200 | 一般成功回應 |
HttpResponseRedirect | 302 | 暫時重導向 |
HttpResponsePermanentRedirect | 301 | 永久重導向 |
HttpResponseNotFound | 404 | 找不到資源 |
HttpResponseForbidden | 403 | 禁止存取 |
HttpResponseBadRequest | 400 | 錯誤的請求 |
JsonResponse | 200 | JSON 格式回應 |
第一個 FBV:Hello World
讓我們從最簡單的 FBV 開始。首先在 views.py 中定義一個 View 函式:
# views.py
from django.http import HttpResponse
def hello_world(request):
"""最簡單的 FBV,回傳純文字"""
return HttpResponse("Hello, World! 歡迎來到 Django FBV 教學。")
接著在 urls.py 中設定 URL 路由,將網址對應到這個 View:
# urls.py
from django.urls import path
from . import views
urlpatterns = [
path('hello/', views.hello_world, name='hello_world'),
]
啟動開發伺服器後,在瀏覽器訪問 http://127.0.0.1:8000/hello/,就能看到 “Hello, World! 歡迎來到 Django FBV 教學。” 的文字。
接收 URL 參數
Django 的 URL 路由(URL Routing)支援從網址中擷取參數,並將它們傳入 View 函式:
# urls.py
from django.urls import path
from . import views
urlpatterns = [
# <str:name> 擷取字串參數,傳入 View 的 name 參數
path('hello/<str:name>/', views.hello_name, name='hello_name'),
# <int:pk> 擷取整數參數,傳入 View 的 pk 參數
path('articles/<int:pk>/', views.article_detail, name='article_detail'),
]
# views.py
from django.http import HttpResponse
def hello_name(request, name):
"""接收 URL 中的 name 參數"""
return HttpResponse(f"Hello, {name}!")
# 訪問 /hello/Django/ → 輸出 "Hello, Django!"
def article_detail(request, pk):
"""接收 URL 中的文章 ID"""
return HttpResponse(f"你正在查看第 {pk} 篇文章。")
# 訪問 /articles/42/ → 輸出 "你正在查看第 42 篇文章。"
常用的 URL 路徑轉換器(Path Converter)包括:
| 轉換器 | 說明 | 範例 |
|---|---|---|
str | 匹配任何非空字串(不含 /) | <str:slug> |
int | 匹配零或正整數 | <int:pk> |
slug | 匹配由字母、數字、-、_ 組成的字串 | <slug:post_slug> |
uuid | 匹配 UUID 格式字串 | <uuid:id> |
處理 GET 請求:列出資料
在實際應用中,GET 請求通常用來「取得」或「列出」資料。以下範例示範如何從資料庫查詢文章列表:
# views.py
from django.shortcuts import render
from .models import Post
def post_list(request):
"""列出所有已發布的文章"""
posts = Post.objects.filter(is_published=True).order_by('-created_at')
# 從 GET 參數取得搜尋關鍵字(選填)
keyword = request.GET.get('q', '')
if keyword:
posts = posts.filter(title__icontains=keyword)
context = {
'posts': posts,
'keyword': keyword,
}
return render(request, 'blog/post_list.html', context)
# render() 會載入模板、注入 context 變數、回傳 HttpResponse
當使用者訪問 /posts/?q=django 時,View 會從 GET 參數中取得搜尋關鍵字 django,過濾出標題包含該關鍵字的文章,並透過 render() 渲染模板後回傳。
處理 POST 請求:建立資料
POST 請求通常用來「提交」或「建立」資料。在同一個 View 中,我們經常需要同時處理 GET(顯示表單)和 POST(接收表單資料)兩種請求:
# views.py
from django.shortcuts import render, redirect
from .forms import PostForm
def post_create(request):
"""建立新文章:GET 顯示空白表單,POST 處理表單提交"""
if request.method == 'POST':
# POST 請求:接收使用者提交的表單資料
form = PostForm(request.POST)
if form.is_valid():
# 表單驗證通過,儲存到資料庫
post = form.save(commit=False)
post.author = request.user
post.save()
# 建立成功,重導向到文章詳情頁
return redirect('post_detail', pk=post.pk)
else:
# GET 請求:建立空白表單
form = PostForm()
# GET 或表單驗證失敗時,渲染表單頁面
return render(request, 'blog/post_form.html', {'form': form})
這個模式是 Django FBV 中最經典的「GET 顯示 / POST 處理」雙重職責(Dual Responsibility)模式,幾乎所有表單處理的 View 都遵循這個結構。
render() 快捷函數
render() 是 Django 最常用的快捷函數(Shortcut Function),它結合了「載入模板」和「回傳回應」兩個步驟:
from django.shortcuts import render
def about_page(request):
context = {
'title': '關於我們',
'team_members': ['Alice', 'Bob', 'Charlie'],
}
# render(request, 模板路徑, context 字典)
return render(request, 'pages/about.html', context)
render() 等同於以下寫法的簡化版:
from django.template.loader import get_template
from django.http import HttpResponse
def about_page(request):
template = get_template('pages/about.html')
context = {'title': '關於我們'}
html = template.render(context, request)
return HttpResponse(html)
實務上幾乎不會使用手動載入模板的寫法,直接用 render() 即可。
redirect() 重導向
redirect() 快捷函數用於回傳 HTTP 重導向回應(預設為 302 暫時重導向),常用於表單提交成功後將使用者引導到另一個頁面:
from django.shortcuts import redirect
def some_view(request):
# 方式一:使用 URL name(推薦)
return redirect('post_list')
# 方式二:使用 URL name 並帶入參數
return redirect('post_detail', pk=42)
# 方式三:直接指定 URL 路徑
return redirect('/articles/')
# 方式四:永久重導向(301)
return redirect('post_list', permanent=True)
最佳實踐: 建議使用 URL name 搭配
redirect(),而非寫死 URL 路徑。這樣即使未來修改了 URL 結構,也不需要逐一更新 View 中的路徑。
JsonResponse:回傳 JSON 資料
當你的 View 需要回傳 JSON(JavaScript Object Notation)格式的資料時(例如提供 API 給前端 JavaScript 呼叫),可以使用 JsonResponse:
from django.http import JsonResponse
def api_post_list(request):
"""回傳文章列表的 JSON 資料"""
posts = Post.objects.filter(is_published=True).values('id', 'title', 'created_at')
data = {
'status': 'ok',
'count': posts.count(),
'results': list(posts),
}
# safe=False 允許回傳非 dict 的頂層資料(如 list)
return JsonResponse(data)
# 輸出:{"status": "ok", "count": 3, "results": [...]}
def api_post_detail(request, pk):
"""回傳單篇文章的 JSON 資料"""
try:
post = Post.objects.get(pk=pk)
except Post.DoesNotExist:
return JsonResponse({'error': '文章不存在'}, status=404)
data = {
'id': post.pk,
'title': post.title,
'content': post.content,
'created_at': post.created_at.isoformat(),
}
return JsonResponse(data)
JsonResponse 會自動將 Python 字典序列化(Serialize)為 JSON 格式,並設定回應的 Content-Type 為 application/json。
get_object_or_404() 快捷函數
在查詢單一物件時,如果物件不存在,我們通常希望回傳 404 錯誤頁面,而非讓程式拋出未處理的例外。get_object_or_404() 正是為此設計的快捷函數:
from django.shortcuts import get_object_or_404, render
def post_detail(request, pk):
# 如果 pk 對應的 Post 不存在,自動回傳 404 頁面
post = get_object_or_404(Post, pk=pk)
return render(request, 'blog/post_detail.html', {'post': post})
這等同於以下手動處理的寫法:
from django.http import Http404
def post_detail(request, pk):
try:
post = Post.objects.get(pk=pk)
except Post.DoesNotExist:
raise Http404("文章不存在")
return render(request, 'blog/post_detail.html', {'post': post})
get_object_or_404() 也支援額外的篩選條件:
# 查詢指定作者的文章,不存在則 404
post = get_object_or_404(Post, pk=pk, author=request.user)
# 也可以傳入 QuerySet
published_posts = Post.objects.filter(is_published=True)
post = get_object_or_404(published_posts, pk=pk)
裝飾器:限制 HTTP 方法
Django 提供了多個裝飾器(Decorator)來限制 View 只接受特定的 HTTP 方法(HTTP Method),如果請求使用了不允許的方法,Django 會自動回傳 405 Method Not Allowed 錯誤。
from django.views.decorators.http import require_http_methods, require_GET, require_POST
# 只允許 GET 和 POST 方法
@require_http_methods(["GET", "POST"])
def post_create(request):
if request.method == 'POST':
# 處理表單提交
pass
else:
# 顯示表單
pass
# 只允許 GET 方法(語法糖)
@require_GET
def post_list(request):
posts = Post.objects.all()
return render(request, 'blog/post_list.html', {'posts': posts})
# 只允許 POST 方法(語法糖)
@require_POST
def post_delete(request, pk):
post = get_object_or_404(Post, pk=pk, author=request.user)
post.delete()
return redirect('post_list')
@require_GET 和 @require_POST 分別是 @require_http_methods(["GET"]) 和 @require_http_methods(["POST"]) 的簡寫形式,讓程式碼更簡潔。
@login_required 簡介
@login_required 裝飾器用於保護 View,確保只有已登入的使用者才能存取。如果使用者尚未登入,Django 會自動將他們重導向到登入頁面:
from django.contrib.auth.decorators import login_required
@login_required
def post_create(request):
"""只有登入的使用者才能建立文章"""
# 這裡的 request.user 保證是已登入的使用者
pass
@login_required(login_url='/accounts/login/')
def dashboard(request):
"""自訂登入頁面的 URL"""
return render(request, 'dashboard.html')
當未登入的使用者嘗試訪問被 @login_required 保護的 View 時,Django 會將他們重導向到 settings.LOGIN_URL(預設為 /accounts/login/),並在 URL 中附帶 next 參數,讓使用者登入後能自動回到原本要訪問的頁面。
完整 CRUD 範例
讓我們把前面學到的所有知識整合起來,以一個部落格的 Post Model 為例,實作完整的 CRUD(Create、Read、Update、Delete)功能。
Model 與 Form 定義
# models.py
from django.db import models
from django.contrib.auth.models import User
class Post(models.Model):
title = models.CharField('標題', max_length=200)
content = models.TextField('內容')
author = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name='作者')
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']
def __str__(self):
return self.title
# forms.py
from django import forms
from .models import Post
class PostForm(forms.ModelForm):
class Meta:
model = Post
fields = ['title', 'content', 'is_published']
widgets = {
'title': forms.TextInput(attrs={'class': 'form-control'}),
'content': forms.Textarea(attrs={'class': 'form-control', 'rows': 8}),
}
Views(完整 CRUD)
# views.py
from django.shortcuts import render, get_object_or_404, redirect
from django.contrib.auth.decorators import login_required
from django.views.decorators.http import require_http_methods, require_GET, require_POST
from .models import Post
from .forms import PostForm
# ─── Read(列表) ───
@require_GET
def post_list(request):
"""列出所有已發布的文章"""
posts = Post.objects.filter(is_published=True)
return render(request, 'blog/post_list.html', {'posts': posts})
# ─── Read(詳情) ───
@require_GET
def post_detail(request, pk):
"""查看單篇文章"""
post = get_object_or_404(Post, pk=pk, is_published=True)
return render(request, 'blog/post_detail.html', {'post': post})
# ─── Create(建立) ───
@login_required
@require_http_methods(["GET", "POST"])
def post_create(request):
"""建立新文章"""
if request.method == 'POST':
form = PostForm(request.POST)
if form.is_valid():
post = form.save(commit=False)
post.author = request.user # 自動設定作者為當前使用者
post.save()
return redirect('post_detail', pk=post.pk)
else:
form = PostForm()
return render(request, 'blog/post_form.html', {'form': form, 'action': '建立'})
# ─── Update(更新) ───
@login_required
@require_http_methods(["GET", "POST"])
def post_update(request, pk):
"""更新文章(僅限作者本人)"""
post = get_object_or_404(Post, pk=pk, author=request.user)
if request.method == 'POST':
form = PostForm(request.POST, instance=post)
if form.is_valid():
form.save()
return redirect('post_detail', pk=post.pk)
else:
form = PostForm(instance=post)
return render(request, 'blog/post_form.html', {
'form': form,
'post': post,
'action': '更新',
})
# ─── Delete(刪除) ───
@login_required
@require_http_methods(["GET", "POST"])
def post_delete(request, pk):
"""刪除文章(僅限作者本人)"""
post = get_object_or_404(Post, pk=pk, author=request.user)
if request.method == 'POST':
post.delete()
return redirect('post_list')
# GET 請求:顯示確認刪除頁面
return render(request, 'blog/post_confirm_delete.html', {'post': post})
URL 路由設定
# urls.py
from django.urls import path
from . import views
urlpatterns = [
path('posts/', views.post_list, name='post_list'),
path('posts/<int:pk>/', views.post_detail, name='post_detail'),
path('posts/create/', views.post_create, name='post_create'),
path('posts/<int:pk>/update/', views.post_update, name='post_update'),
path('posts/<int:pk>/delete/', views.post_delete, name='post_delete'),
]
以上五個 URL 對應五個 View 函式,涵蓋了完整的 CRUD 操作。注意 create/ 路由必須放在 <int:pk>/ 之前,否則 Django 會嘗試將 create 當作 pk 來匹配,導致錯誤。
總結
在這篇教學中,我們深入學習了 Django Function-Based Views(FBV)的核心知識:
- View 的角色:在 MTV 架構中,View 負責接收 HttpRequest、執行邏輯、回傳 HttpResponse,是連接 Model 與 Template 的橋樑。
- HttpRequest 物件:封裝了請求的所有資訊,包括
method、GET、POST、body、user、session等屬性。 - HttpResponse 物件:代表伺服器的回應,可以設定
status_code、content以及自訂標頭。 - 快捷函數:
render()結合模板渲染、redirect()處理重導向、get_object_or_404()安全地查詢單一物件。 - JsonResponse:專門用於回傳 JSON 格式的回應,自動處理序列化與 Content-Type 設定。
- 裝飾器:
@require_http_methods、@require_GET、@require_POST限制允許的 HTTP 方法;@login_required保護需要登入才能存取的 View。 - 完整 CRUD 範例:以 Post Model 為例,展示了列表、詳情、建立、更新、刪除五個 View 的實作方式與 URL 路由設定。
FBV 的最大優勢是直觀易懂,程式碼的執行流程一目了然。對於邏輯簡單或高度客製化的需求,FBV 是非常好的選擇。在後續的教學中,我們將學習 Class-Based Views(CBV),透過類別繼承和 Mixin 機制實現更高效的程式碼複用。