本章介绍不刷新整页的异步请求、文件上传、批量写入、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 响应头。
# 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 %},再从隐藏输入框读取令牌并放入请求头,是较清晰的做法。
<!-- 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>
$("#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 读取和解析。
$.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: "+" }),
});
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。
<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>
$("#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 || "上传失败"));
});
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 应保持安全且幂等,不应改变服务器数据。
<table id="book-table">
<tr data-book-id="1">
<td>西游记</td>
<td><button class="delete-book" type="button">删除</button></td>
</tr>
</table>
$("#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 || "删除失败");
});
});
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 对象或超出数据库参数限制。
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。接口应明确选择要公开的字段,转换为基础数据类型后再返回;这样既能控制响应结构,也能避免泄露敏感字段。
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,它处理页码、总页数、边界与异常更可靠。
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})
<!-- 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" rel="external nofollow" >首页</a></li>
<li><a href="?page={{ page_obj.previous_page_number }}" rel="external nofollow" >上一页</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 }}" rel="external nofollow" >{{ page }}</a></li>
{% endif %}
{% endfor %}
{% if page_obj.has_next %}
<li><a href="?page={{ page_obj.next_page_number }}" rel="external nofollow" >下一页</a></li>
<li><a href="?page={{ page_obj.paginator.num_pages }}" rel="external nofollow" >尾页</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 字符串。
掌握这些做法后,页面交互、数据导入和列表展示可以在保持响应速度的同时,维持清晰的安全与维护边界。
到此这篇关于django ajax、批量操作与分页实践的文章就介绍到这了,更多相关django ajax、批量操作与分页内容请搜索代码网以前的文章或继续浏览下面的相关文章希望大家以后多多支持代码网!
发表评论