一,什么是分页
网页是用户与网站进行交互的主要场所,这种交互主要指数据收集与数据展示。
从试想一下,为什么我们几乎不在网页中一次性展示请求获得的所有数据呢?
如果这个数据量相当小,比如只有几十条,那么一般情况下无需担心,一股脑渲染到页面中就行。
但如果这个数据量比较大,比如几百几千几万条,且一旦这种操作比较频繁,显然就会增加服务器负载,主要瓶颈是数据库。
这里不谈如何实现高并发,只谈如何以轻量化的方式获取并展示数据。
一种有效的方式就是实现分页查询:将一次性获取所有数据的操作,分解为多次查询操作,每次操作只查询并展示一部分数据。其中每一次查询就是获取一页数据。
效果如下:
具体来说有三种方式实现:
- 自定义视图,通过请求参数获取指定范围内的数据。
- 使用分页组件。
- 使用 AJAX 滚动加载。
二,django的分页组件
django的分页组件可以将数据被分割在几个页面上,并带有“上一页/下一页”链接。
(一)Paginator 类和 Page 类
Paginator
类是分页组件的核心,它完成将 QuerySet
拆分为 Page
对象的所有繁重工作。
>>> from django.core.paginator import Paginator
# objects 是一个列表、元组、QuerySet 或其他具有 count() 或 __len__() 方法的可切片对象。
# 如果是 QuerySet,则应该是有序的,例如使用 order_by() 子句或使用模型上的默认 ordering。
>>> objects = ['john', 'paul', 'george', 'ringo']
# 获取分页器实例
>>> p = Paginator(objects, 2) # 可迭代对象,每页数量
# 获取所有页面中的对象总数
>>> p.count
4
# 获取总页数
>>> p.num_pages
2
# 以 1 为基础的页码范围迭代器
>>> type(p.page_range)
<class 'range_iterator'>
>>> p.page_range
range(1, 3)
# 获取指定页
>>> page1 = p.page(1)
>>> page1
<Page 1 of 2>
# 获取指定页中的内容
>>> page1.object_list
['john', 'paul']
>>> page2 = p.page(2)
>>> page2.object_list
['george', 'ringo']
# 判断当前页是否有下一页
>>> page2.has_next()
False
# 判断当前页是否有上一页
>>> page2.has_previous()
True
>>> page2.has_other_pages()
True
# 当前页的下一页页码
>>> page2.next_page_number()
Traceback (most recent call last):
...
EmptyPage: That page contains no results
# 当前页的上一页页码
>>> page2.previous_page_number()
1
# 获取当前页第一条数据在所有数据中的索引
>>> page2.start_index() # The 1-based index of the first item on this page
3
# 获取当前页末一条数据在所有数据中的索引
>>> page2.end_index() # The 1-based index of the last item on this page
4
# 获取错误的页码将报错
>>> p.page(0)
Traceback (most recent call last):
...
EmptyPage: That page number is less than 1
>>> p.page(3)
Traceback (most recent call last):
...
EmptyPage: That page contains no results
(二)在函数视图中使用分页组件
# FBV
from django.core.paginator import Paginator
from django.shortcuts import render
from myapp.models import Contact
def listing(request):
contact_list = Contact.objects.all()
paginator = Paginator(contact_list, 25) # Show 25 contacts per page.
page_number = request.GET.get('page')
page_obj = paginator.get_page(page_number)
return render(request, 'list.html', {'page_obj': page_obj})
# template
{% for contact in page_obj %}
{# Each "contact" is a Contact model object. #}
{{ contact.full_name|upper }}<br>
...
{% endfor %}
<div class="pagination">
<span class="step-links">
{% if page_obj.has_previous %}
<a href="?page=1">« first</a>
<a href="?page={{ page_obj.previous_page_number }}">previous</a>
{% endif %}
<span class="current">
Page {{ page_obj.number }} of {{ page_obj.paginator.num_pages }}.
</span>
{% if page_obj.has_next %}
<a href="?page={{ page_obj.next_page_number }}">next</a>
<a href="?page={{ page_obj.paginator.num_pages }}">last »</a>
{% endif %}
</span>
</div>
在URL中携带page参数。
(三)在类视图中使用分页组件
# CBV
class StudentList(ListView):
model = StudentInfo
paginate_by = 25
template_name = 'list.html'
CBV 会在指定了 paginate_by
属性后
(四)对分页的改进
以上两种方法够简单,但正因如此,它俩都有一个缺点:会一股脑儿地将页码全部显示出来。
比如:
显然不够友好,要是页码有切割就好了:
最简单的解决方法就是使用第三方的分页插件一劳永逸。
(五)使用分页插件Django-pure-pagination
Django-pure-pagination是比较出名的分页插件。
1,安装并注册:
pip install django-pure-pagination
INSTALLED_APPS = (
...
'pure_pagination',
)
2,配置分页切割方式:
# 分页配置
PAGINATION_SETTINGS = {
'PAGE_RANGE_DISPLAYED': 5,
'MARGIN_PAGES_DISPLAYED': 2,
'SHOW_FIRST_PAGE_WHEN_INVALID': True,
}
3,函数视图:
from pure_pagination import Paginator, PageNotAnInteger
def test_dpp(request):
school_list = SchoolInfo.objects.all()
paginator = Paginator(school_list, 5, request=request)
try:
page_number = request.GET.get('page', 1)
except PageNotAnInteger:
page_number = 1
page_obj = paginator.page(page_number)
return render(request, 'list.html', {'page_obj': page_obj})
4,模板:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<style type="text/css">
a {
padding: 5px 10px;
color: white;
background-color: #2caa0d;
margin: 1px; /*设置标签 a 之间的间隔*/
text-decoration: none; /*去除页码数字下面的下划线*/
}
a:hover {
color: black;
}
.current {
color: black;
}
</style>
</head>
<body>
<div>
{% for school in page_obj.object_list %}
<ul>
<li>
{{ school.name }}<br>
</li>
</ul>
{% endfor %}
</div>
{#Django-pure-pagination基础渲染方法#}
{#<div id="pagination">#}
{# {{ page_obj.render }}#}
{#</div>#}
<div class="pageturn">
{% if page_obj.has_previous %}
<a href="?{{ page_obj.previous_page_number.querystring }}">上一页</a>
{% endif %}
{% for page in page_obj.pages %}
{% if page %}
{% ifequal page page_obj.number %}
<a href="?{{ page.querystring }}"><span class="page-link current">{{ page }}</span></a>
{% else %}
<a href="?{{ page.querystring }}" class="page">{{ page }}</a>
{% endifequal %}
{% else %}
<a href="">...</a>
{% endif %}
{% endfor %}
{% if page_obj.has_next %}
<a href="?{{ page_obj.next_page_number.querystring }}">下一页</a>
{% endif %}
</div>
</body>
</html>
5,路由:
from pagination.views import test_dpp
urlpatterns = [
path('admin/', admin.site.urls),
path('', test_dpp),
]
效果如下:
实际上,django project的分页应该是参考过这个插件的实现方式。
三,AJAX 与分页
(一)AJAX 简介
AJAX 就是用来描述一种使用现有技术(HTML 或 XHTM、CSS、JavaScript、DOM、XML、XSLT以及最重要的 XMLHttpRequest)集合的异步处理方法。
当使用结合了这些技术的 AJAX 模型以后,网页应用能够快速地将增量更新呈现在用户界面上,而不需要重载(刷新)整个页面。这使得程序能够更快地回应用户的操作。
1,原生JavaScript实现 AJAX
原生 JavaScript 实现 AJAX 请求的过程比较复杂,简单来说就分为以下几步:
- 定义
XMLHttpRequest
对象。 - 在
onreadystatechange
方法中注册事件处理函数,准备接收响应数据,并进行处理。 - 调用
XMLHtφRequest
对象的open()
方法访问服务器端 URL 地址。 - 调用
XMLHttpRequest
对象的send()
方法发送请求。
举个例子🌰:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>AJAX</title>
<script>
function loadXMLDoc() {
let xmlhttp;
if (window.XMLHttpRequest) {
// IE7+, Firefox, Chrome, Opera, Safari 浏览器执行代码
xmlhttp = new XMLHttpRequest();
} else {
// IE6, IE5 浏览器执行代码
xmlhttp = new ActiveXObject("Microsoft.XMLHTTP");
}
xmlhttp.onreadystatechange = function () {
if (xmlhttp.readyState === 4 && xmlhttp.status === 200) {
//取到的内容:一个JSON对象。相当于一个python的列表,列表里面是嵌套的字典
//[{"model": "cmdb.article", "pk": 1, "fields": {"title": "\u7b2c\u4e00\u4e2a\u6807\u9898", "content": "\u7b2c\u4e00\u6761\u5185\u5bb9\uff01\uff01\uff01"}}]
const data = JSON.parse(xmlhttp.responseText);
const element = document.getElementById("myDiv");
element.innerText = JSON.stringify(data, null, "\t");
display(element, data);
}
}
xmlhttp.open("GET", "/thejson/", true);
xmlhttp.send();
alert(xmlhttp.getAllResponseHeaders());
}
function display(element, data) {
const children = element.childNodes;
for (var i = 0; i < children.length; i++) {
if (children[i].id === "content")
element.removeChild(children);
}
const len = eval(data).length;
for (var i = 0; i < len; i++) {
let newP = document.createElement("p");
newP.setAttribute("id", "content");
var content = data[i].pk + " " + data[i].fields.title + " " + data[i].fields.content
let newContent = document.createTextNode(content);
newP.appendChild(newContent);
element.appendChild(newP);
}
}
</script>
</head>
<body>
<div id="myDiv"><h2>使用 AJAX 获得文本内容</h2></div>
<button type="button" onclick="loadXMLDoc()">获得内容</button>
</body>
</html>
还能进一步设置请求头以实现内容协商。
2,jQuery实现 AJAX
jQuery 封装了常用的、复杂的一些 JavaScript 操作。
除了可以使用全局性函数load()
、get()
、post()
实现页面的异步调用和与服务器交互数据外,在 jQuery 还有一个功能更为强悍的最底层的方法$.ajax()
,该方法不仅可以方便地实现上述三个全局函数完成的功能,还更多地关注实现过程中的细节。
举个例子🌰:
$.ajax({
type: "POST",
url: "some.php",
data: "name=John&location=Boston",
success: function(msg){
alert( "Data Saved: " + msg );
}
});
(二)在django中使用 AJAX
一个简单的参考,可见使用AJAX与Django通信。
(三)使用 AJAX 实现页面的滚动加载
AJAX需要一个触发条件,所以我们通常将它绑定到一个 DOM 事件上。
我们可以将将“下一页”等操作的触发绑定到 DOM 的点击事件上,配合 Ajax 实现实现页面的局部刷新。
也能只将“下一页”操作的触发绑定到页面滚事件,当滚动到末尾时加载下一页,就能实现 infinite scroll 或者 endless 效果,比如在 reddit 首页不断滚都鼠标滚轮所得到的那种效果(注意网页右边的滚动条):
(四)使用插件实现页面的滚动加载
有一些插件可用,比如django-infinite-scroll-pagination、
也能使用一些纯JavaScript插件来实现,比如Waypoints
滚动分页的具体代码就不展示了,请参考文档,这里就看一下简单的效果: