Django视图处理请求与响应:从HttpRequest到中间件的实战指南

发布时间:2026/10/21 19:12:01
Django视图处理请求与响应:从HttpRequest到中间件的实战指南
1. 从一次接口联调失败说起视图到底在干什么很多人学Django学到视图这一章就开始犯迷糊。模型层还能靠数据库表结构去理解模板层好歹能对着HTML看效果唯独视图层代码写完了浏览器一刷新要么是404要么是500要么返回一堆看不懂的报错。我见过不少初学者照着教程把代码敲完跑通了就觉得自己会了结果一换需求——比如前端要传JSON而不是表单、比如要返回文件流而不是页面——立刻就卡住。问题的根子在于很多人把视图理解成了一个函数接收请求返回页面。这个理解不能说错但太浅了。视图的本质是请求与响应之间的调度中枢它决定了一个HTTP请求进来之后该调用哪些逻辑、该操作哪些数据、该以什么格式把结果吐回去。你把它当成一个函数来记就只能写教程里那种demo你把它当成调度中枢来理解才能应对真实项目里千变万化的需求。这篇内容我打算把Django视图处理请求和响应这件事拆透。从请求对象里到底有什么、响应对象该怎么构造到函数视图和类视图怎么选、中间件在中间插了什么手再到实际项目里那些教程不会讲的坑。适合已经能跑通Django基础项目、但对视图层还是一知半解的人。如果你还在纠结path()和re_path()的区别那可能得先补一下路由的基础这篇默认你已经能配URL了。先说一个我自己的教训。早年做一个后台管理系统的导出功能前端点击导出Excel我写了个视图直接返回文件流。本地测试一切正常部署到服务器之后前端下载下来的文件永远是损坏的。排查了大半天最后发现是响应头里的Content-Disposition文件名带了中文没有做编码处理不同浏览器解析出来的文件名乱码导致文件内容被截断。这件事让我意识到响应不是返回个东西就完事了响应头、状态码、内容类型每一个字段都可能成为坑。后面我会专门讲这块。2. 请求对象HttpRequest里藏着你需要的一切2.1 请求对象的属性速查与常见误用Django把每个HTTP请求封装成一个HttpRequest对象作为视图函数的第一个参数传进来。这个对象里到底有什么很多人只记住了request.GET和request.POST其实远不止。先列一下最常用的属性我按使用频率排属性/方法说明典型场景request.method请求方法大写字符串判断GET还是POSTrequest.GETURL查询参数字典搜索、筛选、分页request.POST表单提交数据字典表单处理request.body原始请求体字节串接收JSON、XMLrequest.FILES上传文件字典文件上传request.COOKIESCookie字典读取客户端状态request.META请求头等元信息字典获取IP、User-Agentrequest.path请求路径不含域名和参数日志、权限判断request.user当前登录用户对象权限控制这里有几个高频误用点我一个个说。第一个是request.GET和request.POST的取值方式。很多人写request.GET[keyword]如果URL里没带这个参数直接抛KeyError页面500。正确做法是用request.GET.get(keyword, )给个默认值。这个坑几乎每个新手都会踩一次。第二个是request.POST只能取表单数据。如果前端用fetch发的是application/json你用request.POST是取不到任何东西的必须用request.body读原始字节再自己json.loads。我见过有人在这上面卡了一整天以为是前端没发出去其实是后端取错了地方。第三个是request.META的键名。它不是HTTP头原样而是加了前缀、转了大写、连字符换下划线。比如User-Agent在request.META里是HTTP_USER_AGENTContent-Type是CONTENT_TYPE注意这个没有HTTP_前缀。获取客户端IP要用request.META.get(REMOTE_ADDR)但如果你前面有反向代理这个值可能是代理的IP真实IP在HTTP_X_FORWARDED_FOR里。这个细节在做访问日志和限流的时候特别重要。2.2 从request.body到JSON解析一个完整的接收流程现在前后端分离是主流视图接收JSON请求几乎是标配。我把完整的处理流程写一遍包括异常处理。import json from django.http import JsonResponse from django.views.decorators.csrf import csrf_exempt csrf_exempt def create_order(request): if request.method ! POST: return JsonResponse({code: 405, msg: 方法不允许}, status405) try: payload json.loads(request.body) except json.JSONDecodeError: return JsonResponse({code: 400, msg: 请求体不是合法JSON}, status400) order_no payload.get(order_no) amount payload.get(amount) if not order_no or amount is None: return JsonResponse({code: 400, msg: 缺少必要参数}, status400) # 后续业务逻辑 return JsonResponse({code: 0, msg: 创建成功, data: {order_no: order_no}})这段代码里有几个值得说的点。csrf_exempt装饰器是干什么的Django默认开启CSRF防护表单提交必须带CSRF token。但前后端分离的接口前端通常不带这个token所以要么用csrf_exempt豁免要么用token认证机制。这里用豁免是为了演示简单实际项目里更推荐用DRF的认证体系或者自定义CSRF处理。注意豁免CSRF会降低安全性生产环境要配合其他鉴权手段。json.loads(request.body)这里request.body是字节串Python 3的json.loads能直接处理字节串但如果你用的是老版本或者有编码问题可能需要先request.body.decode(utf-8)。我一般习惯显式解码避免编码坑。异常处理这块json.JSONDecodeError是必须捕获的。前端传了个空body、或者传了个格式错误的字符串不捕获就是500。返回400比500更准确也方便前端定位问题。2.3 文件上传request.FILES的正确打开方式文件上传是另一个高频场景坑也不少。先看基本写法def upload_avatar(request): if request.method ! POST: return JsonResponse({code: 405, msg: 方法不允许}, status405) avatar request.FILES.get(avatar) if not avatar: return JsonResponse({code: 400, msg: 未选择文件}, status400) if avatar.size 5 * 1024 * 1024: return JsonResponse({code: 400, msg: 文件不能超过5MB}, status400) # 保存文件 with open(f/data/avatars/{avatar.name}, wb) as f: for chunk in avatar.chunks(): f.write(chunk) return JsonResponse({code: 0, msg: 上传成功})几个关键点。第一request.FILES只有在请求方法是POST且表单的enctype是multipart/form-data时才有值。如果前端用application/x-www-form-urlencoded文件是传不上来的。第二avatar.chunks()是分块读取比直接avatar.read()更省内存大文件上传一定要用chunks。第三avatar.name是客户端传来的文件名绝对不能直接用来做存储路径因为可能包含../这种路径穿越字符也可能有重名覆盖问题。生产环境要用UUID重命名。我踩过的一个坑是文件大小限制。Django有个DATA_UPLOAD_MAX_MEMORY_SIZE设置默认2.5MB超过这个大小的请求体不会全部读进内存。但文件上传走的是临时文件机制FILE_UPLOAD_MAX_MEMORY_SIZE默认也是2.5MB超过就写临时文件。如果你在视图里直接avatar.read()读大文件可能触发内存问题。用chunks分块处理是最稳的。3. 响应对象不只是return一个HttpResponse3.1 HttpResponse、JsonResponse、StreamingHttpResponse怎么选Django的响应体系有好几个类很多人只知道HttpResponse其实选对了类能省很多事。HttpResponse是最基础的接收一个字符串或字节串作为内容。返回HTML片段、纯文本用它。JsonResponse是HttpResponse的子类专门用来返回JSON。它内部会做json.dumps并自动设置Content-Type: application/json。注意它的第一个参数默认必须是字典如果要返回列表得加safeFalsereturn JsonResponse([1, 2, 3], safeFalse)这个safeFalse我见过太多人忘记加然后报TypeError。原因是早期Django出于安全考虑不允许顶层是列表防止JSON劫持后来虽然放宽了但默认还是只接受字典。StreamingHttpResponse用于流式响应比如大文件下载、实时日志推送。它接收一个迭代器边生成边发送不占内存。做文件下载的时候如果文件很大用HttpResponse会把整个文件读进内存再发服务器内存直接爆掉。用StreamingHttpResponse配合FileResponse才是正解。FileResponse是StreamingHttpResponse的子类专门用于文件下载会自动处理Content-Length和Content-Disposition。做下载功能优先用它。选型逻辑我总结成一句话返回结构化数据用JsonResponse返回页面用HttpResponse或render返回文件用FileResponse返回实时流用StreamingHttpResponse。3.2 响应头、状态码、Cookie那些容易忽略的细节响应不只是内容头信息同样重要。设置响应头有两种方式response JsonResponse({code: 0}) response[X-Custom-Header] some-value response.status_code 201 response.set_cookie(session_id, abc123, max_age3600, httponlyTrue) return response状态码这块很多人只会用200和404。其实RESTful接口里201表示创建成功204表示删除成功无内容返回400表示客户端参数错误401表示未认证403表示无权限405表示方法不允许429表示请求过于频繁。用对状态码前端处理逻辑会清晰很多。Cookie设置有几个安全参数必须注意。httponlyTrue让JavaScript无法读取Cookie防XSS。secureTrue让Cookie只在HTTPS下传输。samesiteLax或Strict防CSRF。这三个参数在生产环境建议都加上。我遇到过一个真实问题设置了secureTrue的Cookie在本地HTTP环境下死活写不进去排查半天才发现是secure参数导致的。本地开发要么关掉这个参数要么用HTTPS。这种环境差异导致的坑特别浪费时间。3.3 重定向与错误页redirect和HttpResponseRedirect的区别重定向用redirect快捷函数最方便from django.shortcuts import redirect def old_page(request): return redirect(/new-page/) def old_page_by_name(request): return redirect(new-page-name)redirect可以接收路径字符串、URL名称、甚至模型对象会调用get_absolute_url()。它返回的是HttpResponseRedirect状态码302。如果要永久重定向用permanentTrue状态码变成301。301会被浏览器缓存改起来很麻烦除非确定这个URL永远不变否则用302。我见过有人把临时重定向写成301后来URL调整了用户浏览器一直跳旧地址清缓存才能解决。自定义错误页需要在URL配置里指定handler# urls.py handler404 myapp.views.custom_404 handler500 myapp.views.custom_500对应的视图函数接收request和exception两个参数。注意DEBUGTrue的时候自定义错误页不生效必须DEBUGFalse才能看到效果。这个也是新手常问的问题。4. 函数视图与类视图不是新旧之争是场景之别4.1 函数视图的直白与类视图的复用函数视图FBV就是一个接收request返回response的函数直白、好懂、逻辑一目了然。类视图CBV把请求方法映射成类方法get()处理GETpost()处理POST通过继承和Mixin实现复用。很多人纠结用哪个。我的经验是逻辑简单、一次性、不需要复用的用函数视图有大量重复逻辑、需要按HTTP方法分派、需要继承扩展的用类视图。举个例子一个博客的列表页和详情页用函数视图写def post_list(request): posts Post.objects.filter(statuspublished) return render(request, post_list.html, {posts: posts}) def post_detail(request, pk): post get_object_or_404(Post, pkpk) return render(request, post_detail.html, {post: post})用类视图写from django.views.generic import ListView, DetailView class PostListView(ListView): model Post queryset Post.objects.filter(statuspublished) template_name post_list.html context_object_name posts class PostDetailView(DetailView): model Post template_name post_detail.html context_object_name post类视图代码更少但前提是你得知道ListView内部帮你做了什么——它自动调用了get_queryset()、自动把结果放进context、自动渲染模板。如果你不知道这些出了问题就无从下手。4.2 类视图的方法分派机制dispatch在做什么类视图的核心是dispatch方法。当请求进来Django会调用View.as_view()返回的那个函数这个函数内部实例化类然后调用dispatch。dispatch根据request.method的小写形式找到对应的get、post等方法去执行。理解这一点很重要因为你可以重写dispatch来做统一的预处理from django.views import View from django.http import JsonResponse class LoginRequiredView(View): def dispatch(self, request, *args, **kwargs): if not request.user.is_authenticated: return JsonResponse({code: 401, msg: 请先登录}, status401) return super().dispatch(request, *args, **kwargs) class OrderListView(LoginRequiredView): def get(self, request): # 这里不用再判断登录了 return JsonResponse({code: 0, data: []})这种写法比在每个方法里重复判断登录优雅得多。Django自带的LoginRequiredMixin就是这个思路只不过它处理的是页面跳转而不是JSON响应。4.3 Mixin的坑继承顺序决定一切Mixin是类视图复用的关键但继承顺序是个大坑。Python的MRO方法解析顺序是从左到右、深度优先。Mixin必须放在基类左边# 正确 class MyView(LoginRequiredMixin, View): pass # 错误LoginRequiredMixin的dispatch不会被调用 class MyView(View, LoginRequiredMixin): pass这个错误特别隐蔽因为代码不报错只是权限检查静默失效。我见过一个项目权限Mixin写在了右边结果所有接口都能未登录访问上线前测试才发现。记住一个原则Mixin永远写在最左边View或基类写在最右边。5. 中间件请求响应链上的隐形关卡5.1 中间件的执行顺序洋葱模型中间件是Django处理请求和响应过程中插入的钩子。它的执行模型像洋葱请求从外到内穿过每一层中间件到达视图响应再从内到外穿回来。配置里的顺序决定了请求阶段的执行顺序响应阶段则相反MIDDLEWARE [ django.middleware.security.SecurityMiddleware, # 请求最先响应最后 django.contrib.sessions.SessionMiddleware, django.middleware.common.CommonMiddleware, django.middleware.csrf.CsrfViewMiddleware, django.contrib.auth.middleware.AuthenticationMiddleware, django.contrib.messages.MessageMiddleware, django.middleware.clickjacking.XFrameOptionsMiddleware, # 请求最后响应最先 ]理解这个顺序很重要。比如AuthenticationMiddleware必须在SessionMiddleware之后因为它依赖session来获取用户。如果你把顺序调换了request.user就会出问题。5.2 自定义中间件记录请求耗时写一个中间件记录每个请求的耗时是理解中间件最好的练习import time import logging logger logging.getLogger(__name__) class TimingMiddleware: def __init__(self, get_response): self.get_response get_response def __call__(self, request): start time.time() response self.get_response(request) duration time.time() - start logger.info(f{request.method} {request.path} 耗时 {duration:.3f}s) response[X-Response-Time] f{duration:.3f}s return response__init__只在服务器启动时调用一次__call__每个请求都调用。get_response是下一个中间件或视图的调用入口。在它之前是请求阶段之后是响应阶段。这个中间件能帮你快速定位慢接口。我一般会在项目初期就加上配合日志分析哪些接口耗时超过1秒一目了然。比事后用性能分析工具去查高效得多。5.3 中间件里改response的注意事项在中间件里修改response要小心。比如你想给所有响应加一个头直接response[X-Foo] bar就行。但如果你想修改响应内容比如压缩、加密就得注意StreamingHttpResponse的情况——流式响应的内容不能直接改因为它是迭代器读一次就没了。还有一个坑是异常处理。如果视图抛异常中间件的响应阶段代码不会执行除非你用try/finally包起来。想在中间件里统一捕获异常得用process_exception钩子旧式写法或者在新式中间件里判断response是否为None。6. 实战中的那些坑从CSRF到异步视图6.1 CSRF防护为什么表单提交总是403CSRF是新手遇到的第一个大坑。表单提交返回403提示CSRF verification failed原因通常是模板里没加{% csrf_token %}。form methodpost {% csrf_token %} input typetext nameusername button typesubmit提交/button /form这个token会渲染成一个隐藏的input提交时一起发给后端后端校验通过才处理。它的原理是攻击者的网站无法读取你网站的Cookie也就无法伪造这个token所以能防住跨站请求伪造。前后端分离的项目CSRF处理更麻烦。常见方案有两种一是用csrf_exempt豁免配合JWT等token认证二是前端从Cookie里读csrftoken放到请求头X-CSRFToken里。Django的CsrfViewMiddleware会检查这个头。我推荐第二种安全性更好。前端配置function getCookie(name) { const value ; ${document.cookie}; const parts value.split(; ${name}); if (parts.length 2) return parts.pop().split(;).shift(); } fetch(/api/order/, { method: POST, headers: { X-CSRFToken: getCookie(csrftoken), Content-Type: application/json }, body: JSON.stringify(data) });后端不用做任何豁免正常写视图就行。这个方案的前提是csrftoken这个Cookie能被JavaScript读取所以它不能设httponly。Django默认的csrftoken就是可读的不用改。6.2 异步视图什么时候该用async defDjango 3.1之后支持异步视图用async def定义async def async_view(request): result await some_async_operation() return JsonResponse({result: result})但异步视图不是万能的。Django的ORM在异步视图里不能直接调用同步方法得用sync_to_async包装from asgiref.sync import sync_to_async async def async_view(request): posts await sync_to_async(list)(Post.objects.all()) return JsonResponse({count: len(posts)})什么时候该用异步当视图里有大量IO等待且这些IO是异步的比如调用异步HTTP客户端、异步数据库驱动用异步能提升并发能力。如果只是普通的ORM查询异步反而因为包装开销更慢。我见过有人为了时髦把所有视图改成异步结果性能不升反降。异步是工具不是目标。6.3 大文件下载FileResponse的正确姿势做文件下载用FileResponse最省心from django.http import FileResponse import os def download_file(request, file_id): file_path f/data/files/{file_id}.pdf if not os.path.exists(file_path): return JsonResponse({code: 404, msg: 文件不存在}, status404) response FileResponse(open(file_path, rb)) response[Content-Type] application/pdf response[Content-Disposition] attachment; filenamereport.pdf return responseContent-Disposition的attachment表示下载而不是在浏览器打开。文件名如果有中文要做URL编码from urllib.parse import quote filename quote(中文报告.pdf) response[Content-Disposition] fattachment; filename*UTF-8{filename}filename*UTF-8是RFC 5987规定的编码方式现代浏览器都支持。不加这个中文文件名在部分浏览器会乱码。这就是我开头提到的那个坑当时排查了很久才定位到。6.4 请求体读取一次就没了body和POST的互斥request.body和request.POST有个互斥关系一旦你读了request.bodyrequest.POST就空了反之亦然。因为请求体是个流读一次就到末尾了。这个坑在写中间件时特别容易踩。比如你在中间件里读了request.body做日志视图里再用request.POST取表单数据就取不到了。解决办法是在中间件里读完body后重新赋值回去class LogBodyMiddleware: def __init__(self, get_response): self.get_response get_response def __call__(self, request): if request.method POST: body request.body # 重新赋值让后续能再读 request._body body logger.info(f请求体: {body[:200]}) return self.get_response(request)request._body是Django内部缓存body的私有属性重新赋值后后续读request.body会直接返回缓存读request.POST也能正常解析。这个技巧知道的人不多但很实用。7. 我个人的一些经验总结视图这块说到底就三件事把请求里的数据取出来、按业务逻辑处理、把结果按正确的格式返回。听起来简单但每一环都有细节。我自己的习惯是视图函数尽量保持薄。数据校验、业务逻辑、数据操作能抽出去就抽出去放到表单类、服务层、模型方法里。视图只负责调度和组装响应。这样视图代码短好读也好测试。一个视图函数超过50行我就会考虑拆分了。另一个习惯是所有对外接口的响应格式统一。我一般用{code: 0, msg: , data: {}}这种结构code为0表示成功非0表示各种错误。前端处理起来逻辑一致不用每个接口单独判断。这个约定在项目初期定好后期维护省很多事。最后说一个调试技巧。视图出问题的时候别急着改代码先在视图第一行加个print(request.method, request.path)确认请求确实到了这个视图。很多时候问题出在URL配置或者中间件拦截根本没进视图。确认进来了再打印request.GET、request.POST、request.body看数据对不对。数据对了再往下查业务逻辑。这个逐层打印的方法比盲目猜测高效得多。视图是Django里承上启下的一层往上连着URL路由往下接着模型和模板。把这一层吃透整个Django的请求响应流程就通了。剩下的就是多写多踩坑坑踩多了自然就熟了。