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

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

本章介绍不刷新整页的异步请求、文件上传、批量写入、JSON 序列化和分页。它们共同服务于更流畅的交互体验,但仍必须遵循 HTTP 方法、CSRF、防护、数据校验和数据库性能等基本规则。

一、Ajax 的作用与请求边界

Ajax(Asynchronous JavaScript and XML)是浏览器通过 JavaScript 主动发起 HTTP 请求、拿到结果后仅更新局部页面的技术集合。现代请求通常传输 JSON 或表单数据,而不是 XML。Ajax 不等于"没有页面跳转",而是由前端决定如何处理服务器响应。

方式 触发方式 常见结果
地址栏、<a> 浏览器导航 整页加载新的页面
<form> 原生提交 GET 或 POST 浏览器按响应导航或刷新
Ajax / Fetch JavaScript 调用 前端根据响应局部更新页面

Ajax 仍然是普通 HTTP 请求:GET 用于读取,POST/PUT/PATCH/DELETE 用于改变数据。后端必须像处理普通表单一样完成认证、权限和输入校验。

二、返回 JSON:使用 JsonResponse

浏览器与 Django 之间交换结构化数据时,JSON 是最常用格式。Python 字典不是 JSON 字符串,推荐使用 JsonResponse 自动序列化并设置 application/json 响应头。

python 复制代码
# user/views.py
from django.http import JsonResponse
from django.views import View


class RegisterView(View):
    def get(self, request, *args, **kwargs):
        return render(request, "user/register.html")

    def post(self, request, *args, **kwargs):
        username = request.POST.get("username", "").strip()
        password = request.POST.get("password", "")

        if not username or not password:
            return JsonResponse({
                "ok": False,
                "error": "用户名和密码不能为空",
            }, status=400)

        # 此处应使用 Django 认证系统处理密码,不能明文保存。
        return JsonResponse({"ok": True, "message": "注册成功"}, status=201)

代码说明:JSON 响应建议使用统一字段,例如 okdataerror,并配合合适 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 %},再从隐藏输入框读取令牌并放入请求头,是较清晰的做法。

html 复制代码
<!-- user/templates/user/register.html -->
{% load static %}
<script src="{% static 'js/jquery.min.js' %}"></script>

<form id="register-form">
  {% csrf_token %}
  <label>用户名 <input id="username" name="username" required></label>
  <label>密码 <input id="password" name="password" type="password" required></label>
  <button id="submit-button" type="submit">注册</button>
</form>
<p id="message" role="status"></p>
javascript 复制代码
$("#register-form").on("submit", function (event) {
  event.preventDefault();

  const csrfToken = $("[name=csrfmiddlewaretoken]").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) {
      const message = xhr.responseJSON?.error || "请求失败,请稍后重试";
      $("#message").text(message);
    });
});

代码说明:dataType: "json" 表示期望服务端响应是 JSON,并不会改变请求体编码。表单提交事件中调用 event.preventDefault() 可以阻止浏览器原生整页提交。success 回调只处理成功状态,使用 .fail()error 回调才能处理 4xx/5xx 响应。

发送 JSON 请求体

当 API 明确约定使用 JSON 时,应把对象序列化为字符串,设置 contentType: "application/json",并在 Django 中从 request.body 读取和解析。

javascript 复制代码
$.ajax({
  url: "/api/calculate/",
  type: "POST",
  contentType: "application/json",
  dataType: "json",
  headers: { "X-CSRFToken": $("[name=csrfmiddlewaretoken]").val() },
  data: JSON.stringify({ number_one: 12, number_two: 3, operator: "+" }),
});
python 复制代码
import json

from django.http import JsonResponse


def calculate(request):
    try:
        payload = json.loads(request.body)
        left = float(payload["number_one"])
        right = float(payload["number_two"])
    except (KeyError, TypeError, ValueError, json.JSONDecodeError):
        return JsonResponse({"ok": False, "error": "参数格式错误"}, status=400)

    operator = payload.get("operator")
    operations = {
        "+": lambda: left + right,
        "-": lambda: left - right,
        "*": lambda: left * right,
        "/": lambda: left / right if right != 0 else None,
    }
    if operator not in operations:
        return JsonResponse({"ok": False, "error": "不支持的运算符"}, status=400)

    result = operations[operator]()
    if result is None:
        return JsonResponse({"ok": False, "error": "除数不能为 0"}, status=400)
    return JsonResponse({"ok": True, "result": result})

代码说明:解析 JSON 时应捕获格式错误并验证数据类型。涉及金额时不要使用 float,应使用 Decimal;此处仅用于简单计算示例。

四、Ajax 上传文件:FormData

文件不能作为普通对象字段可靠地发送。浏览器需要用 FormData 生成 multipart/form-data 请求体,Django 则从 request.FILES 读取上传文件,文本字段仍在 request.POST

html 复制代码
<form id="avatar-form">
  {% csrf_token %}
  <label>用户名 <input id="username" name="username"></label>
  <label>头像 <input id="avatar" name="avatar" type="file" accept="image/*"></label>
  <button type="submit">上传</button>
</form>
<p id="upload-message" role="status"></p>
javascript 复制代码
$("#avatar-form").on("submit", function (event) {
  event.preventDefault();

  const formData = new FormData(this);

  $.ajax({
    url: window.location.pathname,
    type: "POST",
    data: formData,
    processData: false,
    contentType: false,
    headers: { "X-CSRFToken": $("[name=csrfmiddlewaretoken]").val() },
    dataType: "json",
  })
    .done((data) => $("#upload-message").text(data.message))
    .fail((xhr) => $("#upload-message").text(xhr.responseJSON?.error || "上传失败"));
});
python 复制代码
from django.http import JsonResponse


def upload_avatar(request):
    if request.method != "POST":
        return JsonResponse({"error": "仅支持 POST"}, status=405)

    avatar = request.FILES.get("avatar")
    if avatar is None:
        return JsonResponse({"ok": False, "error": "请选择文件"}, status=400)
    if avatar.size > 5 * 1024 * 1024:
        return JsonResponse({"ok": False, "error": "文件不能超过 5 MB"}, status=400)

    # 还应校验真实内容、扩展名和权限,并使用受控存储后端保存。
    return JsonResponse({"ok": True, "message": "文件已接收"})

代码说明:processData: false 防止 jQuery 将 FormData 转成查询字符串,contentType: false 让浏览器自动生成带边界的 multipart 请求头。不要试图读取文件输入框的 .val() 作为真实路径,浏览器会刻意隐藏本地路径。

request.is_ajax() 已在 Django 3.1 弃用并在后续版本移除,不应使用它判断请求类型。若业务确实需要特定标记,可以检查自定义请求头,但接口应以响应格式和 HTTP 约定为准,而不是把"是否 Ajax"作为安全边界。

五、确认弹窗与安全删除

确认弹窗只能改善用户体验,不能代替后端权限校验。删除操作必须使用 POST 或 DELETE,不能使用 GET,因为 GET 应保持安全且幂等,不应改变服务器数据。

html 复制代码
<table id="book-table">
  <tr data-book-id="1">
    <td>西游记</td>
    <td><button class="delete-book" type="button">删除</button></td>
  </tr>
</table>
javascript 复制代码
$("#book-table").on("click", ".delete-book", function () {
  const row = $(this).closest("tr");
  const bookId = row.data("book-id");

  if (!window.confirm("确定删除这本书吗?")) {
    return;
  }

  $.ajax({
    url: `/books/${bookId}/delete/`,
    type: "POST",
    headers: { "X-CSRFToken": $("[name=csrfmiddlewaretoken]").val() },
    dataType: "json",
  })
    .done(function (data) {
      if (data.ok) row.remove();
    })
    .fail(function (xhr) {
      alert(xhr.responseJSON?.error || "删除失败");
    });
});
python 复制代码
from django.contrib.auth.decorators import login_required
from django.http import JsonResponse
from django.views.decorators.http import require_POST

from .models import Book


@login_required
@require_POST
def delete_book(request, book_id):
    book = Book.objects.filter(pk=book_id).first()
    if book is None:
        return JsonResponse({"ok": False, "error": "图书不存在"}, status=404)

    # 此处还应校验 request.user 是否有删除该图书的权限。
    book.delete()
    return JsonResponse({"ok": True})

代码说明:页面中重复项应使用 class 而不是重复的 id。事件委托绑定到表格父元素,即使后续动态添加按钮也能响应点击。SweetAlert、layer 等组件可以替代 window.confirm(),但后端的登录、权限、CSRF 和请求方法限制始终不可省略。

六、批量插入:bulk_create()

逐条调用 create() 会执行大量 INSERT 语句;导入大量数据时,bulk_create() 可以显著减少数据库往返次数。它应使用分批写入,避免一次性创建过多 Python 对象或超出数据库参数限制。

python 复制代码
from decimal import Decimal

from django.db import transaction

from .models import Book


def import_books(rows):
    batch_size = 1000
    batch = []

    with transaction.atomic():
        for index, row in enumerate(rows, start=1):
            batch.append(Book(
                name=row["name"],
                price=Decimal(str(row["price"])),
            ))

            if len(batch) >= batch_size:
                Book.objects.bulk_create(batch, batch_size=batch_size)
                batch.clear()

        if batch:
            Book.objects.bulk_create(batch, batch_size=batch_size)

代码说明:bulk_create() 通常不会调用每个对象的 save(),也不会发送 pre_savepost_save 信号;依赖这些钩子的业务逻辑需要额外处理。示例使用 Decimal(str(...)) 避免浮点数精度问题。百万级数据导入还应考虑后台任务、进度反馈、失败重试和数据库锁影响,不能直接阻塞普通 Web 请求。

七、把 QuerySet 序列化为 JSON

模型实例和 QuerySet 不能直接交给 JsonResponse。接口应明确选择要公开的字段,转换为基础数据类型后再返回;这样既能控制响应结构,也能避免泄露敏感字段。

python 复制代码
from django.http import JsonResponse

from .models import Book


def book_list_api(request):
    books = list(Book.objects.values("id", "name", "price")[:100])
    return JsonResponse({"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,它处理页码、总页数、边界与异常更可靠。

python 复制代码
from django.core.paginator import EmptyPage, PageNotAnInteger, Paginator
from django.shortcuts import render

from .models import Book


def book_list(request):
    books = Book.objects.order_by("id")
    paginator = Paginator(books, 20)
    page_number = request.GET.get("page", 1)

    try:
        page_obj = paginator.page(page_number)
    except PageNotAnInteger:
        page_obj = paginator.page(1)
    except EmptyPage:
        page_obj = paginator.page(paginator.num_pages)

    return render(request, "book/list.html", {"page_obj": page_obj})
html 复制代码
<!-- book/templates/book/list.html -->
<ul>
  {% for book in page_obj.object_list %}
    <li>{{ book.name }} - {{ book.price }}</li>
  {% empty %}
    <li>暂无图书</li>
  {% endfor %}
</ul>

<nav aria-label="图书分页">
  <ul class="pagination">
    {% if page_obj.has_previous %}
      <li><a href="?page=1">首页</a></li>
      <li><a href="?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 %}
        <li class="disabled"><span>{{ page }}</span></li>
      {% elif page == page_obj.number %}
        <li class="active"><span>{{ page }}</span></li>
      {% else %}
        <li><a href="?page={{ page }}">{{ page }}</a></li>
      {% endif %}
    {% endfor %}

    {% if page_obj.has_next %}
      <li><a href="?page={{ page_obj.next_page_number }}">下一页</a></li>
      <li><a href="?page={{ page_obj.paginator.num_pages }}">尾页</a></li>
    {% endif %}
  </ul>
</nav>

代码说明:Paginator 会对 QuerySet 做切片,从而生成带 LIMITOFFSET 的查询。get_elided_page_range 可在页数很多时显示省略号,避免模板手写大量嵌套判断。深页码的 OFFSET 查询在超大表上可能变慢,这时应考虑基于游标或主键的"键集分页"。

九、实践要点

  1. Ajax 请求仍是 HTTP 请求,后端必须校验方法、权限、CSRF 与输入数据。
  2. jQuery 普通对象默认发的是表单编码;发送 JSON 时需 JSON.stringify()contentType: "application/json"
  3. 上传文件使用 FormData,文件从 request.FILES 读取,并在服务端限制大小、类型和存储位置。
  4. 所有删除、创建和更新操作使用非 GET 方法,确认弹窗不能替代服务器权限控制。
  5. 大批量写入分批 bulk_create(),理解它不会触发 save() 和模型信号。
  6. 接口只返回必要字段;分页优先使用 Paginator,而不是拼接带 safe 的 HTML 字符串。

掌握这些做法后,页面交互、数据导入和列表展示可以在保持响应速度的同时,维持清晰的安全与维护边界。

相关推荐
Logintern091 小时前
Langgraph使用MemorySaver建立有记忆的图
开发语言·windows·python
Uncommon.1 小时前
使用pandas处理csv并转为张量
pytorch·python·深度学习·pandas
上玄code2 小时前
【Agent精讲】调一个LLMAPI背后的工程问题
java·开发语言·python·chatgpt
Uncommon.2 小时前
使用Pytorch操作张量(多维数组)
人工智能·pytorch·python
码云骑士2 小时前
105-Ollama本地部署-Llama3-Qwen2-Modelfile-REST-API
python
晚安code2 小时前
MCP Server 开发入门:手把手写一个能跑的 Server,三种协议怎么选
python·ai编程
CTA量化套保2 小时前
量化脚本准备实盘了吗?TqSdk 上线前工程检查
人工智能·python
青 春 记 忆2 小时前
零基础入门python07:让程序记住数据——JSON文件和异常处理
开发语言·windows·python·json·python3.11
清水白石0082 小时前
Python 如何设计一个线程安全的缓存?从锁策略、LRU 到工程化实战
python·安全·缓存