发布于 2026-01-06 0 阅读
0

如何使用 JavaScript 实现分页

如何使用 JavaScript 实现分页

分页是将大量数据分割成若干个较小的独立页面,使用户更容易处理和理解信息的过程。在本教程中,我们将演示如何用三种不同的方式实现JavaScript分页系统。

为什么需要 JavaScript 分页

创建分页系统有很多好处。想象一下,你的博客有成千上万篇文章。如果要把所有文章都列在一页上,那肯定不行。但你可以创建一个分页系统,让用户可以浏览不同的页面。

分页还可以降低服务器负载,因为每次请求时只需传输一部分数据。这可以提升应用程序的整体性能,提供更好的用户体验,从而改善网站的搜索引擎优化 (SEO)。

项目准备

首先,让我们初始化一个全新的 Node.js 项目。进入你的工作目录并运行以下命令:

npm init
Enter fullscreen mode Exit fullscreen mode
npm install express pug sqlite3 prisma @prisma/client
Enter fullscreen mode Exit fullscreen mode

在本课中,我们将使用Prisma.js作为 ORM 示例,但请记住,我们的重点是分页背后的逻辑,而不是工具本身。

使用以下命令初始化 Prisma:

npx prisma init
Enter fullscreen mode Exit fullscreen mode

请创建一个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
Enter fullscreen mode Exit fullscreen mode
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?
}
Enter fullscreen mode Exit fullscreen mode

第 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
Enter fullscreen mode Exit fullscreen mode

.env

DATABASE_URL = "file:database.sqlite";
Enter fullscreen mode Exit fullscreen mode

第 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
Enter fullscreen mode Exit fullscreen mode

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);
  });
Enter fullscreen mode Exit fullscreen mode

然后,您必须告诉 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"
  }
}
Enter fullscreen mode Exit fullscreen mode

最后,执行以下命令运行迁移:

npx prisma migrate dev
Enter fullscreen mode Exit fullscreen mode

如何轻松实现 JavaScript 分页

当你考虑将项目分成页面时,你首先想到的最简单的逻辑是什么?

例如,你可以从数据库中检索所有文章作为一个数组,然后使用该splice()方法根据一定的页面大小将它们拆分成更小的数组。

JavaScript 分页

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}.`
  );
});
Enter fullscreen mode Exit fullscreen mode

在这个例子中,页面大小设置为 5,这意味着每页将显示 5 篇文章。

第 21 行page是当前页码。

第 22 行posts是一个数组,其中包含数据库中存储的所有帖子。

第 24 至 27 行,我们根据页面大小分割数组。该splice(index, count)方法接受两个参数,`a`index和 ` countb`。它从数组的 `a` 开始进行分割,并返回count元素个数index。数组的剩余部分将被赋值给 `b` posts

JavaScript 数组拼接

第 29 行和第 30 行分别根据当前页码指向上一页和下一页。

const prev = page === 1 ? undefined : page - 1;
const next = page === pages.length ? undefined : page + 1;
Enter fullscreen mode Exit fullscreen mode

如果当前页码page为 1,prev则等于 1 ,undefined因为在这种情况下没有上一页。否则,等于 0 page - 1

next另一方面,undefined如果当前page值等于,则等于pages.length,这意味着当前值page是最后一个值。否则,等于page + 1

最后,当前页面()的帖子pages[page - 1],以及prevnext,将被发送到相应的视图(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
Enter fullscreen mode Exit fullscreen mode

您可能已经意识到,这个方案存在一个问题。您必须先从数据库中检索所有帖子,然后才能将它们拆分成单独的页面。这会造成巨大的资源浪费,而且实际上,服务器处理如此庞大的数据量可能需要很长时间。

如何在 JavaScript 中实现基于偏移量的分页

因此,我们需要一个更好的策略。与其检索所有帖子,我们可以先根据页面大小和当前页码确定一个偏移量。这样,我们就可以跳过这些帖子,只检索我们需要的帖子。

在我们的示例中,偏移量等于pageSize * (page - 1),我们将检索pageSize此偏移量之后的帖子数量。

JavaScript 中基于偏移量的分页

以下示例演示了如何使用 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,
  });
});
Enter fullscreen mode Exit fullscreen mode

在这种情况下,前端保持不变。

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
Enter fullscreen mode Exit fullscreen mode

当然,其他 ORM 框架也能实现同样的功能,但逻辑基本相同。在本教程的最后,我们将提供一些资源,帮助您使用其他 ORM 框架创建 JavaScript 分页系统。

如何在 JavaScript 中实现无限滚动

除了基于偏移量的分页之外,还有一种常用的替代方法,称为基于光标的分页。这种方法通常用于创建无限滚动或“加载更多”按钮。

顾名思义,基于游标的分页需要游标。当用户首次访问帖子列表时,游标指向数组中的最后一个元素。

基于游标的 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,
  });
});
Enter fullscreen mode Exit fullscreen mode

请注意,光标也会同步传输到前端。这一点非常重要,您必须确保两端的光标始终保持同步。

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")
Enter fullscreen mode Exit fullscreen mode

光标的初始值将保存在“加载更多”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,
      }),
    })
      . . .
  });
});
Enter fullscreen mode Exit fullscreen mode

点击“加载更多”按钮后,系统会发送一个 POST 请求来/load获取下一批帖子。请注意,您需要将帖子数据发送cursor回服务器,以确保它们始终保持同步。

接下来,创建一个路由处理程序/load。该路由处理程序接收游标并从数据库中检索接下来的十篇文章。请记住跳过一篇文章,以避免游标指向的文章重复出现。

基于 JavaScript 分页光标的下一批

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,
  });
});
Enter fullscreen mode Exit fullscreen mode

该处理程序会将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);
      });
  });
});
Enter fullscreen mode Exit fullscreen mode

🔗下载演示项目

结论

偏移策略和光标策略各有优缺点。例如,如果要跳转到特定页面,偏移策略是唯一选择。

然而,这种策略在数据库层面无法扩展。如果想要跳过前1000条记录,只取前10条,数据库必须先遍历前1000条记录才能返回所需的10条记录。

游标策略更容易扩展,因为数据库可以直接访问指向的项并返回接下来的 10 个项。但是,使用游标无法跳转到特定页面。

最后,在本教程结束之前,如果您正在使用不同的 ORM 框架创建分页系统,这里有一些您可能会觉得有用的资源。

祝您编程愉快!

延伸阅读

文章来源:https://dev.to/thedevspace/how-to-implement-pagination-with-javascript-5066