Django 中间件钩子函数 & CBV vs FBV 实战验证

一、中间件的 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 钩子核心要点

  1. process_request --- 最先执行,可用于请求预处理(如 IP 黑名单)
  2. process_view --- 可拿到视图函数名,适合做视图级别的日志或权限校验
  3. process_exception --- 全局异常捕获,适合统一错误页面
  4. process_response --- 最后执行,适合统一添加响应头(如 CORS)

CBV vs FBV 选择建议

场景 推荐
标准 CRUD(列表/详情/创建/更新/删除) CBV(通用视图一行顶十行)
自定义聚合页面(跨多表查询) FBV(过程式思维更直观)
文件下载/流式响应 FBV(控制响应头更灵活)
REST API CBV(配合 DRF 的 APIView/ViewSet)
简单渲染页面 两者皆可,FBV 更轻量
相关推荐
Maydaycxc5 小时前
企业内网 RPA 离线部署实践:从选型到落地的完整方案
运维·chrome·python·selenium·自动化·rpa
MY_TEUCK13 小时前
【2026最新Python+AI学习基础】Python 入门笔记篇
笔记·python·学习
赢乐14 小时前
大模型学习笔记:检索增强生成(RAG)架构
人工智能·python·深度学习·机器学习·智能体·幻觉·检索增强生成(rag)
浪里行舟16 小时前
你的品牌正在被AI“遗忘”?用BuildSOM找回搜索的下一个风口
人工智能·python·程序员
码界筑梦坊16 小时前
120-基于Python的食品营养特征数据可视化分析系统
开发语言·python·信息可视化·数据分析·毕业设计·echarts·fastapi
logo_2817 小时前
Xpath语法规则的学习和使用
javascript·python·xpath·xpath语法
快乐江湖17 小时前
「层层包装」—— 装饰器模式
开发语言·python·装饰器模式
m0_7020365318 小时前
mysql如何通过索引减少行锁范围_mysql索引与加锁逻辑
jvm·数据库·python
用户03321266636718 小时前
使用 Python 设置 Word 文档文本的颜色
python