写Django后端有一段时间了,从最早用函数视图拼HTML,到后来用类视图、DRF写接口,模板语法和请求响应这三块一直没绕开过。“模板语法、请求与响应”听起来像是Django入门三件套,但真正把这三样吃透,前端后端协作起来会顺畅很多。尤其现在全栈项目里前端可能是Vue、可能是Uniapp、也可能是小程序,Django往往充当的不只是渲染页面的人,还得是提供数据接口的人。这篇文章就把这三块掰开揉碎,讲清楚它们底层的逻辑,再配合一个完整的图书管理小例子,把从请求进来、视图处理、模板渲染、响应返回的完整链路走一遍。
1. 请求到达Django之后发生了什么
先花点时间把请求的生命周期讲明白。很多新手学Django,上来就写视图函数,知道def index(request)里面能拿到request.method,知道返回render还是JsonResponse,但不太清楚请求从浏览器发出来之后,Django内部是怎么一步步处理的。这块理解了,后面排错会省很多事。
1.1 一次HTTP请求的完整链路
用户在浏览器里输入网址按下回车,浏览器会构造一个HTTP请求,带上请求行(方法、路径、协议版本)、请求头(Host、User-Agent、Cookie、Content-Type等)、请求体(GET请求一般是空的,POST请求一般有表单数据或JSON),然后通过网络发到服务器。服务器这边,跑的是WSGI服务器(常见的有Gunicorn、uWSGI,开发时用Django自带的runserver),WSGI服务器把HTTP请求解析成一个Python层面的environ字典,再交给Django的WSGIHandler去处理。
Django内部拿到这个请求之后,会先经过中间件链。中间件的作用有点像安检闸门,SecurityMiddleware做安全头处理,SessionMiddleware处理会话,CsrfViewMiddleware检查CSRF令牌,这些都是在这个环节做的。过了中间件,请求就到了URLconf路由层,Django拿请求路径去和urlpatterns里的规则做匹配,匹配成功就调用对应的视图函数,匹配失败就返回404。
视图函数是这个链路的核心处理单元,它接收到一个HttpRequest对象,经过逻辑处理之后,必须返回一个HttpResponse对象。这个响应对象再原路返回经过中间件,最终由WSGI服务器写回给浏览器。
1.2 request对象里到底藏了哪些信息
视图函数签名def some_view(request)里的request,就是django.http.HttpRequest的实例。它承载了这次请求的所有信息。我用得最多的几个属性和方法:
request.method:字符串,取值一般是GET、POST、PUT、DELETE等。这是判断请求方式的入口,也是后面写增删改查接口第一个要判断的东西。request.GET:QueryDict对象,保存URL查询参数。比如/books/?page=2&size=10,request.GET.get('page')就是'2'。注意它是QueryDict不是普通字典,调用.get()是安全的,键不存在返回None,不会抛KeyError。request.POST:QueryDict对象,保存表单POST数据。前端用axios默认的application/x-www-form-urlencoded编码发送数据,后端能通过request.POST.get('name')直接拿到。但要明确一点:如果前端用application/json发请求体,request.POST里是拿不到数据的,得从request.body里取JSON再解析。request.body:原始请求体字节串。前端发JSON、发文件、发任意自定义格式的请求体,都能在body里拿到原始内容。配合json.loads(request.body)就能拿到Python字典。request.headers:请求头字典,拿User-Agent、Authorization、X-CSRFToken之类的都从这取。request.META:一个更大的字典,包含所有的HTTP头、服务器环境变量等基本信息。request.META['REMOTE_ADDR']能拿到客户端IP。request.FILES:文件上传时用,保存上传的文件对象。前端用multipart/form-data格式上传文件时,文件数据在FILES里,其他字段在POST里。request.session:经过SessionMiddleware处理后才有,用于服务端会话数据管理。request.user:经过AuthenticationMiddleware处理后才有,是当前登录用户对象。没登录一般是匿名用户AnonymousUser。
1.3 GET与POST:前后端联调最常见的两种请求
全栈项目里,GET和POST是用的最多的两种请求方式。GET一般用于读取数据,POST用于提交数据。语义上的区别很重要,但很多初学者容易忽略。
GET请求的参数是拼在URL后面的,也就是查询字符串。Django后端用request.GET.get()获取。前端用axios.get('/api/books/', { params: { page: 1 } })发送,最终请求URL是/api/books/?page=1。要注意:GET请求不应该用来做有副作用的操作,比如删除一条数据用GET也能做,但语义不对,浏览器预取、爬虫都可能导致误操作。我一般约定:查询用GET,新增用POST,修改用PUT/PATCH,删除用DELETE。前后端都要遵守这个约定。
POST请求的参数放在请求体里。这里有个前端开发者容易踩的坑:POST数据,编码格式到底是什么。前端如果写的是:
axios.post('/api/books/', { name: 'Django入门', price: 59.9 })axios默认会把这个JavaScript对象序列化成JSON字符串,请求头里的Content-Type是application/json。这种情况下,Django的request.POST是空的,数据全在request.body里。你得这样处理:
import json def create_book(request): if request.method == 'POST': data = json.loads(request.body) name = data.get('name') price = data.get('price')如果前端是用表单提交的,比如:
const formData = new URLSearchParams(); formData.append('name', 'Django入门'); formData.append('price', '59.9'); axios.post('/api/books/', formData)这就是application/x-www-form-urlencoded编码,Django后端直接用request.POST.get('name')就能拿到。
如果前端用的是FormData对象(比如有文件上传的场景),编码方式是multipart/form-data,Django后端用request.POST.get()取普通字段,request.FILES.get()取文件。这三种情况我在项目里都碰到过,写接口前先确认前端发的什么编码格式,能少走很多弯路。
2. 响应返回:不只是return一个字符串那么简单
视图函数的最终使命是返回一个HttpResponse对象。新手常见的误区是视图里直接return 'hello',Django会报错,因为必须返回的是一个响应对象。Django封装了一些快捷函数,用起来很方便,但理解它们底层做了什么很重要。
2.1 HttpResponse、render、redirect、JsonResponse怎么选
HttpResponse是最基础的响应类,直接传入字符串内容就行。可以设置状态码、设置响应头。
from django.http import HttpResponse def plain_text(request): resp = HttpResponse("<h1>Hello Django</h1>") resp['X-Powered-By'] = 'DjangoBlog' return resprender是渲染模板最常用的快捷方式。它的内部逻辑是:把请求对象和模板上下文传给模板引擎,模板渲染成字符串,包装成HttpResponse返回。
from django.shortcuts import render def book_list(request): books = Book.objects.all() return render(request, 'book/list.html', {'books': books})redirect用于重定向,返回302状态码,浏览器收到之后会重新请求Location头的地址。
from django.shortcuts import redirect def after_create(request): return redirect('/books/')JsonResponse是Django 1.7引入的一个便捷类,专门用来返回JSON数据。它继承自HttpResponse,把数据序列化成JSON字符串,并且把Content-Type设置为application/json。
from django.http import JsonResponse def book_detail(request, book_id): book = Book.objects.get(pk=book_id) return JsonResponse({ 'id': book.id, 'name': book.name, 'price': book.price, })这里有个很重要但很多新手不知道的细节:JsonResponse默认只支持字典对象。如果你想返回列表,需要设置safe=False:
def book_list_api(request): books = Book.objects.values('id', 'name', 'price') return JsonResponse(list(books), safe=False)不设置的话,直接传列表会抛TypeError: In order to allow non-dict objects to be serialized set the safe parameter to False。
中文编码是另一个容易踩的坑。JsonResponse默认把非ASCII字符转成\uXXXX格式,如果想让前端直接看到中文字符,要设置json_dumps_params={'ensure_ascii': False}:
return JsonResponse({'name': 'Django入门'}, json_dumps_params={'ensure_ascii': False})2.2 状态码的语义:后端要主动给前端准确反馈
写接口时状态码的选择是一个约定问题。HTTP状态码在语义上已经有约定,前后端联调时应该遵守,不要自己发明。
| 状态码 | 含义 | 使用场景 |
|---|---|---|
| 200 | 请求成功 | GET查询正常返回 |
| 201 | 资源创建成功 | POST新增数据成功 |
| 204 | 无内容 | 操作成功但不需要返回内容,比如DELETE |
| 301 | 永久重定向 | 页面永久迁移 |
| 302 | 临时重定向 | 登录后跳转 |
| 400 | 请求参数错误 | 缺少必填字段、参数格式不对 |
| 401 | 未认证 | 没登录或Token失效 |
| 403 | 无权限 | 已登录但没有访问权限 |
| 404 | 资源不存在 | URL错误或记录不存在 |
| 500 | 服务器内部错误 | 代码异常 |
我习惯在API里用一个统一的结构体包装数据,比如成功时返回{"code": 0, "message": "success", "data": {...}},失败时返回{"code": 40001, "message": "参数错误", "data": null}。HTTP状态码和业务码并存,前端可以基于HTTP状态码做统一的错误拦截,再基于业务码做具体的业务提示。
JsonResponse默认状态码是200,创建资源时记得显式设置201:
from django.http import JsonResponse def create_book(request): # ... 省略创建逻辑 return JsonResponse({'code': 0, 'message': '创建成功'}, status=201)2.3 设置响应头:跨域、缓存控制、下载文件
HttpResponse对象支持像操作字典一样设置响应头。三个最常见的场景:
跨域CORS。Django默认不允许跨域请求,安装django-cors-headers之后配置CORS_ALLOWED_ORIGINS,或者在后端手动设置响应头:
def api_view(request): resp = JsonResponse({'data': 'ok'}) resp['Access-Control-Allow-Origin'] = 'https://example.com' return resp不过生产环境建议还是用django-cors-headers库统一管理,不要在每个视图里手写。
下载文件。设置Content-Disposition响应头可以让浏览器弹出下载框:
def download_report(request): content = "报表内容..." resp = HttpResponse(content) resp['Content-Type'] = 'text/plain; charset=utf-8' resp['Content-Disposition'] = 'attachment; filename="report.txt"' return resp缓存控制。对不敏感的静态查询接口,可以设置短时间的缓存头,减少重复查询:
resp['Cache-Control'] = 'max-age=300'3. Django模板语法:后端渲染页面的核心语言
在前后端分离的架构下,Django模板的任务被轻量化了很多。但如果是写后台管理系统、服务端渲染页面,或者做混合渲染(首页服务端渲染提升SEO、内部页面走接口),模板语法依然是必须掌握的技能。
3.1 变量渲染:双重花括号与点号取值规则
模板里输出变量用双重花括号{{ variable }}。视图函数通过render的第三个参数传递上下文:
def index(request): context = { 'site_name': 'Ming的博客', 'articles_count': 18, } return render(request, 'index.html', context)模板里写{{ site_name }}就会输出Ming的博客。
这里的核心机制是点号.取值。模板里写{{ user.username }},Django会按照固定顺序解析:先尝试字典键user['username'],再尝试对象属性user.username,再尝试列表索引user[0]等。这个顺序很重要,也是很多新手犯迷糊的地方。举个例子:
context = { 'user': {'name': 'Ming', 'age': 25}, }模板写{{ user.name }},Django先把它当成字典取键,user['name'],拿到Ming。如果user是一个普通的Python对象,有name属性,同样可以{{ user.name }}。如果是一个列表['a', 'b'],可以写{{ item.0 }}取第一个元素。
但注意:变量名解析规则里,如果字典键和对象属性同时存在,字典键优先。所以如果user对象有一个name属性,同时user本身又是一个字典(不可能,因为两者是同一对象),不会冲突。这里更需要小心的是:模板变量解析失败时不会抛异常,而是渲染成空字符串。比如{{ user.email }}如果user没有email这个键或属性,页面静默地输出空,这在调试时很迷惑。
3.2 过滤器:模板里的轻量数据处理函数
模板过滤器是一种类函数的表达式,格式是{{ variable|filter_name }},可以理解为管道:把变量传给过滤器处理,返回值输出到页面上。过滤器可以链式调用,{{ text|truncatechars:20|linebreaks }}。
常用过滤器清单:
| 过滤器 | 作用 | 示例 |
|---|---|---|
default | 变量为空时给默认值 | `{{ name |
length | 求长度,可用于字符串、列表、字典 | `{{ books |
date | 格式化日期 | `{{ create_time |
truncatechars | 截断字符串,超过部分用省略号 | `{{ content |
join | 用指定分隔符合并列表 | `{{ tags |
lower/upper | 转换大小写 | `{{ name |
slice | 切片 | `{{ content |
safe | 标记为安全字符串,不做HTML转义 | `{{ content |
floatformat | 格式化浮点数 | `{{ price |
这里重点说一下safe过滤器,这是模板安全机制里最关键的点。Django模板默认会对所有变量输出做HTML转义,把<转成<、>转成>、&转成&、引号转成'等。为什么要这么设计?因为如果某个变量来自用户输入,用户提交了一段<script>alert('xss')</script>,直接输出到页面上就会执行脚本。HTML转义之后,这段内容会以纯文本形式显示在页面上,不会执行。
拿我自己的博客项目打比方:文章内容如果允许用户输入HTML,在模板里渲染就必须用{{ content|safe }}。但用safe的前提是,你应该确保这个内容经过了白名单过滤(比如只允许部分标签、属性),而不是盲目信任用户输入。这也是模板引擎比纯字符串拼接更稳妥的原因。
3.3 模板标签:逻辑控制与代码复用
模板标签的语法是{% tag %}。最基础的是if和for:
{% if books %} <ul> {% for book in books %} <li>{{ book.name }} - ¥{{ book.price }}</li> {% empty %} <li>暂时没有书籍</li> {% endfor %} </ul> {% else %} <p>还没有上架任何书籍</p> {% endif %}for标签里,Django内置了一些循环变量,比如forloop.counter是从1开始计数的序号,forloop.counter0从0开始,forloop.first判断是否是第一个元素,forloop.last判断是否是最后一个。写表格、输出序号时这些变量很好用。
模板继承是模板语言里最有价值的功能。一个典型的网站,头部导航、底部信息、侧边栏在每个页面基本一样,如果每个页面都复制一份,后期维护就是灾难。模板继承的做法:
首先是基础模板base.html:
<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <title>{% block title %}默认标题{% endblock %}</title> <link rel="stylesheet" href="{% static 'css/style.css' %}"> </head> <body> <header>网站导航</header> <main> {% block content %} {% endblock %} </main> <footer>底部版权信息</footer> </body> </html>子模板book/list.html这样继承:
{% extends "base.html" %} {% block title %}书籍列表 - 我的图书馆{% endblock %} {% block content %} <h1>书籍列表</h1> ... {% endblock %}extends标签必须是子模板的第一个标签,block标签定义可以被覆盖的区块。父模板里block中的默认内容,在子模板没有覆盖相应block的时候生效。这样可以做到"有一个统一骨架,每个页面细节自定义"的效果。
include标签用于引入子模板片段:
{% include "common/pagination.html" %}include和extends的区别在于:extends是继承关系,一个模板只允许一个父模板;include是组合关系,一个页面可以多次引入不同的局部模板片段。
还有一个常用的url标签,它根据URL配置里的name参数反向生成URL路径,避免在模板里硬编码链接:
# urls.py path('books/<int:pk>/', views.book_detail, name='book_detail'),模板里使用:
<a href="{% url 'book_detail' book.pk %}">查看详情</a>如果URL配置发生调整,只要name不变,模板里不需要改动。这个标签在做项目重构时能省下大量时间。
static标签则用于生成静态文件路径:
{% load static %} <link rel="stylesheet" href="{% static 'css/style.css' %}">注意第一行的{% load static %}不能省略,这是加载static模板标签库的声明,不过Django 3.0以后内置模板里static会默认可用。{% load static %}这句写了也没关系。
3.4 模板语法的执行流程与常见误区
深入理解模板的执行机制,能帮助排查一些诡异的问题。模板渲染的步骤大致是:Django加载模板文件,解析成模板对象;渲染时,传入的上下文Context变量会被收集到一个变量栈中;模板里的变量通过名称去这个栈里查找。
这个查找机制带来一个容易困惑的点:在for循环里,如果你循环变量名和外层某个变量名相同,会覆盖外层的。比如:
context = {'book': '外层变量'}模板里:
{% for book in books %} {{ book.name }} {% endfor %} {{ book }}循环结束后,{{ book }}输出了什么?答案是最后一个循环元素,而不是外层变量。因为for循环结束后,循环变量并不会因为作用域结束而消失,它还在当前上下文里,覆盖了同名的外层变量。这是我实际项目里踩过的坑:在模板里循环变量名用了item,结果模板底部想引用传进来的item对象,输出却变成了循环里的最后一个元素。
处理办法是:在模板里尽量使用不冲突的变量名,循环变量用book_item、article_item这类有区分的命名;或者循环里用as关键字把循环结果暂存。
模板里不能写Python表达式,只有变量、过滤器、标签这三类基础能力。部分新手在模板里写{{ request.user.is_authenticated and request.user.is_staff }}想做个逻辑判断,模板会报错。应该改为:
{% if request.user.is_authenticated and request.user.is_staff %}if标签支持and、or、not、in等运算符,支持括号。模板语法的设计目标是保持展示层简单,把复杂的逻辑放在视图层处理。
4. 实战:搭建一个图书管理小功能
纸上谈兵讲再多理论,不如把代码过一遍。下面用一个图书管理的小项目做演示,把前面讲的模板语法、请求处理、响应返回全部串起来。这个项目的业务足够简单:图书列表展示 + 新增图书表单提交,但覆盖了Django开发中最核心的几块内容。
4.1 项目初始化与配置
假设Django已经安装好了,Python环境准备好了。我用Django 3.2+版本(新版4.x也可以用,基础语法基本没变)。
django-admin startproject library_project cd library_project python manage.py startapp books创建好应用之后,第一步是去settings.py注册应用,顺便配置templates目录和数据库(这里用默认的SQLite就够了):
# settings.py INSTALLED_APPS = [ 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', 'books', # 注册自己创建的应用 ] TEMPLATES = [ { 'BACKEND': 'django.template.backends.django.DjangoTemplates', 'DIRS': [BASE_DIR / 'templates'], 'APP_DIRS': True, 'OPTIONS': { 'context_processors': [ 'django.template.context_processors.debug', 'django.template.context_processors.request', 'django.contrib.auth.context_processors.auth', 'django.contrib.messages.context_processors.messages', ], }, }, ] STATIC_URL = 'static/' STATICFILES_DIRS = [BASE_DIR / 'static']APP_DIRS: True的意思是,Django会在每个应用的templates子目录下查找模板文件。我习惯同时建一个项目级的templates目录放公共页面(比如base.html),所以DIRS也要配置。
4.2 模型定义与数据核心
在books/models.py里定义一个简单的Book模型:
from django.db import models class Book(models.Model): name = models.CharField(max_length=200, verbose_name='书名') author = models.CharField(max_length=100, verbose_name='作者') price = models.DecimalField(max_digits=6, decimal_places=2, verbose_name='价格') published_date = models.DateField(verbose_name='出版日期') class Meta: ordering = ['-id'] def __str__(self): return self.name执行数据库迁移:
python manage.py makemigrations python manage.py migrate为了演示方便,进python manage.py shell插入几条测试数据:
python manage.py shellfrom books.models import Book Book.objects.create(name='Django入门指南', author='张三', price='59.90', published_date='2024-01-15') Book.objects.create(name='Python Web开发实战', author='李四', price='79.00', published_date='2024-03-20')4.3 URL路由与视图函数设计
在library_project/urls.py里引入书的应用路由:
from django.contrib import admin from django.urls import path, include urlpatterns = [ path('admin/', admin.site.urls), path('books/', include('books.urls')), ]在books/urls.py里定义路由:
from django.urls import path from . import views urlpatterns = [ path('', views.book_list, name='book_list'), path('create/', views.book_create, name='book_create'), ]视图函数是重头戏,book_list处理GET请求展示列表数据,book_create需要同时处理GET(展示表单页)和POST(接收表单提交):
from django.shortcuts import render, redirect from django.http import JsonResponse from django.views.decorators.http import require_http_methods from .models import Book from .forms import BookForm def book_list(request): books = Book.objects.all() return render(request, 'books/list.html', {'books': books}) @require_http_methods(["GET", "POST"]) def book_create(request): if request.method == 'POST': form = BookForm(request.POST) if form.is_valid(): form.save() return redirect('book_list') else: form = BookForm() return render(request, 'books/create.html', {'form': form})这里用了一个叫BookForm的东西,它其实是Django的ModelForm。用表单可以自动化处理字段校验、错误提示、CSRF令牌验证,比自己手动request.POST.get()然后逐个校验要省事得多,也安全得多。在books/forms.py里定义:
from django import forms from .models import Book class BookForm(forms.ModelForm): class Meta: model = Book fields = ['name', 'author', 'price', 'published_date'] widgets = { 'published_date': forms.DateInput(attrs={'type': 'date'}), }4.4 模板文件编写
项目级templates/base.html作为公共骨架:
{% load static %} <!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <title>{% block title %}默认标题{% endblock %}</title> <link rel="stylesheet" href="{% static 'css/style.css' %}"> </head> <body> <nav> <a href="{% url 'book_list' %}">图书列表</a> <a href="{% url 'book_create' %}">新增图书</a> </nav> <main> {% block content %}{% endblock %} </main> </body> </html>books/templates/books/list.html:
{% extends "base.html" %} {% block title %}图书列表{% endblock %} {% block content %} <h1>图书列表</h1> {% if books %} <table border="1" cellpadding="8" cellspacing="0"> <tr> <th>ID</th> <th>书名</th> <th>作者</th> <th>价格</th> <th>出版日期</th> </tr> {% for book in books %} <tr> <td>{{ book.id }}</td> <td>{{ book.name }}</td> <td>{{ book.author }}</td> <td>¥{{ book.price }}</td> <td>{{ book.published_date|date:"Y-m-d" }}</td> </tr> {% endfor %} </table> {% else %} <p>暂时没有图书数据。</p> {% endif %} {% endblock %}books/templates/books/create.html:
{% extends "base.html" %} {% block title %}新增图书{% endblock %} {% block content %} <h1>新增图书</h1> <form method="post"> {% csrf_token %} {{ form.as_p }} <button type="submit">保存</button> </form> {% endblock %}这里有个关键细节:form表单里那一行{% csrf_token %}必须保留。没有这行,提交POST表单时Django会返回403。这个标签会在表单里渲染一个隐藏的csrfmiddlewaretoken输入框,值是一串随机令牌。Django的CsrfViewMiddleware在接收POST请求时会验证这个令牌,防止跨站请求伪造攻击。
4.5 代码运行效果与请求链路回溯
在项目根目录执行:
python manage.py runserver浏览器访问http://127.0.0.1:8000/books/,会看到图书列表页面。访问http://127.0.0.1:8000/books/create/,填完表单点保存,POST请求发出后经过CSRF验证、表单校验,然后重定向回列表页,能看到新增的图书记录出现在列表里。
把这条链路的每个环节再对照一遍:
- 浏览器访问
/books/路径,发出了GET /books/请求 - Django的URL路由通过
path('', ...)匹配到book_list视图 - 视图函数查询
Book.objects.all()拿到所有图书的QuerySet render函数把books变量放入模板上下文,渲染books/list.html- 模板引擎在
{% for book in books %}循环里逐条输出数据,通过{{ book.name }}等变量访问每条记录字段 - 渲染完成的HTML字符串包装进
HttpResponse返回给浏览器
这个链路理解透了,Django开发的大半个流程就已经在掌握之中了。
5. 常见问题与排查技巧实录
最后这部分,把我做Django项目时踩过的坑、被问得最多的问题整理成速查表。这些问题分散在不同项目里反复出现过,每一条背后都有一个真实的故事。
5.1 请求处理类问题
| 问题现象 | 可能原因 | 解决方案 |
|---|---|---|
request.POST.get('name')返回None | 前端发送的是JSON数据,不是表单编码 | 使用json.loads(request.body)解析 |
| POST请求返回403,提示CSRF验证失败 | 表单或请求里缺少csrftoken | 模板表单加{% csrf_token %};AJAX请求在请求头加X-CSRFToken |
request.GET.get('page')拿到的是字符串不是数字 | GET参数本来就是字符串 | 转换类型:int(request.GET.get('page', 1)),注意捕获ValueError |
上传文件后request.FILES为空 | 没有设置表单enctype="multipart/form-data" | 表单加上enctype="multipart/form-data"属性 |
| URL带中文参数时视图拿到乱码 | 编码问题 | 前端用encodeURIComponent编码,后端用quote/unquote处理,或用Django自带处理 |
CSRF这一条值得多说一句。前后端分离项目里,前端用Vue或小程序发POST请求,没法像模板表单那样直接渲染csrfmiddlewaretoken隐藏字段。常见做法是:后端提供一个接口读取Cookie里的csrftoken,前端在每次请求时把它放进请求头发送过来。
Django的CsrfViewMiddleware验证逻辑是:优先检查请求头X-CSRFToken里的值是否与Cookie里的csrftoken匹配,匹配则通过验证;否则再检查POST表单里的csrfmiddlewaretoken字段。所以前端用axios可以这样写:
axios.defaults.headers.common['X-CSRFToken'] = getCookie('csrftoken');5.2 响应结果类问题
| 问题现象 | 可能原因 | 解决方案 |
|---|---|---|
JsonResponse返回中文变成\uXXXX | 默认ensure_ascii=True | 设置json_dumps_params={'ensure_ascii': False} |
直接返回列表给JsonResponse报TypeError | 默认只接受字典 | 传入safe=False |
| 接口超时且无日志 | 视图内某个数据库查询死锁或外部请求阻塞 | 检查数据库慢查询,外部HTTP请求加超时时间 |
| 下载文件中文名乱码 | Content-Disposition里中文没编码 | 用urllib.parse.quote处理文件名 |
| 前端拿不到响应头里的自定义字段 | 跨域时没有暴露该响应头 | 服务端设置Access-Control-Expose-Headers: X-Custom-Header |
5.3 模板渲染类问题
| 问题现象 | 可能原因 | 解决方案 |
|---|---|---|
{{ user.name }}输出为空 | 变量名拼写错误,或user里没有name键/属性 | 在视图里打印context确认键名;模板调试时用{% debug %}标签 |
页面出现<script>标签源码不执行 | Django对变量输出做了HTML转义,这是安全机制 | 确认内容是可信的,用`{{ content |
for循环里用book.name取不到值 | 循环变量名和上下文变量名冲突 | 检查循环变量是否被覆盖,改变量名 |
{% block content %}里的内容没显示 | 子模板里block名写错,或没有{% extends %} | 确认父模板和子模板的block名称完全一致 |
{% extends "base.html" %}报错找不到模板 | TEMPLATES配置里DIRS没配,或模板放错位置 | 把base.html放在templates目录下,确认路径 |
这里特别提一下调试模板问题的技巧。Django模板变量解析失败不会报错,而是输出空字符串,这个特性对新手很不友好。我的排查经验是:在视图里把传入模板的context打印出来看一眼。或者用Django的{% debug %}模板标签,在页面底部输出所有上下文变量,一眼就能看到有没有你要的那个变量、变量名是不是拼错了。
5.4 静态文件加载不出来
问得最多的问题之一就是"样式加载不了"。开发环境下,出现这个问题的原因基本是这么几个:
settings.py里STATIC_URL没配置- 模板里没写
{% load static %} static目录下文件路径拼错- 没有执行
python manage.py collectstatic(这个在生产环境才需要)
开发模式下,runserver会自动从应用目录下的static子目录和STATICFILES_DIRS指定的目录寻找静态文件。我的目录结构一般是:
library_project/ library_project/ settings.py static/ css/ style.css books/ static/ books/ js/ main.js templates/ base.html模板里这样引用:
{% load static %} <link rel="stylesheet" href="{% static 'css/style.css' %}">记住:{% static %}标签生成的是URL路径,比如/static/css/style.css,它背后映射到静态文件目录,但不等于直接拼接STATICFILES_DIRS里的路径。调试时可以先用浏览器直接访问http://127.0.0.1:8000/static/css/style.css,如果能打开就说明静态文件本身没问题,问题大概率出在模板引用上。
5.5 排查工具与习惯
做完一个Django接口或者页面,我习惯在浏览器开发者工具里看一遍完整请求:网络面板里看请求URL、请求方法、状态码、请求头、响应体。这一步能快速判断问题是出在前端还是后端。比如接口返回了500,响应体里往往会有Django的HTML错误页面,展开能看到堆栈信息,定位问题很快。
后端这边,开发时DEBUG=True时Django会提供详细的错误页面,包含异常堆栈、SQL语句、上下文变量,基本等于一个免费的调试器。但生产环境必须把DEBUG=False,不然会泄露代码路径、配置信息。
我自己还有一个习惯:在项目里安装django-debug-toolbar,开发模式下页面侧边会显示一个调试工具条,能看到所有SQL查询、请求头、模板渲染时间等信息,排查数据库N+1查询和慢接口特别有用。
写在最后
Django的模板语法、请求与响应这三块,表面上看是三个独立的知识点,实际是同一个链路的不同环节。请求到达Django,视图函数处理业务逻辑,要么用模板把数据渲染成HTML页面返回,要么用JsonResponse把数据打包成JSON返回给前端。理解了这个链路的全貌,无论你是做服务端渲染的传统Django项目,还是做前后端分离的全栈项目,都能得心应手。
我在实际项目中最大的体会是:写视图函数之前,先想清楚前端到底需要什么。如果是页面跳转,用render渲染模板;如果是数据交互,用JsonResponse返回结构化的JSON;如果需要先处理再跳转,用redirect。想清楚这个再动手,代码会简洁很多。还有一个用了很久的小技巧:视图函数的逻辑尽量不要写得太长,POST和GET分支分开处理,校验逻辑尽量用Django Form或Serializer封装起来,这样代码的可维护性会好很多。这套思路,从我最早用Django写个人博客,到现在做企业级的全栈项目,一直都适用。