搜索、筛选与分页
学习目标
实现标题/正文搜索、分类筛选和稳定分页,切换页码时保留查询条件,给空结果提供明确反馈。
为什么分页不仅是几个数字
数据量增长后,一次加载全部文章会增加数据库、网络和浏览器成本。分页减少单次处理量,也需要解决状态问题:读者搜索“Django”并选中某分类,点击下一页时应继续这个条件,而不是突然回到全部内容。
GET 表单把筛选状态写进 URL,便于收藏和分享;查询关键词被截断到合理长度,避免无意义的巨大输入。Paginator.get_page() 能把不合法页码转换为可用页;默认排序中的时间和主键保证同一条件下顺序明确。原生 querystring 标签复制当前查询参数并替换 page,省去手工拼接和 HTML 转义问题。
具体怎么做
- 替换首页视图,先公开过滤,再搜索和分类过滤,最后分页。
- 替换首页模板,添加有 label 的 GET 搜索表单。搜索提交不携带旧页码,因此新查询回到第一页。
- 创建共享分页模板,使用
{% querystring page=... %}。它从请求上下文读取参数,依赖已配置的 request context processor。 - 用第 10 章的 15 篇文章验证三页结果,每页 6 篇,最后一页 3 篇。
本章新增或替换的完整文件
以下路径均相对于练习项目根目录。替换同名文件的全部内容;未列出的文件保持上一章版本。
blog/views.py
from django.shortcuts import render, redirect, get_object_or_404
from django.views.decorators.http import require_GET
from django.core.paginator import Paginator
from django.db.models import Q
from .models import Category, Post
@require_GET
def home(request):
posts = Post.objects.filter(status=Post.Status.PUBLISHED).select_related("author", "category")
q = request.GET.get("q", "").strip()[:100]
category = request.GET.get("category", "").strip()[:50]
if q:
posts = posts.filter(Q(title__icontains=q) | Q(body__icontains=q))
if category:
posts = posts.filter(category__slug=category)
page_obj = Paginator(posts, 6).get_page(request.GET.get("page"))
return render(request, "blog/home.html", {"page_obj": page_obj, "q": q,
"selected_category": category, "categories": Category.objects.all()})
@require_GET
def detail(request, pk):
post = get_object_or_404(Post.objects.select_related("author", "category"), pk=pk, status=Post.Status.PUBLISHED)
return render(request, "blog/detail.html", {"post": post})
@require_GET
def about(request):
return render(request, "blog/about.html")
@require_GET
def old_home(request):
return redirect("blog:home")
templates/blog/home.html
{% extends "base.html" %}
{% block content %}
<h1>学习,实践,分享</h1>
<form method="get" class="panel" role="search">
<label for="q">搜索文章</label><input id="q" name="q" value="{{ q }}" maxlength="100" placeholder="输入标题或正文关键词">
<label for="category">分类</label><select id="category" name="category"><option value="">全部分类</option>
{% for category in categories %}<option value="{{ category.slug }}"{% if selected_category == category.slug %} selected{% endif %}>{{ category.name }}</option>{% endfor %}</select>
<button type="submit">查找</button> <a href="{% url 'blog:home' %}">清除条件</a></form>
{% for post in page_obj %}<article><h2><a href="{{ post.get_absolute_url }}">{{ post.title }}</a></h2>
<p class="muted">{{ post.author.username }} · {{ post.category.name }} · {{ post.created_at|date:"Y-m-d" }}</p>
<p>{{ post.body|truncatechars:120 }}</p></article>{% empty %}<p>暂时没有公开文章。</p>{% endfor %}
{% include "blog/pagination.html" %}
{% endblock %}
templates/blog/pagination.html
{% if page_obj.paginator.num_pages > 1 %}
<nav class="pagination" aria-label="分页">
{% if page_obj.has_previous %}<a href="{% querystring page=page_obj.previous_page_number %}" rel="prev">上一页</a>{% endif %}
<span>第 {{ page_obj.number }} / {{ page_obj.paginator.num_pages }} 页</span>
{% if page_obj.has_next %}<a href="{% querystring page=page_obj.next_page_number %}" rel="next">下一页</a>{% endif %}
</nav>{% endif %}
如何确认成功
打开 /?q=Django&category=django&page=2。应保留输入框和分类选中状态,当前第 2 页;下一页链接仍含 q 和 category。输入不存在的关键词后显示无公开文章,点击“清除条件”恢复完整列表。
依次测试 ?page=abc、?page=9999、?page=-1,页面都能显示可用页,不抛出 500。默认 get_page 对超范围值会使用最后一页,这不是自定义的“永远回第一页”规则。
练习:把每页数量改为 5,说明总页数为什么变化。为无结果提示增加“检查关键词”的文案,但不改变搜索状态。用手机横竖屏切换,确认按钮不会挤在一行外。
常见问题
搜索中文准确吗? contains 是简单包含查询,不等于中文分词或全文检索。当前规模适合入门,数据量增长后应测量再选择数据库全文索引或搜索服务。深页查询越来越慢? offset 分页会跳过前面记录;第 22 章讨论测量和适用的游标分页,不要在小项目里提前引入复杂方案。
官方参考
登录后可保存阅读位置、标记章节完成,并在其他设备继续学习。
登录并保存学习进度 →联系站长
微信:VIPdjango。技术问题建议先在 社区讨论 中交流,方便更多人参与和查阅。
章节讨论 0