☰
Django与AJAX交互实践:从CSRF到JSON的完整指南
2026/10/17 12:04:07 网站建设 项目流程

我最早接触AJAX与Django通信,是在一个老项目改造里。当时的情况是:用户填写表单之后点提交,整个页面要白屏一两秒,滚动条被顶回顶部,用户得重新找刚才填到哪了。这种体验放到现在完全没法看,但项目用的还是传统的form表单提交。改造成AJAX通信其实不算难,真正的难点在细节:CSRF Token怎么带、后端怎么解析JSON、404和403怎么区分、接口的返回结构怎么约定。这篇我就把我自己在实践中趟出来的路子完整过一遍,适合已经会写Django视图但没搞过异步交互的朋友,也适合前端调用Django接口总是报错想搞清楚原因的人。

1. 先盘清楚:AJAX 和 Django 到底在配合什么

1.1 它们各自的角色

Django本质上是一个HTTP服务端,它的核心工作就是接收请求、处理逻辑、返回响应。这个"请求-响应"循环,Django处理得再熟练,它本身并不关心请求是从哪里发来的——是从浏览器的地址栏直接访问,从表单按钮提交,还是从JavaScript代码里异步发出,对Django来说都是收到一个HTTP Request,然后返回一个HTTP Response。

AJAX做的是另一件事:让浏览器在不刷新整个页面的前提下,向服务器发起异步请求。传统的表单提交,浏览器会直接导航到action指定的URL,整个页面被响应内容替换掉;而AJAX请求则是JavaScript在后台发出去,拿到响应之后由你用代码去更新页面上的某一块内容,页面本身不会跳转、不会刷新。

这两个角色配合起来就是现代Web应用最常见的交互模式:前端页面保持不动,背后跟服务器要数据、推数据、改数据。

1.2 一次传统表单提交 vs 一次AJAX请求

要理解AJAX的好处,最直观的办法是对比两种请求方式在浏览器层面到底有什么区别。

传统表单提交的流程是这样的:用户点击submit按钮,浏览器收集表单字段,拼成请求发出去,服务器返回一个完整的HTML页面,浏览器丢掉当前页面,加载新页面。这个过程中间用户看到的是白屏、刷新、跳动,而且表单里没填完的内容全部丢失。

AJAX请求的流程是这样的:JavaScript代码用fetch或者XMLHttpRequest发请求,浏览器在后台完成网络交互,页面保持不动,服务器返回的数据通常是一小段JSON文本,JavaScript拿到之后自己决定往页面的哪个位置填什么内容。用户视角上,页面"自己"就更新了,没有跳转感。

这两者的差别用手机银行和柜台业务来类比很贴切:表单提交像是每次办业务都必须重新排队、把身份证材料全部重新交一遍;AJAX请求则像是手机银行里点两下,后台就把事情办了,界面不用离开当前页面。

1.3 为什么是JSON

AJAX的请求可以用各种格式发出,响应也可以是HTML片段、纯文本、XML,但业界通用实践是用JSON。原因很现实:JSON是JavaScript原生支持的数据格式,前端拿到之后一个JSON.parse就能直接用,而且Django内置的json模块和JsonResponse封装得很好。

现在Django项目里做前后端交互,我几乎只做两件事:前端用JSON.stringify把JavaScript对象转成JSON字符串放进请求体;后端用json.loads解析request.body得到Python字典,再通过JsonResponse返回结构化的数据。全链路不需要额外装第三方库。

1.4 Django返回JSON的完整路径

Django返回JSON的方式有几种,有些人直接在视图里写HttpResponse(json.dumps(data), content_type="application/json"),可行但不推荐。正确姿势是用JsonResponse:

from django.http import JsonResponse from django.views.decorators.http import require_http_methods @require_http_methods(["GET"]) def user_profile(request): data = { "username": "alice", "level": 5, "tags": ["python", "django"], } return JsonResponse(data)

JsonResponse会把字典自动序列化成JSON字符串,设好Content-Type,还会处理中文编码问题。默认情况下它要求传入的数据必须是字典,如果你想返回列表,需要加一个参数:

return JsonResponse([1, 2, 3], safe=False)

这里有个容易被忽略的点:JsonResponse构造的时候可以指定json_dumps_params参数,比如json_dumps_params={"ensure_ascii": False},否则中文会变成\uXXXX的转义序列。不过如果你让前端拿到响应后用response.json()去解析,这个转义不会影响结果,所以不是必须要处理的,只是直接看Network面板响应内容时会影响调试体验。

2. 从浏览器到视图:走一遍完整的请求生命周期

2.1 前端fetch的三种常见姿势

现在写AJAX请求我基本只用fetch,它是浏览器原生API,不需要引jQuery,支持Promise,代码干净。最常见的三种姿势如下。

第一种,GET请求带查询参数:

async function fetchUserProfile(userId) { const url = `/api/user/${userId}/profile/`; const response = await fetch(url); const data = await response.json(); console.log(data); }

第二种,POST请求发送JSON数据:

async function createOrder(payload) { const csrftoken = getCookie('csrftoken'); const response = await fetch('/api/order/create/', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-CSRFToken': csrftoken }, body: JSON.stringify(payload) }); const data = await response.json(); return data; }

第三种,POST请求发送FormData——适合文件上传或者要跟普通表单字段混在一起传的场景:

async function uploadFile(fileInput) { const csrftoken = getCookie('csrftoken'); const formData = new FormData(); formData.append('file', fileInput.files[0]); formData.append('description', '项目文档'); const response = await fetch('/api/file/upload/', { method: 'POST', headers: { 'X-CSRFToken': csrftoken }, body: formData }); const data = await response.json(); return data; }

注意第三种方式我没手动设Content-Type头。fetch在检测到body是FormData对象时,会自动生成multipart/form-data的Content-Type,并且自动带上正确的boundary。你要是手动设了Content-Type反而会把boundary丢掉,Django端解析就会失败。这是一个特别经典的坑。

2.2 Django端路由与配置

前端请求发到哪个URL,必须能在Django的URLconf里匹配上。我习惯把所有AJAX接口单独放在一个模块里,统一用/api/前缀,这样路由清晰,排查问题的时候一眼就能看出哪些URL是给异步接口用的。

# myapp/urls.py from django.urls import path from . import views urlpatterns = [ path('api/user/<int:user_id>/profile/', views.user_profile, name='api_user_profile'), path('api/order/create/', views.create_order, name='api_create_order'), ]

有人会把AJAX接口在项目根路由里直接挂,我觉得还是分包管理好,每个app自己负责自己的api路由,最后在根urls.py里include进来。项目大了之后这个整洁度的价值会非常明显。

2.3 视图里怎么拿前端的数据

这是最关键的一段,因为前端发送的数据格式直接决定了后端怎么取数据。我经常遇到新手调不通接口,问题不在代码逻辑,而是前端发了JSON、后端用request.POST去取,结果什么都取到为空。

它们的对应关系是这样:

  • 前端发的是JSON字符串(Content-Type为application/json),后端要用json.loads(request.body)解析,request.POST是空的。
  • 前端发的是FormData,后端直接用request.POST.get('字段名')取值,文件用request.FILES.get('file')拿。
  • 前端用URL查询参数,后端用request.GET.get('参数名')。

举一个具体的视图例子:

import json from django.http import JsonResponse from django.views.decorators.csrf import csrf_exempt from django.views.decorators.http import require_http_methods @require_http_methods(["POST"]) def create_order(request): try: body = json.loads(request.body) except json.JSONDecodeError: return JsonResponse({"code": 400, "message": "请求体不是合法的JSON"}, status=400) product_id = body.get("product_id") quantity = body.get("quantity", 1) if not product_id: return JsonResponse({"code": 400, "message": "缺少product_id"}, status=400) # 这里省略实际的业务逻辑 order_id = 10001 return JsonResponse({ "code": 0, "message": "ok", "data": {"order_id": order_id} })

代码里有两个细节值得说。第一个是我用了require_http_methods装饰器,让视图只接受POST请求,其他方法直接返回405,这比自己判断request.method然后返回要简洁。第二个是JSON解析失败的情况要单独处理,前端传了非法JSON的时候后端不应该抛500,而是应该返回明确的400错误。

2.4 视图里怎么回数据

返回结构我后面专门讲,这里只强调一个原则:尽量返回一个统一的结构,让前端只需要处理一种格式。不要有的接口返回{"success": true},有的返回{"ok": 1},有的直接返回一组裸数据,前端每一处调用都要单独适配,难维护到崩溃。

我常用的统一结构是三层:

def api_response(data=None, code=0, message="ok", http_status=200): return JsonResponse({ "code": code, "message": message, "data": data }, status=http_status)

业务成功的code是0,业务失败的code用非0值,HTTP状态码则用来表达协议层面的成败。这两者的分工我后面展开讲。

3. CSRF:Django 和 AJAX 之间绕不过去的坎

3.1 CSRF到底防的是什么

CSRF(跨站请求伪造)的攻击场景大致是:用户在自己浏览器里登录了某个网站,在没退出登录的情况下又去访问了恶意网站,恶意页面里塞了一个提交表单的代码,诱导浏览器向已登录的网站发起POST请求。因为浏览器会自动带上已登录网站的cookie,服务器根本分不清这请求是用户主动发的还是恶意页面伪造的。

Django内置的CSRF中间件就是来防这件事的。它要求所有非安全方法的请求(POST、PUT、DELETE等)里必须带上一个CSRF Token,这个Token存在cookie里,同时Django会把同样的Token写进页面渲染的上下文中。服务器收到请求时检查两者是否匹配,不匹配直接返回403。

这样说可能还不够直观。我换一个更容易理解的类比:银行办业务不仅要看你的身份证(cookie/Session),还要你输入取款密码(CSRF Token)。传统表单提交的时候,Django的模板标签{% csrf_token %}会自动把这个"密码"填进表单;而AJAX请求没有表单这个载体,所以"密码"要你自己想办法塞进请求里。

3.2 给AJAX请求注入CSRF Token的正道

Django官方文档推荐的做法是从cookie里读取csrftoken,把它放到自定义请求头X-CSRFToken里。一个比较通用的获取cookie的函数像这样:

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; }

Django默认会在模板渲染后设置csrftoken这个cookie。如果你不需要在模板里渲染内容,比如某个页面是纯前端构建后放在Django静态目录下访问的,那么这个cookie可能不会自动出现,这时就需要在视图里主动用django.middleware.csrf.get_token(request)来获取并设置cookie。

我把发送POST请求的封装做成一个公共函数,项目里所有人都用它,避免每个人自己实现一套:

async function sendPost(url, payload, { json = true } = {}) { const csrftoken = getCookie('csrftoken'); const options = { method: 'POST', headers: { 'X-CSRFToken': csrftoken } }; if (json) { options.headers['Content-Type'] = 'application/json'; options.body = JSON.stringify(payload); } else { options.body = payload; // 这里payload是FormData } const response = await fetch(url, options); const data = await response.json(); return { response, data }; }

这样封装有个额外好处:如果项目后期引入了统一的鉴权逻辑或者需要增加Header,只需要改这一个函数。

3.3 403错误与CSRF调试

跟CSRF相关的错误最常见的表现就是请求返回403,Django的响应内容大概长这样:

"Forbidden (CSRF token missing or incorrect.)"

如果你看到这个,按顺序排查三件事。第一,请求是否带了X-CSRFToken这个Header,可以在浏览器的Network面板里点进该请求,查看Request Headers。第二,cookie里是否有csrftoken这一个值,在Application面板的Cookies分类下查看。第三,cookie里的Token和Header里的Token是否一致,有时候是前一秒刚登录,Token还没有写入,刷新页面重新初始化即可。

有一个临时绕过CSRF的装饰器叫csrf_exempt,网上很多教程会教你在视图上加上了事。我必须说清楚:这个装饰器只在特定场景下该用,比如对接第三方支付回调这种没法预知请求方是谁的接口,让它不参与CSRF校验是可以理解的。但如果你只是因为AJAX调不通、不想搞明白CSRF原理就顺手加上,那等于把Django的这层防护打开了一个缺口,项目安全性会受到实实在在的削弱。我见过有人把项目里所有AJAX视图都加了csrf_exempt,这种做法在任何正规项目里都应该被Code Review否决。多花几分钟把CSRF Token通过请求头传过去,这是正道,不麻烦。

4. 接口数据结构与前后端交互约定

4.1 单字段直接返回与统一结构的权衡

新手写AJAX接口最容易犯的毛病是每个接口返回结构都不一样。有的接口成功时返回{"status": "success"},失败时返回{"error": "xxx"};有的接口成功时直接返回一个数组;有的接口又包了一层。前端每次接到响应都得先猜测这个接口的返回格式,项目的维护成本就是这么失控的。

我建议项目从一开始就约定统一的响应结构,就是我前面提的三层:

{ "code": 0, "message": "ok", "data": {} }

code为0代表业务成功,非0代表各类业务失败。data放真正的业务数据,可能是对象、数组或者null。message放可读的描述信息,既可以用来给用户看,也可以在联调的时候方便排查问题。

这种结构跟HTTP状态码是两套体系。HTTP状态码由浏览器层和框架层处理,表达的是"请求是否到达了视图、视图是否处理完";code是业务层自己约定的,表达的"业务逻辑是否成功执行"。举个例子:用户提交的订单里库存不足,HTTP状态码仍然是200(因为请求本身被正常处理了),但body里的code可能是10001,message是"库存不足"。

如果你只用HTTP状态码来区分业务成败,会遇到一个实际麻烦:前端fetch拿到404或500时,response.json()一样能解析出Django返回的错误信息,但你的then分支只能看到HTTP层面失败,还得去catch里再做一层处理。统一用业务code判断,前端代码就简单得多。

4.2 前端如何优雅地处理响应

基于上面的约定,前端的处理逻辑可以写得很统一。我习惯这样做:

async function requestData(url, options = {}) { let response; try { response = await fetch(url, options); } catch (error) { throw new Error('网络异常,请检查网络连接'); } let body; try { body = await response.json(); } catch (error) { throw new Error('服务端返回的数据格式不是合法的JSON'); } if (body.code !== 0) { throw new Error(body.message || '请求失败'); } return body.data; }

注意两层try/catch的分工:第一层捕网络错误,第二层捕获解析错误。前面的判断在HTTP状态码不是200时也要继续做,因为即使Django返回了404或者500,响应体里也可能有可读的错误信息,fetch不会在HTTP 404时把Promise标记为reject,只有网络层完全失败时才reject——这一点经常有人搞混。

4.3 加载中与错误状态的治理

跟服务器交互的过程中,用户看到的状态至少有三个:加载中、成功、失败。这三个状态很多前端漏掉了"加载中"这个环节,导致用户点了提交按钮之后,界面没有任何反馈,性能稍差一点用户就会以为是死机了,又点了一回,同一个请求被发送了两次。

正确处理方式是在点击按钮时立刻禁用按钮并显示"处理中"的文案,请求完成后再恢复。用我上面的封装函数,操作会非常统一:

submitBtn.addEventListener('click', async () => { submitBtn.disabled = true; submitBtn.textContent = '提交中...'; try { const data = await requestData('/api/order/create/', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-CSRFToken': getCookie('csrftoken') }, body: JSON.stringify({ product_id: 123, quantity: 1 }) }); // 成功后的页面操作,比如弹出提示、更新列表 } catch (error) { alert(error.message); } finally { submitBtn.disabled = false; submitBtn.textContent = '提交'; } });

finally里恢复按钮状态这个写法很值得养成习惯,因为无论成功还是失败,按钮都要能重新点击,放在finally里就只写一次。

5. 实际项目中的踩坑记录与调试三板斧

5.1 缓存、URL拼接与UTF-8编码

先说我踩过最多的坑:GET请求被浏览器缓存。AJAX改造初期,我在一个查询接口上用GET请求获取列表数据,前几次调试正常,后来改了后端逻辑,重新启动服务,再刷新前端页面,数据还是旧的,查了半天发现浏览器直接返回了缓存的响应,根本没有发请求到服务器。这个问题的根源是浏览器对GET请求有默认缓存策略,同一个URL短时间内重复请求会直接命中缓存。

解决办法有几个。最粗暴的是在URL后面拼一个时间戳参数:/api/list/?t=+ Date.now(),让每次请求的URL都不一样。第二种是显式设置请求头Cache-Control: no-cache,让服务器指示浏览器不要直接缓存。不过我更推荐的做法是:在Django的视图装饰器层面控制缓存行为,比如用@cache_control(no_cache=True)给特定的接口关掉缓存。

另一个高频坑是URL拼接没有做编码处理。如果要在查询参数里带中文或者特殊字符,必须用encodeURIComponent:

// 错误写法,关键字含中文或&时会出问题 const url = `/api/search/?keyword=${keyword}`; // 正确写法 const url = `/api/search/?keyword=${encodeURIComponent(keyword)}`;

不编码的直接后果很直接:关键字里含空格或中文时,URL可能被截断,Django端request.GET.get取到不完整的值,接口返回的结果自然是错的。

5.2 模板动态URL与Ajax请求的配合

Django模板里的{% url %}标签是用来生成URL的,但直接放在内联JavaScript里有个比较坑的细节。如果你把整个URL写死在JS文件里,项目部署时换了路径前缀或者改了路由名称,JS文件里的URL就全废了,又得去改代码重新部署。

实际项目中我常用的办法是把URL放在DOM元素的数据属性里,JS从元素上读取:

<button id="save-btn" >const saveBtn = document.getElementById('save-btn'); const saveUrl = saveBtn.dataset.saveUrl;

这样做的好处是URL跟着模板走,路由改名之后模板自动生成新URL,JS文件不需要改动。再配合我前面提到的公共请求封装函数,整个项目的AJAX代码非常稳定。

5.3 调试三板斧

调试AJAX与Django通信的问题,我基本靠三样东西。

第一样是浏览器开发者工具的Network面板。打开这个面板,能看到每一个请求的详细信息,点进某个请求后有Request Headers、Request Payload、Response三个子标签页。绝大多数问题都能在这里直接定位:请求发出去了没有、Header带没带CSRF Token、发出去的Payload格式对不对、服务器返回的原始JSON是什么。我调试的时候第一步永远是打开Network看Request Payload和Response,而不是去读代码猜。

第二样是Django服务端的日志。开发环境下DEBUG=True时,视图里抛出异常会在浏览器响应里显示详细的错误页面,方便直接看。但如果请求连路由都没匹配上,返回404,浏览器里看不到Django的日志。这时要看运行Django服务的终端窗口,请求日志会一行一行地打印出来,包括请求路径、状态码和处理时间。

第三样是给视图加临时日志输出。这个方法适合排查逻辑层面的问题。比如前端确实发来了数据,但后端处理结果不对,我就在视图里加print确认request.body的内容:

def create_order(request): print("请求体原始内容:", request.body) body = json.loads(request.body) print("解析后的字典:", body)

这种方法看着土,但效率很高。加上print之后重新请求一次,服务端终端立刻就能看到实际收到的数据,前后端哪一头有问题马上就清楚了。等定位完问题,记得把这些临时的print删掉,别留在代码里上了生产环境,到时候日志里刷的都是调试信息,正经日志反而被淹没。顺带一提,Django项目里做这种临时日志输出,用python的logging模块写DEBUG级别的日志比print更合适,控制台和日志文件都能留下记录,排查线上问题的时候尤其有用。

还有Segment 5.2聊到的CSRF错误调试,其实也属于我接触最多的三类错误之一。这里我干脆列个表总结一下状态码和对应原因:

状态码常见原因排查方向
403CSRF Token缺失/错误检查Header是否带了X-CSRFToken、cookie里是否有csrftoken
404路由没匹配/URL写错检查Django路由表、Network面板里的实际URL
405方法不允许检查视图装饰器允许的请求方法、前端实际用什么方法请求
500视图内部抛异常看Django终端日志、确认DEBUG配置

这套排查路径我实际操作过太多次,基本每次出现问题都能在五分钟内定位到原因。新手容易一上来就怀疑是Django配置问题或者中间件问题,其实大多数情况就是其中的小细节没对齐,比如Content-Type不匹配、CSRF Token忘了带、URL少了一个斜杠。

最后再分享一个我用了很久的小习惯:前后端联调的时候,把公共的AJAX请求封装函数里加一行console.log,把每次请求的URL、Header、Payload和响应全部打印出来。这个做法在调试阶段非常有用,能省掉很多"你那边报了啥错"的来回沟通。等联调结束,删掉这个打印,或者把它提升为按环境变量控制的开关。做AJAX与Django通信,本质上是个细心活,只要你把请求的每一层理清楚,把响应结构定成统一约定,把CSRF和编码这两个拦路虎提前解决掉,整个交互链路就会变得非常顺畅。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询