以下文字是 Django 类基于视图(CBV)探索系列的 最后一篇,介绍了如何通过深入使用 ListView 来实现如 分页搜索排序等在实际工作中常用的功能。如果您在前 7 篇中已查看了 CBV 的基础知识和 Mixin 的使用,那么本篇将综合这些知识,探讨如何优雅地使用 CBV 来处理实际网页开发中常见的需求。

您可以通过下面的链接查看前一篇文章!

类基于视图(CBV)探索系列 ⑦ - Mixin 的使用与权限管理


“通过扩展 Django ListView 实现强大的列表功能,提升用户体验!”


1. ListView,超出预期的可能性

ListView 是一个非常有用的通用视图,用于展示数据列表。但在实际网络应用中,除了简单地展示列表外,还需要实现 分页、有效管理 大量数据,快速查找所需信息的 搜索 ,以及根据用户偏好的 排序 功能。

在这一章中,我们将探讨如何扩展 ListView,以优雅地实现这些实际的需求。


2. 应用分页(Pagination)

一次展示大量数据会影响用户体验。 分页 通过将数据分成多个页面,减少用户的加载负担,并帮助他们更轻松地找到所需信息。

2.1 基本分页设置

ListView 提供了内置的分页功能。只需在视图类中设置 paginate_by 属性即可。

views.py 示例

from django.views.generic import ListView
from .models import Article

class ArticleListView(ListView):
    model = Article
    template_name = 'articles/article_list.html'
    context_object_name = 'articles'
    paginate_by = 10 # 每页显示 10 个 Article 对象
  • paginate_by = 10: 这样设置后,ListView 会自动将结果分为每页 10 个,并将分页相关对象(paginator, page_obj, is_paginated)传递给模板上下文。

2.2 在模板中实现分页 UI

在模板中,可以使用传递过来的 page_obj 显示页面号码链接,并实现上/下一页按钮等功能。

articles/article_list.html 示例

<ul>
    {% for article in articles %}
        <li>{{ article.title }} - {{ article.created_at }}</li>
    {% empty %}
        <li>尚未发布的文章。</li>
    {% endfor %}
</ul>

<div class="pagination">
    <span class="step-links">
        {% if page_obj.has_previous %}
            <a href="?page=1">&laquo; 起始</a>
            <a href="?page={{ page_obj.previous_page_number }}">上一页</a>
        {% endif %}

        <span class="current">
            页面 {{ page_obj.number }} / {{ paginator.num_pages }}
        </span>

        {% if page_obj.has_next %}
            <a href="?page={{ page_obj.next_page_number }}>下一页</a>
            <a href="?page={{ paginator.num_pages }}>最后一页 &raquo;</a>
        {% endif %}
    </span>
</div>
  • page_obj: 包含当前页数据的对象。提供 has_previous, has_next, previous_page_number, next_page_number, number 等属性。
  • paginator: 包含所有页面相关信息的对象。提供 num_pages(总页数)等属性。

2.3 维持 URL 连接

在生成分页链接时,可以看到使用了 ?page=2 这样的 page 查询参数。ListView 会自动识别此参数并显示该页面的数据。


在网站上快速查找所需的信息至关重要。我们将扩展 ListView 实现搜索功能。

3.1 定义表单(Form)

首先定义一个简单的表单,用于接收搜索关键词。

forms.py 示例

from django import forms

class ArticleSearchForm(forms.Form):
    search_term = forms.CharField(label='搜索词', required=False)

3.2 修改 ListView

通过重写 ListViewget_queryset() 方法来实现搜索功能。

views.py 示例

from django.views.generic import ListView
from django.db.models import Q
from .models import Article
from .forms import ArticleSearchForm

class ArticleListView(ListView):
    model = Article
    template_name = 'articles/article_list.html'
    context_object_name = 'articles'
    paginate_by = 10

    def get_queryset(self):
        queryset = super().get_queryset()
        self.form = ArticleSearchForm(self.request.GET) # 从 GET 请求获取表单数据

        if self.form.is_valid():
            search_term = self.form.cleaned_data.get('search_term')
            if search_term:
                # 根据标题或内容过滤包含搜索词的 Article
                queryset = queryset.filter(
                    Q(title__icontains=search_term) | Q(content__icontains=search_term)
                )
        return queryset

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        context['search_form'] = self.form # 传递表单给模板
        return context
  • get_queryset(): 通过重写此方法来修改默认的查询集。
  • 创建表单实例,并在通过验证后从 cleaned_data 中提取搜索词。
  • 使用 Q 对象 ,根据标题(title)或内容(content)过滤包含搜索词的 Article 对象(__icontains 不区分大小写地检查是否包含)。
  • get_context_data(): 将表单实例添加到上下文中,以便在模板中渲染。

3.3 在模板中显示搜索表单并保持结果

在模板中添加搜索表单,并构造 URL,以便在保持搜索结果的同时可以进行分页。

articles/article_list.html

<form method="get">
    {{ search_form }}
    <button type="submit">搜索</button>
</form>

<ul>
    {% for article in articles %}
        <li>{{ article.title }} - {{ article.created_at }}</li>
    {% empty %}
        <li>没有搜索结果。</li>
    {% endfor %}
</ul>

<div class="pagination">
    <span class="step-links">
        {% if page_obj.has_previous %}
            <a href="?page=1&search_term={{ search_form.search_term.value|default:'' }}">&laquo; 起始</a>
            <a href="?page={{ page_obj.previous_page_number }}&search_term={{ search_form.search_term.value|default:'' }}">上一页</a>
        {% endif %}

        <span class="current">
            页面 {{ page_obj.number }} / {{ paginator.num_pages }}
        </span>

        {% if page_obj.has_next %}
            <a href="?page={{ page_obj.next_page_number }}&search_term={{ search_form.search_term.value|default:'' }}">下一页</a>
            <a href="?page={{ paginator.num_pages }}&search_term={{ search_form.search_term.value|default:'' }}">最后一页 &raquo;</a>
        {% endif %}
    </span>
</div>
  • 将表单的 method 设置为 get ,以通过 URL 查询参数传递搜索词。
  • 在生成页面链接时,将当前搜索词(search_form.search_term.value)包含在 URL 参数中,以保持搜索结果可分页。使用 |default:'' 过滤器,在没有搜索词的情况下使用空字符串。

ListView 界面示例 - 含搜索与排序的 UI 列表页面


4. 添加排序(Ordering)功能

添加排序功能,以便用户可以按照所需标准查看数据。

4.1 添加排序选择表单(可选)

您还可以添加一个下拉表单,以选择排序标准。也可以简单地通过 URL 参数处理。在这里,我们将说明使用 URL 参数的方法。

4.2 修改 ListView

修改 get_queryset() 方法,从而应用 order_by()

views.py 示例(包含搜索功能)

from django.views.generic import ListView
from django.db.models import Q
from .models import Article
from .forms import ArticleSearchForm

class ArticleListView(ListView):
    model = Article
    template_name = 'articles/article_list.html'
    context_object_name = 'articles'
    paginate_by = 10

    def get_queryset(self):
        queryset = super().get_queryset()
        self.form = ArticleSearchForm(self.request.GET)
        ordering = self.request.GET.get('ordering', '-created_at') # 默认排序: 最新

        if self.form.is_valid():
            search_term = self.form.cleaned_data.get('search_term')
            if search_term:
                queryset = queryset.filter(
                    Q(title__icontains=search_term) | Q(content__icontains=search_term)
                )

        queryset = queryset.order_by(ordering)
        return queryset

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        context['search_form'] = self.form
        context['ordering'] = self.request.GET.get('ordering', '-created_at') # 将当前排序标准传递给模板
        return context
  • self.request.GET.get('ordering', '-created_at'): 从 URL 查询参数提取 ordering 值。若无值则默认使用 -created_at(按最新排序)。
  • queryset = queryset.order_by(ordering): 使用提取的 ordering 值对查询集进行排序。在模型字段名称前加 - 则按逆序排序。

4.3 在模板中提供排序链接

在模板中提供可按每个字段排序的链接。

articles/article_list.html 示例

<form method="get">
    {{ search_form }}
    <button type="submit">搜索</button>
</form>

<table>
    <thead>
        <tr>
            <th><a href="?ordering=title&search_term={{ search_form.search_term.value|default:'' }}">标题</a></th>
            <th><a href="?ordering=-created_at&search_term={{ search_form.search_term.value|default:'' }}">创建日期</a></th>
            </tr>
    </thead>
    <tbody>
        {% for article in articles %}
            <tr>
                <td>{{ article.title }}</td>
                <td>{{ article.created_at }}</td>
            </tr>
        {% empty %}
            <tr><td colspan="2">没有搜索结果。</td></tr>
        {% endfor %}
    </tbody>
</table>

<div class="pagination">
    <span class="step-links">
        {% if page_obj.has_previous %}
            <a href="?page=1&search_term={{ search_form.search_term.value|default:'' }}&ordering={{ ordering }}">&laquo; 起始</a>
            <a href="?page={{ page_obj.previous_page_number }}&search_term={{ search_form.search_term.value|default:'' }}&ordering={{ ordering }}">上一页</a>
        {% endif %}

        <span class="current">
            页面 {{ page_obj.number }} / {{ paginator.num_pages }}
        </span>

        {% if page_obj.has_next %}
            <a href="?page={{ page_obj.next_page_number }}&search_term={{ search_form.search_term.value|default:'' }}&ordering={{ ordering }}">下一页</a>
            <a href="?page={{ paginator.num_pages }}&search_term={{ search_form.search_term.value|default:'' }}&ordering={{ ordering }}">最后一页 &raquo;</a>
        {% endif %}
    </span>
</div>
  • 为每个表头提供包含排序标准(ordering)参数的链接。
  • 在生成页面链接时,除了当前搜索词外,还要包含当前排序标准(ordering),以便在分页时维持排序状态。

搜索/排序流程图 - ListView 扩展流程可视化


5. 通过 Mixin 组合功能

通过使用之前学习的 Mixin,可以更简洁地管理分页、搜索和排序功能。比如,可以将搜索功能拆分为 Mixin。

mixins.py 示例

from django.db.models import Q
from .forms import ArticleSearchForm

class ArticleSearchMixin:
    def get_queryset(self):
        queryset = super().get_queryset()
        self.form = ArticleSearchForm(self.request.GET)

        if self.form.is_valid():
            search_term = self.form.cleaned_data.get('search_term')
            if search_term:
                queryset = queryset.filter(
                    Q(title__icontains=search_term) | Q(content__icontains=search_term)
                )
        return queryset

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        context['search_form'] = getattr(self, 'form', ArticleSearchForm()) # 如果没有表单,则使用默认表单
        return context

views.py 示例(应用 Mixin)

from django.views.generic import ListView
from .models import Article
from .mixins import ArticleSearchMixin

class ArticleListView(ArticleSearchMixin, ListView):
    model = Article
    template_name = 'articles/article_list.html'
    context_object_name = 'articles'
    paginate_by = 10

    def get_queryset(self):
        queryset = super().get_queryset()
        ordering = self.request.GET.get('ordering', '-created_at')
        queryset = queryset.order_by(ordering)
        return queryset

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        context['ordering'] = self.request.GET.get('ordering', '-created_at')
        return context
  • ArticleSearchMixin 的创建将搜索相关逻辑分离。
  • ArticleListView 继承了 ArticleSearchMixinListView ,同时具备搜索和列表功能。

6. CBV 的优点和局限性(系列完结)

通过 8 篇 CBV 探索系列,我们探讨了从函数基础视图(FBV)转向类基础视图(CBV)的原因,了解了 CBV 的基本结构和使用方法,各种通用视图的使用,以及通过 Mixin 进行功能扩展等 CBV 的核心内容。

CBV 的主要优点:

  • 代码重用性: 通过通用视图和 Mixin,减少重复代码,实现高效开发。
  • 结构化代码: 类基础的代码更易于明确地分离和管理逻辑。
  • 可扩展性: 通过继承和 Mixin,轻松扩展现有功能及添加新功能。
  • 可维护性: 代码模块化,易于维护,减少变更对整体代码的影响。
  • 提升开发生产力: 快速开发,利用 Django 提供的各种通用视图和 Mixin。

CBV 的局限性和考虑事项:

  • 初学曲线: 可能需要对类基础结构有更深入的理解,相较于 FBV。
  • 过度赋值: 过多的 Mixin 可能会导致代码流难以把握。适当地使用 Mixin 很重要。
  • 简单逻辑可能过度设计: 在非常简单的页面或功能中,FBV 可能更简洁。根据情况选择适当的视图方式是明智的。

总的来说,Django CBV 是开发复杂多样功能的网页应用程序强大而实用的工具。如果理解并适当地运用 CBV 的优点,便能撰写更高效、易于维护的代码。

感谢您阅读类基于视图(CBV)探索系列!希望此系列能帮助您理解 Django CBV 并将其应用于实际项目中。


查看之前的文章

  1. 类基于视图(CBV)探索系列 #1 – 从 FBV 到 CBV 的原因及开发者心态
  2. 类基于视图(CBV)探索系列 #2 – 理解 Django 的基本视图类
  3. 类基于视图(CBV)探索系列 #3 – 利用 FormView 简化表单处理
  4. 类基于视图(CBV)探索系列 #4 – ListView & DetailView 的使用方法
  5. 类基于视图(CBV)探索系列 #5 – 使用 CreateView、UpdateView、DeleteView 实现 CRUD
  6. 类基于视图(CBV)探索系列 #6 – 使用 TemplateView & RedirectView
  7. 类基于视图(CBV)探索系列 #7 – Mixin 的使用与权限管理

“通过扩展 ListView 实现更加丰富而用户友好的列表功能,提升您的开发水平,通晓 CBV!”