资讯详情

Django模板语法、请求与响应完整链路解析

📅 2026/10/11 15:10:10 | 华诺云谱 👁 阅读
Django模板语法、请求与响应完整链路解析
上一篇文章讲完Django的项目结构和模型层之后有读者留言说想让我聊聊请求进来之后到底发生了什么。这个切入点确实很实际——很多人能照葫芦画瓢写出视图函数但一旦涉及模板变量怎么传、ajax请求怎么接、响应数据怎么返就开始发懵。正好手头在做一个全栈小项目我就把Django模板语法、请求与响应这三块内容彻底掰开揉碎结合真实业务场景讲一遍。这篇既讲语法细节也讲请求处理的完整链路适合正在学全栈开发、或者已经会写一点Django但总感觉串不起来的同学参考。1. 先搭一个能跑起来的路由骨架URL映射机制与设计习惯1.1 项目路由和应用路由为什么要分开Django的URL配置urls.py是我见过的新手最容易踩坑的地方之一。很多人一上来把所有路由写在项目的urls.py里结果业务一多就变成几百行的面条文件。正确的做法是理解两层路由的职责项目路由根urls.py负责挂载整个项目的入口用include()把请求分发给各个app。应用路由app内的urls.py只负责当前app的URL定义每个app自己管自己的地址。举个我实际项目的例子。一个商城应用商品搜索、商品详情、购物车各是一个独立功能模块那我就建一个goods应用只让根配置做一次转发# config/urls.py项目路由 from django.contrib import admin from django.urls import path, include urlpatterns [ path(admin/, admin.site.urls), path(goods/, include(goods.urls)), ] # goods/urls.py应用路由 from django.urls import path from goods import views urlpatterns [ path(, views.goods_list), path(detail/int:goods_id/, views.goods_detail), ]这样的设计带来的直接好处是应用可以被整体复用下次另一个项目要商品功能直接把这个app拷过去路由跟着走。而且团队协作的时候每个人维护自己app的urls.py几乎不会产生合并冲突。1.2 路径参数的三种传递方式以及一个很隐蔽的坑Django视图函数接收的参数来源其实有三种参数来源示例特点路径参数detail/int:goods_id写在URL里必须通过路由转换器取查询字符串?page2sortprice不在路由定义里出现直接用request.GET取POST请求体表单或JSON用request.POST或request.body取路径参数是新手最需要理解清楚的东西。int:goods_id这个写法难点在于Django内置的转换器只有str、int、slug、uuid、path这几种如果业务需要商品编码为G12345这种格式int直接用不了就得写一个自定义转换器。但大多数场景下int和str就够用了。我遇到过的问题是这样的视图函数定义是def goods_detail(request, goods_id):写接口文档时也写了goods_id但模板里用{% url goods:detail goods.id %}生成链接时模板报错说找不到反向解析参数。排查了半天发现问题出在我路径参数名用了pk而视图函数参数名用了goods_id它们必须一致才能正确绑定。所以路径参数的名字要和视图函数参数一一对应这是最容易被忽略的点。1.3 从URL到视图Django内部做了什么说句实话理解URL怎么匹配到视图函数这件事比记各种配置重要得多。Django收到请求后的大致处理流程是根据REQUEST_METHODGET/POST/PUT/DELETE等加上URL路径找到匹配的path()规则。从URL中提取路径参数和request对象一起传给视图函数。视图函数返回一个HttpResponse对象Django再把它转成HTTP响应发回客户端。这个流程决定了几个关键事实视图函数的第一个参数永远是request除非用path()里带了额外参数比如默认值否则你不能在视图函数里添加多余的必填参数。我见过有人写def index(request, name):但URL里根本没定义name一请求就直接报TypeError就是这个流程没理解透。2. 模板语法里那些真正高频的用法变量、过滤器与继承2.1 模板是一个渲染环境不是拼接字符串很多从Flask转过来的同学会下意识把模板当成HTML字符串的拼接工具但Django的模板语言DTL本质是一个受限的Python渲染环境。你用{{ variable }}输出变量用{% tag %}执行逻辑但它不允许在模板里直接写任意Python表达式——比如{{ list[-1] }}这种写法在DTL里是不支持的得先处理成last_item再传进上下文。这里最值得理解的概念是上下文字典。render(request, goods_list.html, context)的第三个参数context是一个普通字典模板里所有的变量都从这里面取。比如context { goods: goods_queryset, category: category_name, total: len(goods_queryset), } return render(request, goods_list.html, context)模板里就可以这样使用h1{{ category }}列表/h1 p共 {{ total }} 件商品/p理解了模板变量来自context字典这个事实后面排查模板变量显示为空或者KeyError时思路就会清晰很多先查视图里context有没有这个key再查模板里的变量名写没写错。2.2 模板标签for循环、if判断的作用域细节模板标签是{% %}包裹的部分。开发中最常用的是for和if它们连在一起能解决大部分页面渲染场景。比如一个商品列表页ul {% for goods_item in goods %} li a href/goods/detail/{{ goods_item.id }} {{ goods_item.name }} /a {% if goods_item.price 100 %} span性价比好物/span {% elif goods_item.price 500 %} span中端优选/span {% else %} span高端精品/span {% endif %} /li {% empty %} li暂无商品/li {% endfor %} /ul这里有几个新手需要留意的点for...empty是DTL里一个很实用的组合当序列为空时自动执行empty分支省得先判断再for。for循环内部自带变量{{ forloop.counter }}从1开始和{{ forloop.counter0 }}从0开始这个在做分页序号时特别有用。if标签支持and、or、not也支持、!、、但不支持用括号改变优先级复杂的条件判断场景最好在视图里先把结果算好。还有一个隐蔽的坑模板里访问字典key用item.key这种方式看起来没问题但如果item是一个字典且key是动态的或者包含特殊字符比如商品-名称那item.商品-名称就会解析失败。这种情况应该先把字典转换成带普通属性名的对象或者在视图中提取好再传给模板。2.3 过滤器default、date、safe到底该怎么用过滤器是DTL里特别能提升效率的一类语法格式是{{ value|filter_name }}还可以串联。我常用的过滤器就这几个过滤器用法示例作用default{{ pricedefault:暂无价格 }}length{{ usernamelength }}date{{ created_at|date:Y-m-d H:i }}格式化日期时间truncatechars{{ content|truncatechars:20 }}截断字符串超出部分显示省略号safe{{ html_content|safe }}标记内容为安全的HTML不转义floatformat{{ price|floatformat:2 }}保留两位小数safe过滤器是我最想提醒大家谨慎使用的。Django默认对变量进行HTML转义把变成lt;这是为了防止XSS注入。如果你在后台存了一段富文本确实希望它按HTML渲染那用safe没问题但如果数据来自用户输入你用safe就直接把攻击入口打开了。我的习惯是内容可信度不高的情况下宁可放弃展示效果也不用safe。自定义过滤器也不难。在app里建templatetags包写一个模块# goods/templatetags/goods_filters.py from django import template register template.Library() register.filter def price_range(price): if price 100: return 筑基级 if price 500: return 金丹级 return 元婴级然后在模板里这样用{% load goods_filters %} span{{ goods_item.price|price_range }}/span注意templatetags目录下必须有__init__.py文件否则Django找不到这个标签库。2.4 模板继承的extends与block以及那个著名的第一行限制模块化几乎是所有模板系统都支持的思路DTL靠extends和block实现。我的项目里通常有一个base.html{% load static %} !DOCTYPE html html langzh head meta charsetUTF-8 title{% block title %}全栈测试商城{% endblock %}/title /head body div classcontent {% block content %} {% endblock %} /div div classfooter 底部公共信息 /div /body /html子页面这样继承{% extends base.html %} {% block title %}商品详情{% endblock %} {% block content %} h1{{ goods.name }}/h1 p{{ goods.desc }}/p {% endblock %}这里我必须强调一个制度性极强的坑{% extends %}必须是模板文件中的第一个标签它前面不能有空格、注释、HTML代码甚至空行在某些版本上也会出问题。因为这个标签的作用是告诉Django我要以另一个模板为骨架Django需要先把这个信息解析出来后面的内容才会被放进对应的block里。另一个常见问题是extends和include的区别。include是引入一个子模板到当前页面子模板可以拿到当前上下文变量而extends是整体继承布局子模板只负责填充block。我在实际开发里的经验是页面级结构用extends公共小组件比如商品卡片、分页条用include这样拆分工最清晰。3. 请求对象到底装了什么拿参数的正确姿势3.1 request.GET和request.POST背后的QueryDict机制request对象承载了HTTP请求的所有信息。最常见的属性就是method、path、GET、POST、META、body。其中request.GET和request.POST都是QueryDict类型这个类型的特点是同一个key可以对应多个value。举个例子一个多选筛选功能用户选了红色、蓝色两个颜色表单提交的URL可能是/search/?colorredcolorblue这时候request.GET.get(color)只会拿到red而request.GET.getlist(color)才能拿到[red, blue]。所以取参数的时候要想清楚你取的是单值还是复合值。搜索框、分页参数、评价等级这些单值场景用get()多选筛选一定要用getlist()。QueryDict还有一个特性它是不可变的。很多人为了给参数加默认值直接写request.GET[page] 1这一行就会触发AttributeError。正确做法是request.GET.get(page, 1)它不会修改原始数据而是给你一个安全的返回值。3.2 参数取值和类型转换代码要写到什么程度很多新手写参数取值只是图省事拿到字符串直接用。但真实的业务场景下参数可能为空、可能是乱写的、可能是SQL注入的起点。我总结了一套稳定写法def goods_list(request): # 分类筛选可空 category request.GET.get(category, ).strip() # 页数必须转换成int给了默认值 try: page int(request.GET.get(page, 1)) except (TypeError, ValueError): page 1 # 排序字段白名单校验 sort_by request.GET.get(sort, default) if sort_by not in (price_asc, price_desc, default): sort_by default goods_queryset GoodsInfo.objects.all() # 后续根据category / sort_by / page 做过滤、排序、切片...这里面最关键的是int()那里。request.GET.get(page, 1)返回的是字符串1如果你直接拿去和数字做运算或者直接传给ORM的分页逻辑轻则类型不一致重则如果用户传了?pageabc你的视图就会抛ValueError崩溃。所以任何从request参数里拿到的数据在正式使用前都要做类型转换和兜底处理。3.3 编码问题为什么用axios时request.POST永远取不到值这个话题在热搜词里出现了不止一遍ajax请求设置编码格式axiosget请求以及为什么请求格式是input不是message。在Django项目里最常见的编码问题是前端用axios发的JSON格式POST请求后端request.POST拿不到任何数据。原因是Django的request.POST只解析application/x-www-form-urlencoded普通表单和multipart/form-data文件表单两种编码格式。当axios默认发送application/json的数据时数据会出现在request.body里request.POST自然是空的。所以前后端联调时要么前端设置axios.post(/api/order/, { goods_id: 1001, count: 2, }, { headers: { Content-Type: application/json, } });后端这样取import json def create_order(request): if request.method POST: data json.loads(request.body) goods_id data.get(goods_id) count data.get(count)要么前端直接用表单格式let params new URLSearchParams(); params.append(goods_id, 1001); params.append(count, 2); axios.post(/api/order/, params)后端就能直接用request.POST.get(...)取到。我强烈建议前后端约定好一种格式不要混用。如果项目里既有老页面用表单提交又有新接口用JSON那就要在视图里做兼容先判断request.content_type再决定从request.POST取值还是从request.body解析。判定标准很简单——request.POST存不存在数据和请求头Content-Type是强相关的。这个问题的背后不只是Django的机制也是HTTP协议本身的机制理解了协议层以后遇到Go、Java后端类似场景也不会慌。3.4 请求头里还有什么值得关注的东西除了GET和POST参数request.META这个字典里有大量HTTP头部信息。常用的包括request.META.get(HTTP_USER_AGENT)用户浏览器信息。request.META.get(HTTP_REFERER)来源页面可以用于记录用户从哪里跳转而来。request.META.get(REMOTE_ADDR)客户端IP地址。request.META.get(HTTP_AUTHORIZATION)有时候放Token。request.headers.get(X-Custom-Header)Django 2.2之后推荐用request.headers访问请求头。有一次排查一个爬虫问题我发现所有异常请求的HTTP_USER_AGENT都为空于是直接在视图里加了个判断非法UA直接返回403这个问题当天就解决了。这就是请求头的价值。4. 响应对象返回数据和渲染页面是两回事4.1 HttpResponse一切响应形式的基底Django里所有返回给客户端的内容本质上都是HttpResponse实例。不管是render()渲染模板还是JsonResponse返回JSON还是redirect()重定向底层都是构造了一个HttpResponse对象只不过content部分不同。from django.http import HttpResponse def health_check(request): return HttpResponse(OK, status200, content_typetext/plain; charsetutf-8)理解这点很重要因为有时候你想在响应头里塞一些自定义信息比如给前端返回一个追踪IDresponse HttpResponse(OK) response[X-Trace-Id] e9f3a1b2-8471-4c2e-9b1c-5d2f3a4b5c6d return response这些自定义响应头在使用场景上很常见——比如让前端知道当前请求是否命中缓存、或者告诉调试验证码的接口是否触发了频率限制。只要你记得视图返回的是Response对象这个灵活度就有了。4.2 JsonResponse前后端分离的主力输出做全栈开发前端页面用fetch或者axios请求接口时后端返回数据的最佳方式就是JsonResponse。它做的事情很简单把Python的dict或list序列化成JSON字符串并设置Content-Type: application/json。from django.http import JsonResponse def goods_detail_json(request, goods_id): goods GoodsInfo.objects.filter(idgoods_id).first() if goods is None: return JsonResponse({code: 1001, message: 商品不存在}, status404) return JsonResponse({ code: 0, data: { id: goods.id, name: goods.name, price: goods.price, } })两个小细节值得记住JsonResponse的json_dumps_params参数可以控制序列化格式比如{ensure_ascii: False}可以让中文以明文方式输出而不是\u4ea7\u54c1这种转义形式。当data是list类型时要加safeFalse参数JsonResponse(goods_list, safeFalse)。因为JsonResponse默认只允许顶层是dict否则会抛TypeError。我还经常遇到一个场景后端返回了Decimal类型的价格Django的DecimalField读出来是DecimalJSON序列化会报错。这时候要么在ORM查询后用float()转换要么自定义一个JSONEncoder。我通常直接在视图里把字段都转成字符串或浮点数再返回省得前端拿到奇怪的类型。4.3 render的底层原理模板引擎怎么和Context合并很多教程会告诉你render就是加载模板加上上下文但没讲清楚细节。render(request, template_name, context)的完整调用链路大致是用loader.get_template(template_name)加载模板文件。用Template.render(context)把上下文字典和模板语法结合生成HTML字符串。把HTML字符串封装成HttpResponse并返回。也就是说render()的本质是模板渲染 HttpResponse包装。如果你不想用模板直接返回字符串也行如果你不想走模板引擎那直接用HttpResponse。这个理解对调试也有帮助。比如模板报TemplateDoesNotExist时很多人第一反应是文件路径写错了但还有一个常见原因是模板目录没配置正确。在settings.py中TEMPLATES配置项里DIRS指定了额外搜索目录默认是空列表。如果你的模板不是放在app下的templates目录里而是放在项目根目录的templates下那DIRS里面必须加上BASE_DIR / templates。4.4 redirect到底发生了什么以及POST请求重定向的坑redirect()是开发高频函数它返回的其实是一个HttpResponseRedirect对象本质是一个带Location响应头的302响应。浏览器收到302后会重新向Location指定的地址发起GET请求。这就引出一个经典问题PRG模式Post/Redirect/Get。假设用户通过POST提交了一个表单视图直接返回render()渲染一个提交成功页面。用户按F5刷新浏览器会重复提交POST请求——这在支付、下单这种场景是灾难级的。正确做法是提交成功后redirect()到另一个GET请求页面比如订单详情这样刷新的是GET页面POST请求被隔离了。写代码时还要注意redirect()的传参用法from django.shortcuts import redirect def order_success(request): # 反解析URL return redirect(goods:goods_detail, goods_id1001) # 或者直接给完整路径 return redirect(/goods/detail/1001/) # 或者加后缀查询参数 return redirect(f/goods/detail/{goods_id}?fromorder)4.5 状态码和cookie响应头里还能做什么文章状态码的语义在Django里没有特殊改写但你要知道什么时候该用什么状态码。我的习惯是200正常返回数据和页面。301/302重定向。404资源不存在。400参数错误。401/403未认证/无权限。500服务器崩溃。设置Cookie是响应里另一个高频操作。登录状态用Session或JWT存储时后端经常通过HttpResponse的set_cookie方法下发信息response JsonResponse({code: 0, message: 登录成功}) response.set_cookie(token, token_value, max_age3600, httponlyTrue, samesiteLax) return responsehttponlyTrue会禁止JavaScript读取这个Cookie能有效减少XSS导致的信息泄露这个在涉及登录态的项目里几乎是必须的。5. 把三者串起来一个实际全栈页面的完整请求流5.1 场景设定一个简单的商品搜索页讲完单独的知识点必须把它们串成一个完整的请求链路否则学到的东西还是散的。我用一个最经典的商品搜索页面来演示。需求用户在页面输入关键字点击搜索浏览器向服务端发送请求服务端过滤商品把结果渲染回页面。整个链路涉及URL路由 - 视图处理 - 模板渲染 - 浏览器展示。5.2 第一步浏览器的GET请求怎么到达视图用户在/goods/search/?keyword手机这样的URL发起GET请求。注意这里不是表单提交是浏览器地址栏直接带参数访问。Django的路由规则很简单# goods/urls.py from django.urls import path from goods import views urlpatterns [ path(search/, views.goods_search, namegoods_search), ]视图函数from django.shortcuts import render from goods.models import GoodsInfo def goods_search(request): keyword request.GET.get(keyword, ).strip() if keyword: goods_queryset GoodsInfo.objects.filter(name__icontainskeyword) else: goods_queryset GoodsInfo.objects.all() context { goods: goods_queryset, keyword: keyword, count: goods_queryset.count(), } return render(request, search.html, context)这里有个性能细节值得注意goods_queryset.count()在底层会执行SELECT COUNT(*)而goods_queryset本身是惰性的只有到模板里真正遍历时才执行SELECT。所以count()此时只是多了一次轻量查询不算重复查询问题。但如果你在遍历之后再调用count()意味着第二次查询数据库这种写法要避免。5.3 第二步模板如何把context渲染成HTMLsearch.html的模板内容{% extends base.html %} {% block content %} h1商品搜索/h1 form methodget action{% url goods_search %} input typetext namekeyword value{{ keyword }} button typesubmit搜索/button /form p共找到 strong{{ count }}/strong 件商品/p ul {% for goods_item in goods %} li a href/goods/detail/{{ goods_item.id }}{{ goods_item.name }}/a span classprice{{ goods_item.price }}/span /li {% empty %} li没有匹配的商品/li {% endfor %} /ul {% endblock %}这个页面用表单的GET提交。用户点击搜索按钮后浏览器会把表单中所有name属性的值拼到URL查询串上然后跳转到?keyword...。这里的好处是URL可以直接分享比如发给别人一个带搜索词的链接别人打开看到的就是同样的搜索结果。关于表单的{% url goods_search %}它反解析到/goods/search/。如果以后路由路径改了模板不用动这就是反解析的价值。5.4 第三步前端异步请求和Django返回数据的完整闭环搜索页这样整页刷新没问题但现在的全栈项目前端交互往往要局部刷新。假设用户在商品列表页点击加载更多前端需要向后台请求第2页的商品数据用fetch发起fetch(/goods/api/goods/?page${page}page_size10, { headers: { Accept: application/json, } }) .then(res res.json()) .then(data { if (data.code 0) { renderGoods(data.data.goods); } });后端视图返回JsonResponsefrom django.http import JsonResponse def goods_api(request): page int(request.GET.get(page, 1)) page_size int(request.GET.get(page_size, 10)) goods_queryset GoodsInfo.objects.all()[(page - 1) * page_size: page * page_size] data [] for g in goods_queryset: data.append({ id: g.id, name: g.name, price: str(g.price), }) return JsonResponse({ code: 0, data: {goods: data, page: page} })前端再根据返回的data.goods渲染DOM。这里和模板渲染的区别在于模板渲染是服务端把数据和HTML合在一起交给浏览器API方式则是后端只返回结构化数据前端自己拼HTML。两者各自有使用场景。需要SEO的页面一定要模板渲染页面每次加载只做一次完整跳转而交互频繁的局部刷新场景API更合适。5.5 全栈项目里绕不过去的CSRF问题当页面通过fetch发送POST请求时Django的CSRF保护会默认拦截。热搜里也有django cookie设置token、给ajax请求参数赋值这些词正好把这里捋清楚。CSRF跨站请求伪造的核心机制是Django会在响应时种下一个随机token前端下次提交POST请求时必须带上这个tokenDjango一比对就能识别出这个请求是不是自己页面发起的。模板渲染的写法很简单在form里加上{% csrf_token %}。但fetch请求没有模板标签可依赖常见的做法是在返回页面的模板里通过JavaScript读取Cookie里的csrftoken。每次fetch请求把token放在请求头X-CSRFToken中。function getCookie(name) { let cookieValue null; if (document.cookie document.cookie ! ) { const cookies document.cookie.split(;); for (let i 0; i cookies.length; i) { const cookie cookies[i].trim(); if (cookie.substring(0, name.length 1) (name )) { cookieValue decodeURIComponent(cookie.substring(name.length 1)); break; } } } return cookieValue; } fetch(/api/order/, { method: POST, headers: { Content-Type: application/json, X-CSRFToken: getCookie(csrftoken), }, body: JSON.stringify({goods_id: 1001, count: 2}), });有些人图省事直接用csrf_exempt装饰器关掉CSRF校验但这是一个非常危险的行为——等于把写操作的接口完全裸奔在互联网上任何人只要知道URL就能伪造请求提交数据。我个人的经验是如果接口是给内部系统用且不经过浏览器可以关掉校验并在网络层做好防护但面向公网的全栈应用请务必保持CSRF开启。做前后端分离的纯API项目CSRF可以配合token认证来替代也不是简单关掉就完了。6. 调试与排错把这些年踩过的坑集中讲一遍6.1 怎么判断问题出在请求还是响应这是全栈开发里最基本也最重要的排查思路。页面出问题先别急着改代码第一步是打开浏览器开发者工具的Network面板刷新页面看请求的状态码和响应体。如果Network里根本没有发请求或者请求标红显示Failed to load——问题在前端多半是JS报错或者URL拼接错误。如果请求发出了状态码是4xx/5xx——问题在后端可以直接看响应体或服务端日志。如果状态码是200但页面内容不对——可能是模板变量传错或者前端渲染逻辑有问题。我做了一个很稳定的判断列表现象优先检查方向页面空白视图是否return了内容模板是否有语法错误500页面查看服务端Traceback日志404路由路径是否匹配URL是否拼错参数为空request.GET.get()的key名字是否和前端name属性一致POST数据拿不到检查Content-Type看数据在request.POST还是request.body模板变量显示空白context里的key名和模板变量名是否一致值是否为None6.2 两个高频报错的根因复盘报错一MultiValueDictKeyError这个报错常见于新手直接写request.GET[keyword]。Key不存在时中括号取值方式会抛KeyError。解决方案就一句话用.get(keyword, )替代中括号取值。这个错误在搜索页面最常见因为用户可能直接访问/search/而不带任何参数。我自己的规范是所有query参数一律用.get()加默认值取除非你确定这个参数一定要存在且缺失时就应该报错暴露问题。报错二ValueError: invalid literal for int()这是int(request.GET.get(page))在没有做异常处理时用户传了?pageabc的结果。前面的代码段里已经给了try-except的兜底方案这里不重复。我想补充的是这类问题在现网应用中不是极少数情况用户的输入是无限自由的扫描工具也不会体贴你所以第3.2节写的那种防御式取值方式应该成为你的默认肌肉记忆。6.3 模板渲染失败时怎么看TracebackDjango的调试模式DEBUGTrue会给你一个很详细的错误页面但新手经常被一长串报错吓住。我的建议是从最底部的异常类型往前看。页面会告诉你During handling of the above exception, another exception occurred然后给出最后一行关键错误。举个例子TemplateDoesNotExist报错时页面会列出所有查找过的模板路径。这时候你要检查两件事模板文件是否真的在app/templates目录下目录名是不是拼错了比如templates写成template。这两个占了这个报错八成以上的原因。另外视图返回了空值也会导致页面看起来像崩了。几年前我遇到过一个问题视图查无数据时返回NoneDjango直接报ValueError: The view ... didnt return an HttpResponse object.。这个报错的意思很明确——视图函数所有分支都必须返回HttpResponse对象不能某个分支不返回东西。修法也很简单if not goods_queryset:时返回一个render或者JsonResponse让视图每个路径都有出口。6.4 一个让我印象深刻的联调bugaxios给Django传JSON有一次我给一个商城项目做前后端联调前端用axios写了一个下单接口我后端怎么调试request.POST都是空的。当时先怀疑是路由写错又怀疑是视图名字没对上折腾了快两个小时才反应过来是编码格式问题。前端默认发的Content-Type是application/json数据全在request.body里而我一直在request.POST里找当然什么都找不到。后来我把这个问题做进了团队的接口规范里后端提供API时在接口文档里写清楚入参的格式是JSON还是form-data前端按格式发后端按格式取谁也别打哑谜。这个规范非常有用从那以后联调环节的返工率几乎是零。这个坑强烈建议大家提前知悉不要等上线前才踩。6.5 写代码之前先学会用print和大括号定位问题调试Django视图有个很朴素的技巧在视图函数里直接print(request.method, request.path, request.GET)然后在服务端的终端里观察日志。这个方式用在DEBUG模式下极快能瞬间分辨出请求有没有到达这个视图以及到达时的参数是什么。模板里的问题则可以用{{ debug }}或者在视图返回前print(context)确认。如果页面是模板渲染且变量显示异常不要急着检查模板文件先在视图的context字典里打印一下对应的key确认数据源有没有问题。从数据源头开始排查比在模板里猜来猜去有效得多。这一步是我调试所有Django项目的固定起点。要真论起效率一个趁手的IDE断点调试远比print好用但print这种粗暴方式有一个不可替代的好处——它让你时刻清楚自己的代码执行到哪里、数据长什么样。尤其当你被前端、后端、模板三端问题同时纠缠的时候print能帮你把复杂的链路拆成一段一段地确认避免被各种细节带偏。这套Django模板语法—请求—响应的三段式链路是我做全栈项目时反复依赖的骨架。别看单个知识点简单把它们串起来之后一个请求从浏览器出发经过URL路由、视图逻辑、模板渲染再以HTML或JSON的形式回到前端整个闭环就跑通了。建议你手头备一个特别小的练习项目每次改一个参数、加一个接口都从头到尾把链路走一遍时间久了这些概念就完全长在脑子里了。我在实际操作中还有一个体会先别急着上复杂的前后端分离框架把Django原生的模板请求响应吃透这套基本功在任何全栈架构里都不会过时。
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑