如何使用 JavaScript 实现分页
分页是将大量数据分割成若干个较小的独立页面,使用户更容易处理和理解信息的过程。在本教程中,我们将演示如何用三种不同的方式实现JavaScript分页系统。
为什么需要 JavaScript 分页
创建分页系统有很多好处。想象一下,你的博客有成千上万篇文章。如果要把所有文章都列在一页上,那肯定不行。但你可以创建一个分页系统,让用户可以浏览不同的页面。
分页还可以降低服务器负载,因为每次请求时只需传输一部分数据。这可以提升应用程序的整体性能,提供更好的用户体验,从而改善网站的搜索引擎优化 (SEO)。
项目准备
首先,让我们初始化一个全新的 Node.js 项目。进入你的工作目录并运行以下命令:
npm init
npm install express pug sqlite3 prisma @prisma/client
在本课中,我们将使用Prisma.js作为 ORM 示例,但请记住,我们的重点是分页背后的逻辑,而不是工具本身。
使用以下命令初始化 Prisma:
npx prisma init
请创建一个schema.prisma文件。打开该文件并进行以下编辑。
.
├── .env
├── index.js
├── libs
├── package-lock.json
├── package.json
├── prisma
│ ├── database.sqlite
│ ├── migrations
│ ├── schema.prisma <===
│ └── seed.js
├── statics
│ └── js
│ └── app.js
└── views
└── list.pug
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "sqlite"
url = env("DATABASE_URL")
}
model Post {
id Int @id @default(autoincrement())
title String
content String?
}
第 5 行至第 8 行指定了所使用的数据库类型(sqlite在本例中为 ),并url定义了连接字符串,该字符串是从存储在我们.env文件中的环境变量中提取的。
.
├── .env <===
├── index.js
├── libs
├── package-lock.json
├── package.json
├── prisma
│ ├── database.sqlite
│ ├── migrations
│ ├── schema.prisma
│ └── seed.js
├── statics
│ └── js
│ └── app.js
└── views
└── list.pug
.env
DATABASE_URL = "file:database.sqlite";
第 10 行到第 14 行创建了一个Posts包含 atitle和 的新表content。
在本教程中,我们需要创建大量文章来演示 JavaScript 中的分页功能。为了简化操作,我们不会手动创建这么多文章,而是先创建一个数据库种子数据。这样可以确保在运行数据库迁移时,数据库能够自动填充数据。
seed.js在该目录下创建一个文件prisma。
.
├── .env <===
├── index.js
├── libs
├── package-lock.json
├── package.json
├── prisma
│ ├── database.sqlite
│ ├── migrations
│ ├── schema.prisma
│ └── seed.js <===
├── statics
│ └── js
│ └── app.js
└── views
└── list.pug
seed.js
const { PrismaClient } = require("@prisma/client");
const prisma = new PrismaClient();
async function main() {
for (i = 0; i <= 99; i++) {
await prisma.post.create({
data: {
title: `Post #${i}`,
content: `Lorem ipsum dolor sit amet...`,
},
});
}
}
main()
.then(async () => {
await prisma.$disconnect();
})
.catch(async (e) => {
console.error(e);
await prisma.$disconnect();
process.exit(1);
});
然后,您必须告诉 Prisma 该seed.js文件的位置。打开该package.json文件并添加以下键:
package.json
{
"name": "pagination",
"type": "module", // Enables ES Modules, more info here: https://www.thedevspace.io/course/javascript-modules
"version": "1.0.0",
"description": "",
"main": "index.js",
"prisma": {
"seed": "node prisma/seed.js" // <===
},
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC",
"dependencies": {
"@prisma/client": "^5.16.1",
"express": "^4.19.2",
"prisma": "^5.16.1",
"pug": "^3.0.3",
"sqlite3": "^5.1.7"
}
}
最后,执行以下命令运行迁移:
npx prisma migrate dev
如何轻松实现 JavaScript 分页
当你考虑将项目分成页面时,你首先想到的最简单的逻辑是什么?
例如,你可以从数据库中检索所有文章作为一个数组,然后使用该splice()方法根据一定的页面大小将它们拆分成更小的数组。
index.js
import express from "express";
import { PrismaClient } from "@prisma/client";
const app = express();
const port = 3001;
const prisma = new PrismaClient();
app.set("views", "./views");
app.set("view engine", "pug");
app.use(express.urlencoded({ extended: true })); // for parsing application/x-www-form-urlencoded
app.use(express.json());
app.use("/statics", express.static("statics"));
// The easy way
// ===========================================================
app.get("/pages/:page", async function (req, res) {
const pageSize = 5;
const page = Number(req.params.page);
const posts = await prisma.post.findMany({});
const pages = [];
while (posts.length) {
pages.push(posts.splice(0, pageSize));
}
const prev = page === 1 ? undefined : page - 1;
const next = page === pages.length ? undefined : page + 1;
res.render("list", {
posts: pages[page - 1],
prev: prev,
next: next,
});
});
app.listen(port, () => {
console.log(
`Blog application listening on port ${port}. Visit http://localhost:${port}.`
);
});
在这个例子中,页面大小设置为 5,这意味着每页将显示 5 篇文章。
第 21 行page是当前页码。
第 22 行posts是一个数组,其中包含数据库中存储的所有帖子。
第 24 至 27 行,我们根据页面大小分割数组。该splice(index, count)方法接受两个参数,`a`index和 ` countb`。它从数组的 `a` 开始进行分割,并返回count元素个数index。数组的剩余部分将被赋值给 `b` posts。
第 29 行和第 30 行分别根据当前页码指向上一页和下一页。
const prev = page === 1 ? undefined : page - 1;
const next = page === pages.length ? undefined : page + 1;
如果当前页码page为 1,prev则等于 1 ,undefined因为在这种情况下没有上一页。否则,等于 0 page - 1。
next另一方面,undefined如果当前page值等于,则等于pages.length,这意味着当前值page是最后一个值。否则,等于page + 1。
最后,当前页面()的帖子pages[page - 1],以及prev和next,将被发送到相应的视图(list.pug)。
list.pug
ul
each post in posts
li
a(href="#") #{post.title}
else
li No post found.
if prev
a(href=`/pages/${prev}`) Prev
if next
a(href=`/pages/${next}`) Next
您可能已经意识到,这个方案存在一个问题。您必须先从数据库中检索所有帖子,然后才能将它们拆分成单独的页面。这会造成巨大的资源浪费,而且实际上,服务器处理如此庞大的数据量可能需要很长时间。
如何在 JavaScript 中实现基于偏移量的分页
因此,我们需要一个更好的策略。与其检索所有帖子,我们可以先根据页面大小和当前页码确定一个偏移量。这样,我们就可以跳过这些帖子,只检索我们需要的帖子。
在我们的示例中,偏移量等于pageSize * (page - 1),我们将检索pageSize此偏移量之后的帖子数量。
以下示例演示了如何使用 Prisma 实现此功能。它skip指定了偏移量,并take定义了从该偏移量开始要检索的帖子数量。
// Offset pagination
// ===========================================================
app.get("/pages/:page", async function (req, res) {
const pageSize = 5;
const page = Number(req.params.page);
const posts = await prisma.post.findMany({
skip: pageSize * (page - 1),
take: pageSize,
});
const prev = page === 1 ? undefined : page - 1;
const next = page + 1;
res.render("list", {
posts: posts,
prev: prev,
next: next,
});
});
在这种情况下,前端保持不变。
list.pug
ul
each post in posts
li
a(href="#") #{post.title}
else
li No post found.
if prev
a(href=`/pages/${prev}`) Prev
if next
a(href=`/pages/${next}`) Next
当然,其他 ORM 框架也能实现同样的功能,但逻辑基本相同。在本教程的最后,我们将提供一些资源,帮助您使用其他 ORM 框架创建 JavaScript 分页系统。
如何在 JavaScript 中实现无限滚动
除了基于偏移量的分页之外,还有一种常用的替代方法,称为基于光标的分页。这种方法通常用于创建无限滚动或“加载更多”按钮。
顾名思义,基于游标的分页需要游标。当用户首次访问帖子列表时,游标指向数组中的最后一个元素。
当用户点击“加载更多”按钮时,系统会向后端发送请求,后端返回下一批文章。前端接收传输的数据,并以编程方式渲染新文章,同时更新光标,指向这批新文章的最后一项。
实际实现这种基于游标的分页功能时,情况会变得稍微复杂一些,因为这种策略需要前端和后端协同工作。不过别担心,我们会一步一步地讲解。
首先,我们创建根路由(/)。当用户访问此页面时,将检索前十篇文章,并将cursor指向id最后一篇文章的页面。回想一下,该路由at(-1)会检索数组的最后一个元素。
//Cursor-based pagination (load more)
// ===========================================================
const pageSize = 10;
app.get("/", async function (req, res) {
const posts = await prisma.post.findMany({
take: pageSize,
});
const last = posts.at(-1);
const cursor = last.id;
res.render("list", {
posts: posts,
cursor: cursor,
});
});
请注意,光标也会同步传输到前端。这一点非常重要,您必须确保两端的光标始终保持同步。
list.pug
button(id="loadMore" data-cursor=`${cursor}`) Load More
ul(id="postList")
each post in posts
li
a(href="#") #{post.title}
else
li No post found.
script(src="/statics/js/app.js")
光标的初始值将保存在“加载更多”data-cursor按钮的属性中,前端的 JavaScript 代码可以访问该属性。在本例中,我们将所有前端 JavaScript 代码都放在./statics/js/app.js
/statics/js/app.js
document.addEventListener("DOMContentLoaded", function () {
const loadMoreButton = document.getElementById("loadMore");
const postList = document.getElementById("postList");
let cursor = loadMoreButton.getAttribute("data-cursor");
loadMoreButton.addEventListener("click", function () {
fetch("/load", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
cursor: cursor,
}),
})
. . .
});
});
点击“加载更多”按钮后,系统会发送一个 POST 请求来/load获取下一批帖子。请注意,您需要将帖子数据发送cursor回服务器,以确保它们始终保持同步。
接下来,创建一个路由处理程序/load。该路由处理程序接收游标并从数据库中检索接下来的十篇文章。请记住跳过一篇文章,以避免游标指向的文章重复出现。
app.post("/load", async function (req, res) {
const { cursor } = req.body;
const posts = await prisma.post.findMany({
take: pageSize,
skip: 1,
cursor: {
id: Number(cursor),
},
});
const last = posts.at(-1);
const newCursor = last.id;
res.status(200).json({
posts: posts,
cursor: newCursor,
});
});
该处理程序会将200OK响应连同检索到的帖子一起发送回前端,前端 JavaScript 代码将再次接收这些响应。
/statics/js/app.js
document.addEventListener("DOMContentLoaded", function () {
const loadMoreButton = document.getElementById("loadMore");
const postList = document.getElementById("postList");
let cursor = loadMoreButton.getAttribute("data-cursor");
loadMoreButton.addEventListener("click", function () {
fetch("/load", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
cursor: cursor,
}),
})
.then((response) => response.json())
.then((data) => {
if (data.posts && data.posts.length > 0) {
data.posts.forEach((post) => {
const li = document.createElement("li");
const a = document.createElement("a");
a.href = "#";
a.textContent = post.title;
li.appendChild(a);
postList.appendChild(li);
});
cursor = data.cursor;
} else {
loadMoreButton.textContent = "No more posts";
loadMoreButton.disabled = true;
}
})
.catch((error) => {
console.error("Error loading posts:", error);
});
});
});
结论
偏移策略和光标策略各有优缺点。例如,如果要跳转到特定页面,偏移策略是唯一选择。
然而,这种策略在数据库层面无法扩展。如果想要跳过前1000条记录,只取前10条,数据库必须先遍历前1000条记录才能返回所需的10条记录。
游标策略更容易扩展,因为数据库可以直接访问指向的项并返回接下来的 10 个项。但是,使用游标无法跳转到特定页面。
最后,在本教程结束之前,如果您正在使用不同的 ORM 框架创建分页系统,这里有一些您可能会觉得有用的资源。
祝您编程愉快!
延伸阅读
- JavaScript 中的数据类型有哪些?
- JavaScript 中的 Map 和 Set 是什么?
- JavaScript 中的高阶函数是什么?
- JavaScript 和异步编程
- 如何优化您的 Web 应用程序以获得更好的性能





