ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Django Ajax、批量操作与分页实践

Django Ajax、批量操作与分页实践 Django Ajax、批量操作与分页实践本章介绍不刷新整页的异步请求、文件上传、批量写入、JSON 序列化和分页。它们共同服务于更流畅的交互体验但仍必须遵循 HTTP 方法、CSRF、防护、数据校验和数据库性能等基本规则。一、Ajax 的作用与请求边界AjaxAsynchronous JavaScript and XML是浏览器通过 JavaScript 主动发起 HTTP 请求、拿到结果后仅更新局部页面的技术集合。现代请求通常传输 JSON 或表单数据而不是 XML。Ajax 不等于“没有页面跳转”而是由前端决定如何处理服务器响应。方式触发方式常见结果地址栏、a浏览器导航整页加载新的页面form原生提交GET 或 POST浏览器按响应导航或刷新Ajax / FetchJavaScript 调用前端根据响应局部更新页面Ajax 仍然是普通 HTTP 请求GET 用于读取POST/PUT/PATCH/DELETE 用于改变数据。后端必须像处理普通表单一样完成认证、权限和输入校验。二、返回 JSON使用 JsonResponse浏览器与 Django 之间交换结构化数据时JSON 是最常用格式。Python 字典不是 JSON 字符串推荐使用JsonResponse自动序列化并设置application/json响应头。# user/views.pyfromdjango.httpimportJsonResponsefromdjango.viewsimportViewclassRegisterView(View):defget(self,request,*args,**kwargs):returnrender(request,user/register.html)defpost(self,request,*args,**kwargs):usernamerequest.POST.get(username,).strip()passwordrequest.POST.get(password,)ifnotusernameornotpassword:returnJsonResponse({ok:False,error:用户名和密码不能为空,},status400)# 此处应使用 Django 认证系统处理密码不能明文保存。returnJsonResponse({ok:True,message:注册成功},status201)代码说明JSON 响应建议使用统一字段例如ok、data、error并配合合适 HTTP 状态码。不要用HttpResponse(dict)直接返回字典若手动使用json.dumps()还需自行正确设置content_type。三、jQuery Ajax 提交普通表单数据jQuery 的$.ajax()传入普通对象时默认以application/x-www-form-urlencoded形式编码因此 Django 可从request.POST读取数据。它并不会自动发送 JSON。POST 请求必须携带 Django 的 CSRF Token。模板中先输出{% csrf_token %}再从隐藏输入框读取令牌并放入请求头是较清晰的做法。!-- user/templates/user/register.html --{% load static %}scriptsrc{% static js/jquery.min.js %}/scriptformidregister-form{% csrf_token %}label用户名inputidusernamenameusernamerequired/labellabel密码inputidpasswordnamepasswordtypepasswordrequired/labelbuttonidsubmit-buttontypesubmit注册/button/formpidmessagerolestatus/p$(#register-form).on(submit,function(event){event.preventDefault();constcsrfToken$([namecsrfmiddlewaretoken]).val();$.ajax({url:window.location.pathname,type:POST,data:{username:$(#username).val(),password:$(#password).val(),},headers:{X-CSRFToken:csrfToken},dataType:json,}).done(function(data){$(#message).text(data.message);}).fail(function(xhr){constmessagexhr.responseJSON?.error||请求失败请稍后重试;$(#message).text(message);});});代码说明dataType: json表示期望服务端响应是 JSON并不会改变请求体编码。表单提交事件中调用event.preventDefault()可以阻止浏览器原生整页提交。success回调只处理成功状态使用.fail()或error回调才能处理 4xx/5xx 响应。发送 JSON 请求体当 API 明确约定使用 JSON 时应把对象序列化为字符串设置contentType: application/json并在 Django 中从request.body读取和解析。$.ajax({url:/api/calculate/,type:POST,contentType:application/json,dataType:json,headers:{X-CSRFToken:$([namecsrfmiddlewaretoken]).val()},data:JSON.stringify({number_one:12,number_two:3,operator:}),});importjsonfromdjango.httpimportJsonResponsedefcalculate(request):try:payloadjson.loads(request.body)leftfloat(payload[number_one])rightfloat(payload[number_two])except(KeyError,TypeError,ValueError,json.JSONDecodeError):returnJsonResponse({ok:False,error:参数格式错误},status400)operatorpayload.get(operator)operations{:lambda:leftright,-:lambda:left-right,*:lambda:left*right,/:lambda:left/rightifright!0elseNone,}ifoperatornotinoperations:returnJsonResponse({ok:False,error:不支持的运算符},status400)resultoperations[operator]()ifresultisNone:returnJsonResponse({ok:False,error:除数不能为 0},status400)returnJsonResponse({ok:True,result:result})代码说明解析 JSON 时应捕获格式错误并验证数据类型。涉及金额时不要使用float应使用Decimal此处仅用于简单计算示例。四、Ajax 上传文件FormData文件不能作为普通对象字段可靠地发送。浏览器需要用FormData生成multipart/form-data请求体Django 则从request.FILES读取上传文件文本字段仍在request.POST。formidavatar-form{% csrf_token %}label用户名inputidusernamenameusername/labellabel头像inputidavatarnameavatartypefileacceptimage/*/labelbuttontypesubmit上传/button/formpidupload-messagerolestatus/p$(#avatar-form).on(submit,function(event){event.preventDefault();constformDatanewFormData(this);$.ajax({url:window.location.pathname,type:POST,data:formData,processData:false,contentType:false,headers:{X-CSRFToken:$([namecsrfmiddlewaretoken]).val()},dataType:json,}).done((data)$(#upload-message).text(data.message)).fail((xhr)$(#upload-message).text(xhr.responseJSON?.error||上传失败));});fromdjango.httpimportJsonResponsedefupload_avatar(request):ifrequest.method!POST:returnJsonResponse({error:仅支持 POST},status405)avatarrequest.FILES.get(avatar)ifavatarisNone:returnJsonResponse({ok:False,error:请选择文件},status400)ifavatar.size5*1024*1024:returnJsonResponse({ok:False,error:文件不能超过 5 MB},status400)# 还应校验真实内容、扩展名和权限并使用受控存储后端保存。returnJsonResponse({ok:True,message:文件已接收})代码说明processData: false防止 jQuery 将FormData转成查询字符串contentType: false让浏览器自动生成带边界的 multipart 请求头。不要试图读取文件输入框的.val()作为真实路径浏览器会刻意隐藏本地路径。request.is_ajax()已在 Django 3.1 弃用并在后续版本移除不应使用它判断请求类型。若业务确实需要特定标记可以检查自定义请求头但接口应以响应格式和 HTTP 约定为准而不是把“是否 Ajax”作为安全边界。五、确认弹窗与安全删除确认弹窗只能改善用户体验不能代替后端权限校验。删除操作必须使用 POST 或 DELETE不能使用 GET因为 GET 应保持安全且幂等不应改变服务器数据。tableidbook-tabletrdata-book-id1td西游记/tdtdbuttonclassdelete-booktypebutton删除/button/td/tr/table$(#book-table).on(click,.delete-book,function(){constrow$(this).closest(tr);constbookIdrow.data(book-id);if(!window.confirm(确定删除这本书吗)){return;}$.ajax({url:/books/${bookId}/delete/,type:POST,headers:{X-CSRFToken:$([namecsrfmiddlewaretoken]).val()},dataType:json,}).done(function(data){if(data.ok)row.remove();}).fail(function(xhr){alert(xhr.responseJSON?.error||删除失败);});});fromdjango.contrib.auth.decoratorsimportlogin_requiredfromdjango.httpimportJsonResponsefromdjango.views.decorators.httpimportrequire_POSTfrom.modelsimportBooklogin_requiredrequire_POSTdefdelete_book(request,book_id):bookBook.objects.filter(pkbook_id).first()ifbookisNone:returnJsonResponse({ok:False,error:图书不存在},status404)# 此处还应校验 request.user 是否有删除该图书的权限。book.delete()returnJsonResponse({ok:True})代码说明页面中重复项应使用 class 而不是重复的 id。事件委托绑定到表格父元素即使后续动态添加按钮也能响应点击。SweetAlert、layer 等组件可以替代window.confirm()但后端的登录、权限、CSRF 和请求方法限制始终不可省略。六、批量插入bulk_create()逐条调用create()会执行大量 INSERT 语句导入大量数据时bulk_create()可以显著减少数据库往返次数。它应使用分批写入避免一次性创建过多 Python 对象或超出数据库参数限制。fromdecimalimportDecimalfromdjango.dbimporttransactionfrom.modelsimportBookdefimport_books(rows):batch_size1000batch[]withtransaction.atomic():forindex,rowinenumerate(rows,start1):batch.append(Book(namerow[name],priceDecimal(str(row[price])),))iflen(batch)batch_size:Book.objects.bulk_create(batch,batch_sizebatch_size)batch.clear()ifbatch:Book.objects.bulk_create(batch,batch_sizebatch_size)代码说明bulk_create()通常不会调用每个对象的save()也不会发送pre_save、post_save信号依赖这些钩子的业务逻辑需要额外处理。示例使用Decimal(str(...))避免浮点数精度问题。百万级数据导入还应考虑后台任务、进度反馈、失败重试和数据库锁影响不能直接阻塞普通 Web 请求。七、把 QuerySet 序列化为 JSON模型实例和QuerySet不能直接交给JsonResponse。接口应明确选择要公开的字段转换为基础数据类型后再返回这样既能控制响应结构也能避免泄露敏感字段。fromdjango.httpimportJsonResponsefrom.modelsimportBookdefbook_list_api(request):bookslist(Book.objects.values(id,name,price)[:100])returnJsonResponse({results:books})代码说明values()返回字典形式的QuerySetlist()触发查询并得到可 JSON 序列化的列表。返回对象包一层results可在以后添加分页、统计信息或错误字段而不破坏接口结构。django.core.serializers.serialize(json, queryset)也可将模型序列化为 JSON但输出结构包含模型标签、主键和fields嵌套通常不适合作为面向前端的稳定 API。复杂 API 更适合使用 Django REST Framework 的 Serializer 明确控制字段和验证规则。八、分页使用 Django Paginator分页避免一次性向数据库和页面加载所有记录。最基础的切片公式是第page页、每页per_page条数据对应[ (page - 1) * per_page : page * per_page ]。但业务项目应优先使用 Django 的Paginator它处理页码、总页数、边界与异常更可靠。fromdjango.core.paginatorimportEmptyPage,PageNotAnInteger,Paginatorfromdjango.shortcutsimportrenderfrom.modelsimportBookdefbook_list(request):booksBook.objects.order_by(id)paginatorPaginator(books,20)page_numberrequest.GET.get(page,1)try:page_objpaginator.page(page_number)exceptPageNotAnInteger:page_objpaginator.page(1)exceptEmptyPage:page_objpaginator.page(paginator.num_pages)returnrender(request,book/list.html,{page_obj:page_obj})!-- book/templates/book/list.html --ul{% for book in page_obj.object_list %}li{{ book.name }} - {{ book.price }}/li{% empty %}li暂无图书/li{% endfor %}/ulnavaria-label图书分页ulclasspagination{% if page_obj.has_previous %}liahref?page1首页/a/liliahref?page{{ page_obj.previous_page_number }}上一页/a/li{% endif %} {% for page in page_obj.paginator.get_elided_page_range %} {% if page page_obj.paginator.ELLIPSIS %}liclassdisabledspan{{ page }}/span/li{% elif page page_obj.number %}liclassactivespan{{ page }}/span/li{% else %}liahref?page{{ page }}{{ page }}/a/li{% endif %} {% endfor %} {% if page_obj.has_next %}liahref?page{{ page_obj.next_page_number }}下一页/a/liliahref?page{{ page_obj.paginator.num_pages }}尾页/a/li{% endif %}/ul/nav代码说明Paginator会对QuerySet做切片从而生成带LIMIT和OFFSET的查询。get_elided_page_range可在页数很多时显示省略号避免模板手写大量嵌套判断。深页码的 OFFSET 查询在超大表上可能变慢这时应考虑基于游标或主键的“键集分页”。九、实践要点Ajax 请求仍是 HTTP 请求后端必须校验方法、权限、CSRF 与输入数据。jQuery 普通对象默认发的是表单编码发送 JSON 时需JSON.stringify()和contentType: application/json。上传文件使用FormData文件从request.FILES读取并在服务端限制大小、类型和存储位置。所有删除、创建和更新操作使用非 GET 方法确认弹窗不能替代服务器权限控制。大批量写入分批bulk_create()理解它不会触发save()和模型信号。接口只返回必要字段分页优先使用Paginator而不是拼接带safe的 HTML 字符串。掌握这些做法后页面交互、数据导入和列表展示可以在保持响应速度的同时维持清晰的安全与维护边界。
返回列表