
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 字符串。掌握這些做法后頁面交互、數據導入和列表展示可以在保持響應速度的同時維持清晰的安全與維護邊界。