Django生成zip完成后自动刷新HTML页面及CSRF、按钮问题解决
问题根因
- 连续提交触发CSRF报错:直接返回文件流的响应不会触发页面刷新,页面内嵌的CSRF token始终是首次加载时的旧值,下次提交时后端校验新旧token不匹配就会抛错,手动刷新相当于重新拉取了带最新token的页面,所以能临时解决问题。
- 绑定onclick后后端无响应:在按钮的click事件里直接禁用按钮,会阻断表单默认的提交行为,请求根本没有发送到后端。
- 无法实现下载后自动刷新:原生同步表单提交返回文件流时,前端没有对应的完成回调钩子,没法感知下载结束的时机。
解决方案
一、后端views.py调整
不要在表单POST的同步响应里直接返回文件流,先把生成的zip存入临时缓存,给前端返回下载标识,单独提供下载接口返回文件,去掉多余的手动设置CSRF cookie逻辑(Django自带中间件会自动处理CSRF cookie,手动设置签名错误反而会导致校验失败)。
修正后的代码如下:
from django.shortcuts import render from django.http import HttpResponse, JsonResponse from .forms import FileFieldForm from .perform_conversion import FileConverter import zipfile import io import uuid from django.core.cache import cache def FileFieldFormView(request, *args, **kwargs): if request.method == 'POST': form = FileFieldForm(request.POST, request.FILES) files = request.FILES.getlist('file_field') if form.is_valid(): zip_buffer = io.BytesIO() with zipfile.ZipFile(zip_buffer, "w", False) as zip_file: for f in files: fileconverter = FileConverter(f.name) fileconverter.run(f.file) for img_name, img in fileconverter.output.items(): data = io.BytesIO(img) zip_file.writestr(img_name, data.getvalue()) # 生成唯一下载ID,zip内容存入缓存10分钟过期 download_id = str(uuid.uuid4()) cache.set(f"zip_download_{download_id}", zip_buffer.getvalue(), 600) return JsonResponse({ "code": 200, "download_id": download_id }) else: return JsonResponse({"code": 400, "msg": "表单校验失败,请检查上传文件"}, status=400) # GET请求正常渲染页面 return render(request, 'file_converter.html') # 新增独立下载接口 def zip_download(request, download_id): zip_content = cache.get(f"zip_download_{download_id}") if not zip_content: return HttpResponse("下载链接已过期,请重新提交文件", status=404) # 下载后删除缓存避免占用内存 cache.delete(f"zip_download_{download_id}") response = HttpResponse(zip_content, content_type='application/octet-stream') response['Content-Disposition'] = "attachment; filename=converted.zip" return response
别忘了在urls.py中新增下载路由,对应
zip_download视图,示例:path('download/<str:download_id>/', zip_download, name='zip_download')
二、前端代码调整
核心逻辑:
- 拦截原生表单提交事件,改用异步请求发送表单数据,方便感知请求完成时机
- 点击提交后立刻禁用按钮、显示处理提示,从根源上避免重复提交
- 拿到后端返回的下载ID后,动态创建a标签触发文件下载
- 下载触发后自动刷新页面,页面重载后CSRF token自动更新为最新值,按钮也自动恢复可用状态
替换原有HTML/JS代码即可:
{% load static %} <html> <head> <link rel="stylesheet" href="{% static '/styleSheet.css' %}"> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <link href="https://fonts.googleapis.com/css2?family=Quicksand:wght@400;700&display=swap" rel="stylesheet"> <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script> </head> <body> <form enctype="multipart/form-data" id="convertForm" method="post"> {% csrf_token %} <div class="main_Body"> <div class="section"> <h1>Fileconverter</h1> <br> <label for="file_field" class="custom-file-upload"> <i class="fa fa-cloud-upload"></i>Upload File(s)</label> <input type="file" id="file_field" name="file_field" class="file-upload_button" multiple> <label id="file_name"></label> <br><br><br><br> <br> <input type="submit" class="file-submit__button" id="submitButton" value="提交转换"> <p id="tipMsg" style="color:red;display:none;">文件处理中,请不要关闭页面...</p> </div> </div> </form> </body> <footer> <p>点击"选择文件"选中需要转换的文件</p> <p>点击"提交"后开始转换任务</p> <p>转换完成后浏览器会自动下载zip压缩包</p> <p>处理过程中请勿重复点击提交按钮</p> </footer> </html> <script> // 选中文件后显示文件名 document.querySelector("#file_field").onchange = function(){ if(this.files.length > 0){ let fileNameList = Array.from(this.files).map(f => f.name).join(', '); document.querySelector("#file_name").textContent = fileNameList; } } // 拦截表单提交逻辑 const form = document.getElementById('convertForm'); const submitBtn = document.getElementById('submitButton'); const tipMsg = document.getElementById('tipMsg'); form.addEventListener('submit', function(e){ // 阻断原生表单提交 e.preventDefault(); // 立刻禁用按钮、显示提示,防止重复点击 submitBtn.disabled = true; tipMsg.style.display = 'block'; // 组装表单数据发异步请求 const formData = new FormData(form); fetch("", { method: "POST", body: formData, credentials: "same-origin" }).then(res => res.json()) .then(res => { if(res.code === 200){ // 触发文件下载 const a = document.createElement('a'); a.href = `/download/${res.download_id}/`; a.download = "converted.zip"; document.body.appendChild(a); a.click(); document.body.removeChild(a); // 下载触发后自动刷新页面,重置表单和CSRF token window.location.reload(); }else{ alert(res.msg || "转换失败,请重试"); submitBtn.disabled = false; tipMsg.style.display = 'none'; } }).catch(err => { alert("请求出错,请重试"); submitBtn.disabled = false; tipMsg.style.display = 'none'; }) }) </script>
三、注意事项
- 如果是多进程/多服务器部署,临时缓存不要用Django默认的本地内存缓存,换成Redis等集中式缓存,避免下载时找不到对应zip文件
- 原代码中
X-UA-Compatible属性写的IE=edstore是笔误,已修正为IE=edge,避免IE兼容模式异常 - 如果单文件转换耗时超过1分钟,建议额外增加转换进度提示,避免用户误以为页面卡顿
内容的提问来源于stack exchange,提问作者novawarning
相关产品推荐
相关产品推荐

