做Django开发有一段时间的同学,多半会碰到这样一个困惑:明明前端已经把参数传过来了,后端却拿不到,或者拿到的东西跟你预想的不一样。这个问题十有八九出在request对象的理解上。Django里的request对象,说白了就是“前端请求在后端世界里的投影”,它把HTTP请求里里外外的东西都装在一个Python对象里,你只有搞清楚它的结构和脾气,才能真正做到“前端怎么传,后端怎么接”。
这篇文章基于我自己的Django学习笔记整理,聚焦request对象本身。我会把它从里到外拆开,配合前端传参的常见姿势和后端接收的对应写法,把容易踩的坑也一并说了。适合刚学Django、准备自己做项目实战的新手,也适合写过一些接口但还没系统梳理过请求处理逻辑的同学。内容以实用为主,代码可以直接抄,原理讲得通俗,保证你看完就能用。
1. 理解核心思路:request对象到底装了什么
1.1 一次HTTP请求变成Python对象的过程
在浏览器或者Postman里发一个请求,本质上只是一堆文本在网络里跑了一圈。这些文本包含请求行(方法、路径)、请求头(Headers)、请求体(Body)三个部分。Django的WSGI服务器(比如runserver自带的wsgiref,或者部署时的Gunicorn)接收到这堆文本后,会在中间件层完成一道关键工序:把这堆文本解析、封装成一个WSGIRequest实例,也就是我们视图函数里拿到的request。
这个过程怎么说呢,就好比快递员送包裹,前端把箱子(HTTP报文)递过来,Django帮你拆箱、编号、分类,最后把一张填写好的“包裹清单”(request对象)交到你手里。你不需要关心箱子原来长什么样,只需要知道清单上哪个字段对应哪项信息即可。
视图函数接收request作为一个必需的参数,这几乎是Django每个视图的固定姿势。在函数视图里它就是第一个参数,在类视图里它通过self.request访问。只要记住一个核心结论:Django视图里处理请求,第一步永远是搞明白当前这个request里面有哪些信息可以用、该用什么方式取。而它的主要数据来源,就是请求方法、请求头、请求体这三块。
1.2 request对象的三大核心数据入口
打开Django源码,WSGIRequest类的核心属性并不多,但对于日常开发来说,最有价值的是下面这三个数据入口。
第一块:request.method。这是一个字符串,取值一般是GET、POST、PUT、DELETE、PATCH这些HTTP方法。它不是给你看的摆设,而是判断请求类型的开关。很多新手一上来就写request.POST.get('xxx'),结果前端发的是GET请求,后端什么都没取到,原因就是没先判断method。
第二块:request.GET和request.POST。这两个是QueryDict对象(后面细讲),分别对应查询字符串里的参数和表单格式提交的请求体参数。它们是最常用的取参入口,百分之八十的“前端传参、后端接收”问题都发生在这两个对象上。
第三块:request.body和request.headers。request.body是原始请求体,类型是bytes(字节串),前端如果用JSON格式提交数据,参数就在这里,需要用json.loads()解析。request.headers是请求头,是一个大小写不敏感的类字典结构,自定义头部参数都从这里面取。
搞清楚这三块,就建立起了最基本的认知框架。接下来要做的,是把每种传参方式对应到具体的代码写法上。
2. 核心细节解析:前端传参的多种姿势与后端对应接收方式
2.1 查询字符串传参:新手最熟悉的GET参数
查询字符串就是URL里?后面的那段,比如/users/?page=2&size=10。前端传参时往往直接把参数拼在URL上,这也是浏览器地址栏唯一能直接传参的方式,适合非敏感数据。
后端接收用request.GET:
def user_list(request): page = request.GET.get('page', 1) # 默认值1,前端没传时兜底 size = request.GET.get('size', 10) keyword = request.GET.get('keyword', '') # 后续用这几个参数做分页、搜索...这里必须强调QueryDict的两个特性,否则你一定会踩坑。
特性一:它是不可变对象。这意味着你不能直接执行request.GET['page'] = '3'这种操作,会报错。如果需要修改,必须复制一份:data = request.GET.copy()。
特性二:它天生支持一个键对应多个值。比如/search/?tag=python&tag=django,同一个tag出现了两次。普通字典会丢字段,但QueryDict会把它们都存起来。取的时候有讲究:
tag = request.GET.get('tag') # 只取最后一个值,结果是'django' tags = request.GET.getlist('tag') # 取全部值,结果是['python', 'django']这个多值特性经常被忽略,等到前端真的传了重复参数,后端才发现问题。
2.2 请求体传参:表单格式与JSON格式的接收差异
POST请求的参数一般放在请求体里,但请求体的格式不是固定的,这是很多新手最容易混乱的地方。主要有两种:表单格式(application/x-www-form-urlencoded或multipart/form-data)和JSON格式(application/json)。
表单格式传参时,Django已经帮你解析好了,直接用的还是request.POST:
def login(request): if request.method == 'POST': username = request.POST.get('username') password = request.POST.get('password') # 处理登录逻辑...JSON格式传参时,request.POST是空的,因为Django默认不解析JSON请求体。你需要从原始字节里自己取:
import json def create_user(request): if request.method == 'POST': # request.body是bytes类型 data = json.loads(request.body) name = data.get('name') age = data.get('age')这里有个细节值得注意:json.loads()要求传入合法的JSON字符串,所以request.body需要先解码。在Python 3里,bytes类型可以直接传给json.loads(),它会自动按UTF-8解码,所以不用手动调.decode('utf-8')。但如果请求体本身不是JSON格式,比如空字符串或者纯文本,json.loads()会直接抛json.JSONDecodeError,这个异常后面再说怎么处理。
判断前端到底传的是哪种格式,最可靠的方法是看request.content_type:
if request.content_type == 'application/json': data = json.loads(request.body) else: data = request.POST很多项目前端用的Axios默认发的是JSON格式,如果后端还傻傻地用request.POST.get(),拿到的一律是None。这就是“传了却接不住”的经典场景。
2.3 URL路径参数与请求头参数:容易被忽略的传参渠道
除了查询字符串和请求体,还有两种传参渠道分别是路径参数和请求头参数。
路径参数是定义在URL路由规则里的,比如/users/<int:pk>/,这个pk会作为关键字参数直接传给视图函数,不需要通过request取:
# urls.py path('users/<int:pk>/', user_detail) # views.py def user_detail(request, pk): # 直接用pk这个变量 user = get_object_or_404(User, pk=pk)这种方式适合标识资源ID,比如"查看第几篇文章""删除第几条记录"。
请求头参数则用于传递元信息,最常见的场景是认证token。前端在Header里带上Authorization: Bearer <token>,后端接收时这样取:
import re def auth_view(request): auth_header = request.headers.get('Authorization', '') # 解析Bearer token if auth_header.startswith('Bearer '): token = auth_header[7:] # 校验token...request.headers是Django 2.2版本之后引入的,用起来比request.META.get('HTTP_AUTHORIZATION')直观得多。但要注意,自定义的Header头在HTTP协议层面会被翻译成HTTP_开头的META键,比如X-Custom-Header对应HTTP_X_CUSTOM_HEADER。用request.headers则不需要担心这个转化问题,直接写原始的名字就行。
3. 实操过程:从零搭建一个完整的传参接收示例
3.1 准备项目环境与基础视图
光说不练假把式。我创建一个演示项目,把前面几种方式串起来跑一遍。先做好环境准备:
# 创建虚拟环境并激活 python -m venv venv source venv/bin/activate # Windows下用 venv\Scripts\activate # 安装Django并创建项目 pip install django django-admin startproject request_demo cd request_demo python manage.py startapp myapp记得在settings.py的INSTALLED_APPS里注册myapp。然后打开urls.py,设计一组能覆盖各种传参场景的URL:
# request_demo/urls.py from django.contrib import admin from django.urls import path from myapp import views urlpatterns = [ path('admin/', admin.site.urls), path('articles/', views.article_list), # 查询字符串+表单 path('articles/<int:pk>/', views.article_detail), # 路径参数 path('articles/json/', views.article_create_json), # JSON请求体 ]3.2 视图函数完整实现与前端对应写法
先写查询字符串和表单处理的视图:
# myapp/views.py import json from django.http import JsonResponse def article_list(request): # 无论GET还是POST,都尝试从两个入口取参 page = request.GET.get('page', 1) size = request.GET.get('size', 10) if request.method == 'POST': title = request.POST.get('title') content = request.POST.get('content') if title and content: # 模拟保存文章 result = {'status': 'created', 'title': title, 'page': page, 'size': size} return JsonResponse(result, status=201) # GET请求返回问答列表数据 result = {'status': 'ok', 'page': page, 'size': size, 'items': [{'id': 1, 'title': 'Django request对象实践'}]} return JsonResponse(result)注意这里我同时用了request.GET和request.POST。实际开发中,POST请求的URL上也经常带查询字符串,两者可以并存,各自取各自的,互不影响。
接着是JSON请求体的视图:
def article_create_json(request): if request.method != 'POST': return JsonResponse({'error': 'only POST allowed'}, status=405) try: data = json.loads(request.body or b'{}') except json.JSONDecodeError: return JsonResponse({'error': 'invalid JSON body'}, status=400) title = data.get('title') content = data.get('content') if not title or not content: return JsonResponse({'error': 'title and content are required'}, status=422) # 模拟保存 return JsonResponse({'status': 'created', 'title': title, 'content': content}, status=201)这里request.body or b'{}'的写法是个小技巧,避免前端传空请求体时json.loads(b'')抛异常。
路径参数的视图就简单了:
def article_detail(request, pk): # 从Header里取用户标识,演示header传参 user_agent = request.headers.get('User-Agent', 'unknown') token = request.headers.get('X-Auth-Token', 'anonymous') # 模拟按pk查询文章 article = {'id': pk, 'title': f'文章{pk}', 'content': '这是正文'} return JsonResponse({'article': article, 'user_agent': user_agent, 'token_status': 'authenticated' if token != 'anonymous' else 'missing'})3.3 前端传参的几种实际写法
作为后端开发者,我习惯用Python的requests库模拟前端请求来验证接口。你完全可以用Postman、Apifox或者浏览器来测试。下面是三种典型传参方式的模拟代码:
import requests # 1. 查询字符串传参(GET) resp = requests.get('http://127.0.0.1:8000/articles/?page=2&size=20') print(resp.json()) # 输出: {'status': 'ok', 'page': '2', 'size': '20', ...} # 2. 表单格式传参(POST) resp = requests.post('http://127.0.0.1:8000/articles/', data={'title': '笔记', 'content': '内容'}) print(resp.json()) # 输出: {'status': 'created', 'title': '笔记', ...} # 3. JSON格式传参(POST) resp = requests.post('http://127.0.0.1:8000/articles/json/', json={'title': 'JSON笔记', 'content': '用json传的'}) print(resp.json()) # 输出: {'status': 'created', 'title': 'JSON笔记', ...}用JavaScript的fetch来写前端,则是这个样子:
// 查询字符串 fetch('/articles/?page=3&size=15') .then(res => res.json()) .then(data => console.log(data)); // JSON请求体 fetch('/articles/json/', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({title: '前端标题', content: '前端内容'}) }) .then(res => res.json()) .then(data => console.log(data));这里要特别提醒一点:浏览器环境里,Content-Type: application/json的POST请求会触发CORS预检(如果是跨域请求),所以生产环境记得配好CORS中间件。同源开发下则无所谓。
3.4 参数校验:别让脏数据直接进数据库
新手最容易忽略的是参数校验。request.GET.get('page', 1)拿到的page是字符串'abc',如果直接拿去算分页偏移量,程序运行到一半就会炸。
推荐的校验姿势有两种。一种是自己写简单判断:
def to_int(value, default=1): try: return int(value) except (TypeError, ValueError): return default page = to_int(request.GET.get('page'), 1)另一种是借助Django表单(Form)或者DRF(Django REST Framework)的序列化器。以我个人的项目经验来说,接口数量少时手写校验完全够用;接口一旦多起来,还是上DRF更省心,它的ModelSerializer自带类型转换、必填校验和错误提示,能省掉大量重复代码。不过那是进阶话题,这篇先不展开。
4. 常见问题与排查技巧实录
4.1 参数获取不到的典型场景排查
我在学习和带新人的过程中,发现“前端传了参数但后端取不到”这个问题反复出现。下面这张表基本覆盖了所有常见原因:
| 现象 | 大概率原因 | 排查方式 |
|---|---|---|
| POST表单数据取不到 | 前端实际发的是JSON格式 | 打印request.content_type确认 |
| JSON数据取不到 | request.POST里当然没有 | 改用json.loads(request.body) |
| GET参数全是None | 参数名拼写不一致 | 打印request.GET.keys()对照 |
| 中文参数乱码 | 没做URL编码或编码格式不对 | 检查前端encodeURIComponent |
| 同一个key只想取第一个值 | 用了get()但实际返回最后一个 | 改用getlist()并明确取值方式 |
| 发送了请求但视图没执行 | URL路由没匹配上 | 检查path()规则和末尾斜杠 |
排查这类问题,第一个动作就是打印。在视图函数里加一行print(request.META.get('CONTENT_TYPE')),或者在中间件里临时打印request.body,比盲猜高效得多。我的习惯是写一个简单的请求日志中间件,在DEBUG模式下打印每个请求的方法、路径、content_type和body片段,排查问题事半功倍。
4.2 编码问题与中文乱码的解决
中文传参乱码是一个高频坑。前端在URL里直接拼中文参数:
/articles/?keyword=深度学习浏览器一般会自动做URL编码,但有些自定义的HTTP客户端不会。如果实际传过去的是未编码的UTF-8字节,后端request.GET.get('keyword')拿到的那串字符串在打印时就会出现乱码或报错。
解决办法是让前端统一用encodeURIComponent或requests的params参数(它会自动编码):
// 前端安全写法 const keyword = encodeURIComponent('深度学习'); fetch(`/articles/?keyword=${keyword}`);# Python requests库安全写法 resp = requests.get('http://127.0.0.1:8000/articles/', params={'keyword': '深度学习'})如果遇到后端取到的值已经是乱码,可以尝试手动解码:
from urllib.parse import unquote keyword = unquote(request.GET.get('keyword', ''))注意这个unquote处理的是百分号编码的字符串,如果前端压根没编码则处理不了。所以根治方法还是在传参源头处理。
4.3 CSRF校验的坑与解决方案
写POST、PUT等修改类接口时,Django默认开启CSRF校验。前端用表单提交时,模板里需要加{% csrf_token %}。但如果前端是JavaScript的fetch或者用Postman调试,不带CSRF token的请求会被403拦截。
处理方式取决于项目形态:
- 页面直接渲染Django模板的项目:在表单里加
{% csrf_token %},fetch请求需要先从cookie取出csrftoken值,再放到请求头X-CSRFToken里。 - 前后端分离的API项目:大多数情况下接口不需要CSRF保护(因为不依赖cookie会话),在视图上添加
@csrf_exempt装饰器即可:
from django.views.decorators.csrf import csrf_exempt @csrf_exempt def article_create_json(request): # ...但注意,如果项目已经把django.contrib.auth登录逻辑暴露成JSON接口,应当考虑用DRF的SessionAuthentication配合CSRF策略,而不是图省事把整个视图的CSRF检查关掉。安全底线不能放松。
4.4 请求体过大的处理
用json.loads(request.body)时,如果前端传了一个几十MB的JSON,不仅解析慢,还可能拖垮服务。Django对请求体默认没有硬性大小限制,但中间件(如Gunicorn配置)可能有。我建议在后端入口做一层校验:
if len(request.body) > 1024 * 1024: # 超过1MB拒绝 return JsonResponse({'error': '请求体过大'}, status=413)前端也会报413 Request Entity Too Large,这种错误在后端排查时往往因为刚接手老项目才会意识到是限制问题。实战中还有过因为这个错误把锅甩给Django的,后来发现是Nginx的client_max_body_size默认1MB挡掉的——反向代理前端的请求,这层限制比Django本身的限制更常踩。
5. 进阶实践:把request对象用得更优雅
5.1 统一封装请求参数获取方法
项目里的接口一旦多起来,每次都要判断request.method、解析request.body,代码会很零散。我习惯封装一个小的工具函数:
import json def get_request_data(request): """统一获取请求参数,兼容GET和POST、JSON和表单""" if request.method == 'GET': return request.GET.dict(), False if request.content_type == 'application/json': try: return json.loads(request.body or b'{}'), True except json.JSONDecodeError: return {}, False return request.POST.dict(), False注意这里request.GET.dict()会把多值参数直接截断,只保留最后一项。如果确实需要处理多值参数,就用request.GET.getlist()单独处理,不要走这个封装。
封装之后,视图函数的开头就变成一行:
def article_list(request): params, is_json = get_request_data(request) page = params.get('page', 1) title = params.get('title', '') # ...5.2 request对象在日志与调试中的实战用法
把request对象的信息记录下来,对排查线上问题帮助巨大。我推荐在中间件层记录如下几个字段:
# myapp/middleware.py import time import json class RequestLogMiddleware: def __init__(self, get_response): self.get_response = get_response def __call__(self, request): start_time = time.time() response = self.get_response(request) # 收集关键信息 log_data = { 'method': request.method, 'path': request.path, 'status': response.status_code, 'duration_ms': round((time.time() - start_time) * 1000, 2), 'content_type': request.content_type, 'user': str(request.user) if hasattr(request, 'user') else 'anonymous', } # 记录请求体(注意脱敏) if request.body and len(request.body) < 2048: try: body_data = json.loads(request.body) if 'password' in body_data: body_data['password'] = '***' log_data['body'] = body_data except (ValueError, TypeError): log_data['body_raw'] = request.body[:200].decode('utf-8', errors='replace') print(json.dumps(log_data, ensure_ascii=False)) return response有了这个中间件,前端传参的任何问题都会在日志里留下痕迹。尤其是生产环境排查“某个接口收到奇怪参数”的case,这份日志能直接还原事故现场。
5.3 从request到DRF:为什么说理解request是基础
如果你后续计划使用Django REST Framework,会发现DRF对request做了进一步封装,变成了Request对象。它保留了.method、.path这些属性,但把.GET、.POST换成了更灵活的.query_params和.data:
# DRF视图示例 from rest_framework.views import APIView from rest_framework.response import Response class ArticleList(APIView): def get(self, request): page = request.query_params.get('page', 1) return Response({'page': page}) def post(self, request): title = request.data.get('title') return Response({'title': title})可以明显看到,DRF把“前端到底传的JSON还是表单”这个差异抹平了:request.data会自动解析JSON表单、multipart等格式,不需要你再手动判断content_type和调用json.loads。但不管你用不用DRF,对原生request对象的那套理解仍然是地基——因为DRF的Request对象本质上就是在原生request之上做了一层包装,很多自定义逻辑仍然需要操作底层的request._request。
6. 写在最后的几点提醒
学习Django的request对象,我个人最大的体会是不要死记API,而是先在脑子里建立“HTTP请求长什么样”的画面。你一旦理解了HTTP报文的结构,Django的request只不过是把那层报文结构翻译成了Python的字典和对象而已。
再说一个提升效率的小技巧:调试接口时,给项目加一个“请求回显”的临时接口,直接把这个request的各项信息打印出来,比每次靠猜测来得快:
def echo_request(request): """调试用:把请求的所有信息原样返回""" return JsonResponse({ 'method': request.method, 'path': request.path, 'GET': dict(request.GET), 'POST': dict(request.POST), 'headers': dict(request.headers), 'body': request.body.decode('utf-8', errors='replace')[:500], 'content_type': request.content_type, })把这个接口挂到本地路由上,前端同事调试起来也轻松,大家互相之间少扯皮。这个法子我在几个项目里都用过,反响很好。
如果这篇文章对你有帮助,我的建议是不要停留在看,赶紧打开自己的Django项目,把涉及到request对象的代码全部翻出来按今天说的分类方式重新过一遍。理解了GET和POST参数去哪里取、JSON请求体怎么处理、Header参数从哪里读,你就已经跨过了Django入门阶段一个不小的坎。