Django 响应对象、文件上传与类视图
视图的职责是接收 HttpRequest、执行业务逻辑,并返回 HttpResponse。本章介绍文本、模板、重定向和 JSON 响应的选择方式,说明表单文件上传如何进入 request.FILES,再通过函数视图(FBV)与类视图(CBV)理解 Django 如何按 HTTP 方法分发请求。
一、视图常用响应方式
Django 中最常用的三个响应工具是 HttpResponse、render() 和 redirect():
| 工具 | 返回内容 | 常见场景 |
|---|---|---|
HttpResponse |
原始文本、字节数据或自定义内容类型 | 简单文本、CSV、手动构造响应 |
render() |
渲染后的 HTML 响应 | 返回模板页面 |
redirect() |
3xx 重定向响应 | 提交成功后跳转、根据路由名跳转 |
HttpResponse:返回文本或自定义内容
HttpResponse 的内容应是字符串或字节。字典不是 JSON,不能直接作为业务数据返回;若要返回 JSON,应先序列化,或直接使用更合适的 JsonResponse。
python
from django.http import HttpResponse
def health_check(request):
return HttpResponse("ok", content_type="text/plain; charset=utf-8")
def download_csv(request):
content = "name,age\ndream,18\n"
response = HttpResponse(content, content_type="text/csv; charset=utf-8")
response["Content-Disposition"] = 'attachment; filename="students.csv"'
return response
代码说明:content_type 告诉浏览器如何解释响应内容。返回 HTML 常用 text/html,返回纯文本常用 text/plain,下载文件时可配合 Content-Disposition 指示浏览器作为附件处理。
render:渲染模板页面
render(request, template_name, context=None) 会将上下文字典传给模板并返回 HTML 响应。应显式传递模板真正需要的数据,避免把 locals() 中的所有局部变量意外暴露到模板。
python
from django.shortcuts import render
def profile(request):
user_info = {"name": "dream", "age": 18}
page_title = "个人资料"
return render(request, "user/profile.html", {
"user_info": user_info,
"page_title": page_title,
})
html
<!-- user/templates/user/profile.html -->
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<title>{{ page_title }}</title>
</head>
<body>
<h1>{{ user_info.name }}</h1>
<p>年龄:{{ user_info.age }}</p>
</body>
</html>
代码说明:render() 的第三个参数是上下文(context)。模板变量默认会进行 HTML 转义,通常不应对不可信内容使用 |safe 或直接输出为 HTML。
redirect:返回跳转响应
redirect() 返回一个重定向响应,默认状态码为 302。优先传入命名路由而不是硬编码 URL,以减少路由变动带来的修改成本。
python
from django.shortcuts import redirect
def after_login(request):
return redirect("user:profile", user_id=1)
代码说明:浏览器收到 302 响应后会再请求目标地址,因此重定向不是"在服务器内部调用另一个视图函数"。处理 POST 提交后常采用"POST-Redirect-GET"模式,避免用户刷新页面时重复提交表单。
二、返回 JSON:JsonResponse 优先
JSON 是接口传输的文本格式,Python 字典不是 JSON 字符串。可以手动调用 json.dumps() 后交给 HttpResponse,但 Django 的 JsonResponse 会自动序列化数据并设置正确的 Content-Type,通常更合适。
python
import json
from django.http import HttpResponse, JsonResponse
def manual_json(request):
data = {"name": "dream", "age": 18}
content = json.dumps(data, ensure_ascii=False)
return HttpResponse(content, content_type="application/json; charset=utf-8")
def json_response(request):
data = {"name": "dream大哥", "age": 18}
return JsonResponse(data, json_dumps_params={"ensure_ascii": False})
代码说明:ensure_ascii=True(默认值)会把非 ASCII 字符转义为 \uXXXX 形式,这仍是合法 JSON;ensure_ascii=False 则会直接保留中文字符,便于人工阅读。两种写法在浏览器解析后的数据含义相同。
返回列表和其他非字典数据
出于历史安全考虑,JsonResponse 默认只接受字典。若明确需要返回列表,必须传入 safe=False;返回前仍应确认数据中没有敏感字段。
python
from django.http import JsonResponse
def student_list(request):
data = [
{"id": 1, "name": "dream"},
{"id": 2, "name": "hope"},
]
return JsonResponse(data, safe=False, json_dumps_params={"ensure_ascii": False})
代码说明:JsonResponse 使用 Django 的 DjangoJSONEncoder,能处理 datetime、Decimal、UUID 等常见类型。模型实例和 QuerySet 不能直接作为 JSON 返回,应先挑选需要的字段并转换为基础类型。
python
from django.http import JsonResponse
from .models import Student
def students_api(request):
students = Student.objects.values("id", "name", "age")
return JsonResponse({"results": list(students)})
三、表单提交与文件上传
普通 HTML 表单默认使用 application/x-www-form-urlencoded 编码。该编码适合文本字段,文件输入只会传递文件名等文本信息,无法把文件二进制内容作为上传文件交给 Django。
要上传文件,表单必须同时满足两个条件:使用 method="post",并设置 enctype="multipart/form-data"。此外,Django 的 POST 表单应包含 {% csrf_token %}。
html
<form method="post" enctype="multipart/form-data">
{% csrf_token %}
<label>
用户名
<input type="text" name="username" required>
</label>
<label>
头像
<input type="file" name="avatar" accept="image/*">
</label>
<fieldset>
<legend>爱好</legend>
<label><input type="checkbox" name="hobby" value="music">音乐</label>
<label><input type="checkbox" name="hobby" value="reading">阅读</label>
</fieldset>
<fieldset>
<legend>性别</legend>
<label><input type="radio" name="gender" value="male">男</label>
<label><input type="radio" name="gender" value="female">女</label>
</fieldset>
<button type="submit">提交</button>
</form>
代码说明:accept="image/*" 只是一种浏览器端提示,不能替代服务端文件类型校验。多个复选框使用同一个 name,因此后端需要用 getlist() 获取全部值。
在视图中读取表单和文件数据
文本字段在 request.POST,上传文件在 request.FILES。当未选择文件或字段名不匹配时,request.FILES.get() 返回 None,必须先处理这个情况。
python
from django.http import HttpResponseBadRequest
from django.shortcuts import redirect, render
def register(request):
if request.method == "GET":
return render(request, "user/register.html")
username = request.POST.get("username", "").strip()
hobbies = request.POST.getlist("hobby")
gender = request.POST.get("gender", "")
avatar = request.FILES.get("avatar")
if not username:
return HttpResponseBadRequest("用户名不能为空")
if avatar is None:
return HttpResponseBadRequest("请选择头像")
# 保存前还需校验文件大小、真实类型和权限。
print(username, hobbies, gender, avatar.name, avatar.size)
return redirect("user:register-success")
代码说明:不要读取或打印密码等敏感数据。上传对象可能是 InMemoryUploadedFile 或 TemporaryUploadedFile,取决于文件大小和 Django 的上传处理配置;业务代码只需按 UploadedFile 的公共 API 使用它。
使用存储系统保存上传文件
不应直接将用户提供的文件名拼接到本地路径中,否则可能造成路径穿越、重名覆盖等问题。Django 的 FileSystemStorage 会使用存储后端管理文件名和目录,实际项目还可替换为对象存储。
python
from pathlib import Path
from django.conf import settings
from django.core.files.storage import FileSystemStorage
def save_avatar(uploaded_file):
storage = FileSystemStorage(location=Path(settings.MEDIA_ROOT) / "avatars")
saved_name = storage.save(uploaded_file.name, uploaded_file)
return storage.url(saved_name)
python
# settings.py(开发环境示例)
MEDIA_URL = "/media/"
MEDIA_ROOT = BASE_DIR / "media"
代码说明:生产环境应将用户上传内容存放在受控目录或对象存储,并限制文件大小、扩展名、MIME 类型和内容。不要把用户上传文件写入 static/;静态资源通常由部署时收集,上传文件属于媒体资源(media)。
四、常用 request 属性
request 是 Django 封装的 HttpRequest 对象,包含请求方法、路径、查询参数、表单数据、上传文件、请求头和用户会话等信息。不同属性代表不同来源的数据,不能互相替代。
| 属性或方法 | 作用 |
|---|---|
request.method |
请求方法,如 GET、POST |
request.GET |
URL 查询字符串参数 |
request.POST |
表单编码的 POST 数据,不含文件 |
request.FILES |
multipart/form-data 上传的文件 |
request.body |
原始请求体字节,例如 JSON API 请求体 |
request.headers |
请求头的映射对象 |
request.COOKIES |
浏览器发送的 Cookie |
request.path |
不含查询参数的路径 |
request.get_full_path() |
包含查询字符串的完整路径 |
request.user |
认证中间件提供的当前用户 |
python
import json
from django.http import JsonResponse
def request_info(request):
if request.method == "POST" and request.content_type == "application/json":
try:
payload = json.loads(request.body.decode("utf-8"))
except (UnicodeDecodeError, json.JSONDecodeError):
return JsonResponse({"detail": "JSON 格式错误"}, status=400)
return JsonResponse({"received": payload})
return JsonResponse({
"method": request.method,
"path": request.path,
"full_path": request.get_full_path(),
"query": request.GET.dict(),
"user_agent": request.headers.get("User-Agent", ""),
})
代码说明:request.GET 和 request.POST 是 QueryDict,同一键可能有多个值。request.GET.dict() 只保留每个键的最后一个值,处理复选框、多选项等重复字段时仍应使用 getlist()。request.body 是原始字节,适用于 JSON 等非表单请求体。
五、FBV 与 CBV
函数视图(FBV,Function-Based View)用一个函数处理请求;类视图(CBV,Class-Based View)用一个类的不同方法处理不同 HTTP 方法。两者都能完成相同业务,选择重点是复杂度与复用需求,而不是谁"更高级"。
函数视图(FBV)
python
from django.http import HttpResponseNotAllowed
from django.shortcuts import render
def login(request):
if request.method == "GET":
return render(request, "user/login.html")
if request.method == "POST":
username = request.POST.get("username", "").strip()
return render(request, "user/login_success.html", {"username": username})
return HttpResponseNotAllowed(["GET", "POST"])
代码说明:FBV 直观且适合简单流程。请求方法增多、多个页面共享逻辑,或需要复用 mixin 时,CBV 往往更便于组织。
类视图(CBV)
CBV 继承 django.views.View,将 GET、POST 等 HTTP 方法分别实现为同名小写方法。注册路由时必须调用 .as_view(),它会把类转换为 Django 可调用的视图函数。
python
# user/views.py
from django.http import HttpResponseBadRequest
from django.shortcuts import redirect, render
from django.views import View
class LoginView(View):
template_name = "user/login.html"
def get(self, request, *args, **kwargs):
return render(request, self.template_name)
def post(self, request, *args, **kwargs):
username = request.POST.get("username", "").strip()
if not username:
return HttpResponseBadRequest("用户名不能为空")
# 完成认证后,使用 PRG 模式跳转。
return redirect("user:profile", user_id=1)
python
# user/urls.py
from django.urls import path
from .views import LoginView
urlpatterns = [
path("login/", LoginView.as_view(), name="login"),
]
代码说明:每个请求都会创建一个新的 LoginView 实例,因此不要把当前用户、表单数据等可变请求状态放在类属性中。类属性适合保存模板名、固定配置等不随请求改变的数据。
六、CBV 的方法分发机制
as_view() 的主要工作是返回一个普通可调用对象,使 URL 路由可以像使用函数视图一样调用它。收到请求后,Django 大致会执行以下过程:
text
path(..., LoginView.as_view())
-> as_view() 返回 view 函数
-> 每次请求创建 LoginView 实例
-> setup() 保存 request、args、kwargs
-> dispatch() 根据 request.method 查找 get/post/... 方法
-> 调用对应处理方法并返回 HttpResponse
可以通过重写 dispatch() 为整个类视图添加统一逻辑,例如权限检查;重写时必须调用 super().dispatch(),否则后续的 HTTP 方法分发不会执行。
python
from django.http import HttpResponseForbidden
from django.views import View
class StaffOnlyView(View):
def dispatch(self, request, *args, **kwargs):
if not request.user.is_authenticated or not request.user.is_staff:
return HttpResponseForbidden("无权访问")
return super().dispatch(request, *args, **kwargs)
def get(self, request, *args, **kwargs):
return render(request, "staff/dashboard.html")
代码说明:若请求方法不在 http_method_names 中,或类中没有实现对应方法,View.dispatch() 会返回 405 Method Not Allowed。Django 还会自动用 get() 处理 HEAD 请求(当未显式定义 head() 时)。
七、实践要点
- 页面响应使用
render(),跳转使用redirect(),JSON 接口优先使用JsonResponse。 - 对外返回的数据必须经过筛选,不能直接序列化模型全部字段或用户隐私数据。
- 文件上传需要
multipart/form-data,文本数据从POST读取,文件从FILES读取。 - 上传文件必须进行服务端校验,并使用媒体存储系统保存,不要拼接用户提供的路径或写入
static/。 - 简单逻辑使用 FBV;需要按方法拆分、复用 mixin 或统一权限控制时使用 CBV。
掌握这些响应与视图组织方式后,就可以安全地处理模板页面、JSON 接口和表单上传等常见 Web 请求。