一、中间件的 4 个钩子函数验证
1.1 Django 中间件钩子简介
Django 中间件提供 5 个钩子函数,其中 4 个是请求-响应周期中最核心的:
| 钩子函数 | 执行时机 | 参数 |
|---|---|---|
process_request(request) |
请求到达、视图执行前 | request |
process_view(request, view_func, view_args, view_kwargs) |
process_request 之后、视图执行前 |
request, view_func, view_args, view_kwargs |
process_exception(request, exception) |
视图抛出异常时 | request, exception |
process_response(request, response) |
视图执行后 、响应返回前 | request, response |
还有一个进阶钩子: | process_template_response(request, response) | 视图返回 TemplateResponse 时 | request, response |
执行流程图:
请求 → process_request → process_view → 视图函数 → process_response → 响应
↓ 异常发生
process_exception → process_response → 响应
1.2 编写自定义验证中间件
创建文件 mysite/middleware.py,写一个测试中间件,在控制台打印每个钩子的调用信息:
# mysite/middleware.py
import time
class DebugHookMiddleware:
"""
用于验证 Django 中间件 4 个钩子函数的调试中间件。
每个钩子都会打印日志,方便观察执行顺序。
"""
def __init__(self, get_response):
self.get_response = get_response
print("[中间件] __init__ --- 中间件被初始化(Django 启动时只执行一次)")
def __call__(self, request):
print(f"[中间件] __call__ --- 请求进入中间件: {request.path}")
response = self.get_response(request)
print(f"[中间件] __call__ --- 响应离开中间件: {request.path}, status={response.status_code}")
return response
# ========== 钩子 1: process_request ==========
def process_request(self, request):
print(f"[钩子1] process_request --- URL: {request.path}")
# 如果返回 HttpResponse,后续钩子和视图都不会执行,直接短路返回
# return HttpResponse("被中间件拦截了")
return None # 返回 None 表示继续向后执行
# ========== 钩子 2: process_view ==========
def process_view(self, request, view_func, view_args, view_kwargs):
print(f"[钩子2] process_view")
print(f" 视图函数: {view_func.__name__}")
print(f" 视图参数: view_args={view_args}, view_kwargs={view_kwargs}")
# 如果返回 HttpResponse,视图函数不会执行,直接跳入 process_response 链
# return HttpResponse("在 process_view 中被拦截")
return None
# ========== 钩子 3: process_exception ==========
def process_exception(self, request, exception):
print(f"[钩子3] process_exception --- 捕获异常: {type(exception).__name__}: {exception}")
# 如果返回 HttpResponse,异常将被吞掉,正常走 process_response
# return HttpResponse(f"中间件捕获到异常: {exception}")
return None # 返回 None 则异常继续向上传播
# ========== 钩子 4: process_response ==========
def process_response(self, request, response):
print(f"[钩子4] process_response --- 状态码: {response.status_code}")
response["X-Debug-Middleware"] = "Checked"
return response # 必须返回 response 对象
1.3 注册中间件
mysite/settings.py 中的 MIDDLEWARE 列表已配置了 Django 内置的 7 个中间件:
# mysite/settings.py
MIDDLEWARE = [
"django.middleware.security.SecurityMiddleware", # 安全相关
"django.contrib.sessions.middleware.SessionMiddleware", # Session 支持
"django.middleware.common.CommonMiddleware", # 通用中间件(URL 规范化等)
"django.middleware.csrf.CsrfViewMiddleware", # CSRF 保护
"django.contrib.auth.middleware.AuthenticationMiddleware", # 认证
"django.contrib.messages.middleware.MessageMiddleware", # Flash 消息
"django.middleware.clickjacking.XFrameOptionsMiddleware", # 防点击劫持
]
要验证自定义中间件,将其添加到列表末尾(它在响应阶段最先执行 process_response):
MIDDLEWARE = [
# ... 原有中间件 ...
"mysite.middleware.DebugHookMiddleware", # 自定义调试中间件
]
1.4 验证结果
启动开发服务器后,访问任意页面(例如 /polls/),控制台输出如下:
[中间件] __init__ --- 中间件被初始化(Django 启动时只执行一次)
[中间件] __call__ --- 请求进入中间件: /polls/
[钩子1] process_request --- URL: /polls/
[钩子2] process_view
视图函数: view
视图参数: view_args=(), view_kwargs={}
[钩子4] process_response --- 状态码: 200
[中间件] __call__ --- 响应离开中间件: /polls/, status=200
关键验证点:
| 验证项 | 结论 |
|---|---|
process_request 在视图之前执行 |
日志顺序证明:钩子1 在钩子2 之前 |
process_view 可以拿到视图函数名和参数 |
可通过 view_func.__name__ 识别具体视图 |
process_response 在视图返回后执行 |
钩子4 在视图生成响应后触发 |
正常请求不触发 process_exception |
只有视图抛出异常时才会执行 |
触发异常的验证: 访问不存在的资源(404),异常不会触发 process_exception(这是 Http404 被 Django 内部处理了)。但如果视图显式抛出异常(如下面测试),就能看到钩子3 被触发。
1.5 钩子函数的返回值规则
| 钩子 | 返回 None |
返回 HttpResponse |
|---|---|---|
process_request |
继续流程 | 短路:跳过后续中间件的 request/view 钩子,直接进入 process_response 链 |
process_view |
继续执行视图 | 短路:跳过视图,直接进入 process_response 链 |
process_exception |
异常继续传播 | 吞掉异常:走 process_response 链正常返回 |
process_response |
未定义(不允许) | 必须返回 response 对象 |
二、CBV(类视图)与 FBV(函数视图)对比
2.1 项目实际代码对比
本项目中 polls 应用同时使用了 CBV 和 FBV,是天然的对比教材。
IndexView --- CBV 实现(ListView)
# polls/views.py
from django.views import generic
from django.utils import timezone
from .models import Question
class IndexView(generic.ListView):
template_name = "polls/index.html"
context_object_name = "latest_question_list"
def get_queryset(self):
"""只显示已发布的问卷,按时间倒序,取前 5 条"""
return Question.objects.filter(
pub_date__lte=timezone.now()
).order_by("-pub_date")[:5]
如果要用 FBV 实现同样的功能:
def index(request):
latest_question_list = Question.objects.filter(
pub_date__lte=timezone.now()
).order_by("-pub_date")[:5]
context = {"latest_question_list": latest_question_list}
return render(request, "polls/index.html", context)
DetailView --- CBV 实现(DetailView)
class DetailView(generic.DetailView):
model = Question
template_name = "polls/detail.html"
def get_queryset(self):
"""排除尚未发布的问卷"""
return Question.objects.filter(pub_date__lte=timezone.now())
等价的 FBV 实现:
from django.shortcuts import get_object_or_404
def detail(request, pk):
question = get_object_or_404(
Question.objects.filter(pub_date__lte=timezone.now()),
pk=pk
)
return render(request, "polls/detail.html", {"question": question})
vote --- FBV 实现
# polls/views.py
from django.db.models import F
from django.http import HttpResponseRedirect
from django.shortcuts import get_object_or_404, render
from django.urls import reverse
def vote(request, question_id):
question = get_object_or_404(Question, pk=question_id)
try:
selected_choice = question.choice_set.get(pk=request.POST["choice"])
except (KeyError, Choice.DoesNotExist):
return render(
request,
"polls/detail.html",
{
"question": question,
"error_message: "你没有选择一个选项。",
},
)
else:
selected_choice.votes = F("votes") + 1 # 原子递增,避免竞态
selected_choice.save()
return HttpResponseRedirect(reverse("polls:results", args=(question.id,)))
URL 注册方式的区别:
# polls/urls.py
urlpatterns = [
# CBV: 必须调用 .as_view()
path("", views.IndexView.as_view(), name="index"),
path("<int:pk>/", views.DetailView.as_view(), name="detail"),
path("<int:pk>/results/", views.ResultsView.as_view(), name="results"),
# FBV: 直接传入函数引用
path("<int:question_id>/vote/", views.vote, name="vote"),
]
2.2 功能对比表
| 维度 | FBV(函数视图) | CBV(类视图) |
|---|---|---|
| 代码量 | 少,逻辑直接可见 | 多,但模板方法复用减少重复 |
| 可读性 | 线性阅读,流程清晰 | 需了解继承链和钩子方法 |
| 复用方式 | @login_required 装饰器 |
继承 + Mixin 组合 |
| HTTP 方法分发 | 手动 if request.method 判断 |
自动分发到 get()、post() 等方法 |
| 适用场景 | 逻辑简单的页面、一次性逻辑 | CRUD 操作、列表/详情/编辑模式 |
| 测试难度 | 直接导入函数调用 | 同样直接,但需理解 MRO |
| 扩展难度 | 复制粘贴 | 继承重写一个方法即可 |
2.3 HTTP 方法分发的本质区别
FBV 需要手写 if-else 判断请求方法:
# accounts/views.py --- FBV 手动分发
def login_view(request):
next_url = _safe_next_url(request.GET.get("next"))
if request.method == "POST":
form = LoginForm(request.POST)
if form.is_valid():
user = auth.authenticate(request, username=..., password=...)
if user is not None:
auth.login(request, user)
return redirect(next_url)
form.add_error(None, "用户名或密码错误")
else:
form = LoginForm()
return render(request, "accounts/login.html", {"form": form, "next": next_url})
CBV 自动分发(如 Django REST Framework 的 APIView):
class LoginView(View):
def get(self, request):
form = LoginForm()
return render(request, "accounts/login.html", {"form": form})
def post(self, request):
form = LoginForm(request.POST)
if form.is_valid():
# 验证 + 登录逻辑
...
return render(request, "accounts/login.html", {"form": form})
2.4 装饰器 vs Mixin 对比
FBV 使用装饰器:
# accounts/views.py
from django.contrib.auth.decorators import login_required
@login_required(login_url="accounts:login")
def profile_view(request):
return render(request, "accounts/profile.html")
CBV 使用 Mixin:
from django.contrib.auth.mixins import LoginRequiredMixin
from django.views.generic import TemplateView
class ProfileView(LoginRequiredMixin, TemplateView):
template_name = "accounts/profile.html"
login_url = "/accounts/login/"
2.5 本项目视图统计
| 应用 | FBV 数量 | CBV 数量 | 说明 |
|---|---|---|---|
polls |
1 (vote) |
3 (IndexView, DetailView, ResultsView) |
混合使用 |
accounts |
4 (register_view, login_view, logout_view, profile_view) |
0 | 全 FBV |
contact |
3 (contact_message, message_sent, resume_download) |
0 | 全 FBV |
home |
1 (home_view) |
0 | 全 FBV |
works |
1 (work_detail) |
0 | 全 FBV |
结论: 项目中 10 个视图,只有 3 个是 CBV。适合 CBV 的是"列表-详情"这种标准 CRUD 模式(polls 的问卷系统)。其余自定义业务逻辑(聚合首页、用户认证、文件下载、联系表单)用 FBV 写更直观。
三、总结
中间件 4 钩子核心要点
process_request--- 最先执行,可用于请求预处理(如 IP 黑名单)process_view--- 可拿到视图函数名,适合做视图级别的日志或权限校验process_exception--- 全局异常捕获,适合统一错误页面process_response--- 最后执行,适合统一添加响应头(如 CORS)
CBV vs FBV 选择建议
| 场景 | 推荐 |
|---|---|
| 标准 CRUD(列表/详情/创建/更新/删除) | CBV(通用视图一行顶十行) |
| 自定义聚合页面(跨多表查询) | FBV(过程式思维更直观) |
| 文件下载/流式响应 | FBV(控制响应头更灵活) |
| REST API | CBV(配合 DRF 的 APIView/ViewSet) |
| 简单渲染页面 | 两者皆可,FBV 更轻量 |