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 响应建议使用统一字段,例如 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 %},再从隐藏输入框读取令牌并放入请求头,是较清晰的做法。
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_save、post_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() 返回字典形式的 QuerySet,list() 触发查询并得到可 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 做切片,从而生成带 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 字符串。
掌握这些做法后,页面交互、数据导入和列表展示可以在保持响应速度的同时,维持清晰的安全与维护边界。