如何使用 MySQL 对 Node.js 进行分页?
Node.js 是一个类似于 Chrome 的 V8 JavaScript 引擎的运行时环境。 Node.js 是一个在 Web 浏览器之外执行的开源、跨平台和后端运行时环境。
MySQL 是一个快速、可靠、灵活和健壮的开源关系数据库管理系统。 MySQL 和 Node.js 都是构建 Web 应用程序时的流行选择。 MySQL 的另一个优点是它为分页等实用程序提供了内置支持。
什么是分页,为什么它很重要?
分页只不过是将数据划分为离散的块或页面。将数千条记录划分为页面的网页感觉更吸引人、更具交互性,并且对应用程序性能更好。因此,分页有助于更好的显示、更好的性能、更好的用户体验,因为它不会让用户被数据淹没并避免长时间滚动。
我们既可以进行客户端分页,也可以进行服务器端分页。在本文中,我们将看到一个服务器端分页的示例。
服务器端分页:根据 IBM,服务器端分页用于:
- 大数据集。
- 更快的初始页面加载。
- 那些不运行 JavaScript 的人的可访问性。
- 复杂的视图业务逻辑。
- 对并发更改的弹性。
服务器端分页通常在中间件(业务逻辑)代码或数据库层中完成。服务器端分页通常比客户端更具挑战性,但它的扩展性更好。
客户端分页:客户端分页适用于以下情况:
- 数据集很小。
- 更快的后续页面加载
- 完全支持排序和过滤要求(除非结果大于最大大小)。
客户端分页实现起来更快,但扩展性不是很好。
执行服务器端分页的步骤?
我们将看一个服务器端分页的例子。我们将在数据库级别本身处理分页。对于使用 MySQL 的分页,我们需要使用带有 Offset 值的 LIMIT 子句。限制条款仅检索部分记录。 Limit 子句的基本语法如下:
Select
From
Where
LIMIT , ;
偏移量是可选的,默认值为 0,但可以获得小于数据集中记录数的任何正值。
示例应用程序:我们将使用 Node.js、Express.js、MySQL 和 Sequelize ORM 构建一个简单的应用程序。确保您的系统上有 Node.js 和 MySQL。我们将使用 Visual Studio Code 来开发应用程序。
创建项目文件夹并更改文件夹
mkdir PaginationExample
cd PaginationExample
初始化应用程序
npm init
生成 package.json 文件。
接下来,我们为模板安装 express.js、sequelize 和 pug。
npm install -g express sequelize pug dotenv express-paginate
我们还需要安装 dotenv 和 express-paginate 包。 Express-paginate 包公开了各种方法,如 href 和中间件。文档中给出了函数的详细信息。
我们的文件夹结构如下。
要运行节点应用程序,我们需要从 Visual Studio Code 的终端运行以下命令。
node server.js
如果一切正常,您应该会在终端中看到与此类似的输出:
看到此消息后,您可以打开浏览器并转到链接:localhost:8000
我们已经在应用程序中构建了调用,我们应该直接看到一个包含记录的表和一个分页选项。
应用程序代码:我们将逐层查看应用程序代码文件。
Server.js 文件: server.js 是主文件,包含您所有与 express 相关的配置,也是我们获取记录和调用服务文件的唯一途径。服务器文件具有以下代码。
Javascript
// Required External modules
const express = require("express");
const path = require("path");
require("dotenv").config();
const paginate = require("express-paginate");
// Required code files
const services = require("./service/services.js");
// Application Variables
const app = express();
const port = 8000;
// Server
app.listen(port, () => {
console.log(`App running on port ${port}.`);
});
// Configuration
app.set("views", path.join(__dirname, "./views"));
app.set("view engine", "pug");
app.use("/static", express.static(
path.join(__dirname, "public")));
app.use(paginate.middleware(10, 50));
// Routes
app.get("/", (req, res) => {
const limit = req.query.limit || 10;
const offset = req.offset;
services.findRecords({
offset: offset,
limit: limit
}).then((results) => {
const pageCount = Math.ceil(results.count / limit);
res.render("paginatedTable", {
data: results.rows,
pageCount,
pages: paginate.getArrayPages(req)
(3, pageCount, req.query.page),
});
});
});
Javascript
const Sequelize = require("sequelize");
module.exports = new Sequelize({
dialect: "mysql",
username: process.env.DB_USER,
password: process.env.DB_PASS,
host: process.env.DB_HOST,
port: process.env.DB_PORT,
database: process.env.DB_DATABASE,
logging: (log) => console.log("logging:", log),
});
Javascript
var Sequelize = require("sequelize");
db = require("../config/dbconfig.js");
const nicer_but_slower_film_list = db.define(
"nicer_but_slower_film_list", {
FID: {
type: Sequelize.SMALLINT,
// To ensure that Sequelize
// does not use id by default
primaryKey: true,
},
title: Sequelize.STRING,
description: Sequelize.STRING,
category: Sequelize.STRING,
price: Sequelize.DECIMAL,
length: Sequelize.SMALLINT,
rating: Sequelize.ENUM("G", "PG", "PG-13", "R", "NC-17"),
actors: Sequelize.STRING,
},
{
// This is to ensure that Sequelize
// does not pluralize table names
freezeTableName: true,
// This is to ensure that Sequelize
// does not add its own timestamp
// variables in the query.
timestamps: false,
createdAt: false,
updatedAt: false,
}
);
module.exports = nicer_but_slower_film_list;
Javascript
const Sequelize = require("sequelize");
// Model file
var model = require("../models/models.js");
// db Configuration
db = require("../config/dbconfig.js");
let findRecords = async (req, res) => {
return model.findAndCountAll({
offset: req.offset,
limit: req.limit
});
};
module.exports = { findRecords: findRecords };
HTML
html
head
link(rel='stylesheet' href='https://getbootstrap.com/docs/4.4/dist/css/bootstrap.min.css')
style
include ../public/style.css
body
h1 Movies
table
thead
tr
th Title
th Description
th Category
th Length
th Rating
th Actors
tbody
each dat in data
tr
td #{dat.title}
td #{dat.description}
td #{dat.category}
td #{dat.length}
td #{dat.rating}
td #{dat.actors}
if paginate.hasPreviousPages || paginate.hasNextPages(pageCount)
.navigation.well-sm#pagination
ul.pager
if paginate.hasPreviousPages
a(href=paginate.href(true)).prev
i.fa.fa-arrow-circle-left
| Previous
if pages
each page in pages
a.btn.btn-default(href=page.url)= page.number
if paginate.hasNextPages(pageCount)
a(href=paginate.href()).next
| Next
i.fa.fa-arrow-circle-right
script(src='https://code.jquery.com/jquery-3.4.1.slim.min.js')
script(src='https://getbootstrap.com/docs/4.4/dist/js/bootstrap.bundle.min.js')
CSS
table {
width: 100%;
border: 1px solid #fff;
border-collapse: collapse;
border-radius: 8px;
}
th,
td {
text-align: left;
text-transform: capitalize;
border: 1px solid darkgrey;
color: black;
}
th {
padding: 8px 10px;
height: 48px;
background-color: #808e9b;
}
td {
padding: 6px 8px;
height: 40px;
}
a:hover {
background-color: #555;
}
a:active {
background-color: black;
}
a:visited {
background-color: #ccc;
}
Sequelize 文件:我们将数据库配置、Sequelize 模型和调用分成三个单独的文件,以便在应用程序扩展时更容易维护。
- Services 文件包含我们所有的 Sequelize 调用。
- models.js 包含我们用于查询的表结构。我们使用 SAKILA 数据库中的 nicer_but_slower_film_list 表作为示例。
- dbconfig.js 文件包含 Sequelize 对象。此存储库中提供了与文件相关的完整代码。
- Sequelize 提供了一个内置方法:findAndCountAll,非常适合分页。 findAndCountAll 方法接受参数 offset 和 limit 并根据限制和偏移值返回可用的总记录和实际记录。代码如下:
dbconfig.js : dbconfig 保存 Sequelize 对象。创建 Sequelize 对象的属性来自基于您的数据库设置的 .env 文件。在这里,我们创建了一个简单的数据库对象。
Javascript
const Sequelize = require("sequelize");
module.exports = new Sequelize({
dialect: "mysql",
username: process.env.DB_USER,
password: process.env.DB_PASS,
host: process.env.DB_HOST,
port: process.env.DB_PORT,
database: process.env.DB_DATABASE,
logging: (log) => console.log("logging:", log),
});
models.js: models.js 文件包含我们在查询中使用的表的描述。它是关系数据库表的 Sequelize 表示。
Javascript
var Sequelize = require("sequelize");
db = require("../config/dbconfig.js");
const nicer_but_slower_film_list = db.define(
"nicer_but_slower_film_list", {
FID: {
type: Sequelize.SMALLINT,
// To ensure that Sequelize
// does not use id by default
primaryKey: true,
},
title: Sequelize.STRING,
description: Sequelize.STRING,
category: Sequelize.STRING,
price: Sequelize.DECIMAL,
length: Sequelize.SMALLINT,
rating: Sequelize.ENUM("G", "PG", "PG-13", "R", "NC-17"),
actors: Sequelize.STRING,
},
{
// This is to ensure that Sequelize
// does not pluralize table names
freezeTableName: true,
// This is to ensure that Sequelize
// does not add its own timestamp
// variables in the query.
timestamps: false,
createdAt: false,
updatedAt: false,
}
);
module.exports = nicer_but_slower_film_list;
services.js:服务文件包含我们执行的 Sequelize 调用。该文件将保存搜索、创建、更新、删除等调用。该文件取决于 Sequelize 对象 (dbconfig.js) 和 Sequelize 模型 (models.js)。
Javascript
const Sequelize = require("sequelize");
// Model file
var model = require("../models/models.js");
// db Configuration
db = require("../config/dbconfig.js");
let findRecords = async (req, res) => {
return model.findAndCountAll({
offset: req.offset,
limit: req.limit
});
};
module.exports = { findRecords: findRecords };
所以如果我们设置的limit是10,offset是20(也就是page 3条记录),那么由findAndCountAll形成并在数据库中触发的查询是:
SELECT `FID`, `title`, `description`, `category`, `price`, `length`, `rating`, `actors` FROM `nicer_but_slower_film_list` AS `nicer_but_slower_film_list` LIMIT 20, 10;
该查询从数据库中提供以下结果:
屏幕上显示的输出如下:
应用程序 UI:除了上面提到的文件之外,项目结构还有 node_modules 文件夹、node 和 express 安装文件以及 .env 文件。 .env 文件包含数据库相关信息,如用户名、密码、MySQL 端口号等,我们在 dbconfig.js 文件中使用这些信息来构建 Sequelize 连接对象。
用户界面:为了处理用户界面,我们使用 PUG 模板。我们使用 express-paginate 方法来处理 PUG 模板中的分页控件。以下代码处理是否显示上一个和下一个按钮链接。
paginatedTable.pug:这是带有分页结果的用户界面。
HTML
html
head
link(rel='stylesheet' href='https://getbootstrap.com/docs/4.4/dist/css/bootstrap.min.css')
style
include ../public/style.css
body
h1 Movies
table
thead
tr
th Title
th Description
th Category
th Length
th Rating
th Actors
tbody
each dat in data
tr
td #{dat.title}
td #{dat.description}
td #{dat.category}
td #{dat.length}
td #{dat.rating}
td #{dat.actors}
if paginate.hasPreviousPages || paginate.hasNextPages(pageCount)
.navigation.well-sm#pagination
ul.pager
if paginate.hasPreviousPages
a(href=paginate.href(true)).prev
i.fa.fa-arrow-circle-left
| Previous
if pages
each page in pages
a.btn.btn-default(href=page.url)= page.number
if paginate.hasNextPages(pageCount)
a(href=paginate.href()).next
| Next
i.fa.fa-arrow-circle-right
script(src='https://code.jquery.com/jquery-3.4.1.slim.min.js')
script(src='https://getbootstrap.com/docs/4.4/dist/js/bootstrap.bundle.min.js')
hasPrevious 和 hasNext 是 express-paginate 包公开的两个方法,它们返回布尔值。根据这些布尔值的值,UI 会显示 Next 和 Previous 按钮。
样式.css
该页面的样式表如下:
CSS
table {
width: 100%;
border: 1px solid #fff;
border-collapse: collapse;
border-radius: 8px;
}
th,
td {
text-align: left;
text-transform: capitalize;
border: 1px solid darkgrey;
color: black;
}
th {
padding: 8px 10px;
height: 48px;
background-color: #808e9b;
}
td {
padding: 6px 8px;
height: 40px;
}
a:hover {
background-color: #555;
}
a:active {
background-color: black;
}
a:visited {
background-color: #ccc;
}
该应用程序是如何工作的?
- 第一次访问 localhost:8000 时,由于 express-paginate 中间件,Limit 值默认设置为 10,偏移量设置为 0。因此从数据库中检索前十条记录并显示。
- 当用户点击 Next 按钮或页码,即 1、2 或 3 时,分页中间件计算偏移量。计算偏移量的公式很简单:
pageNumber(we see in the URL on the UI) -1 * limit
pageNumber 从 1 开始。
- 我们还可以将限制增加到 50 条记录。我们不能将限制增加到超过 50 条记录,因为我们已将其指定为中间件函数中的最大限制。限制已在 server.js 文件中设置。
app.use(paginate.middleware(10, 50));
触发这样的查询:: http://localhost:8000/?page=1&limit=500 不会导致错误,但显示的记录数仍然是 50。我们还可以增强功能以显示一些仅显示的消息一次可查看 50 条记录。
简介:本文展示了分页如何使用来自 MySQL 的示例数据库与 Node.js 和 MySQL 一起工作。我们还看到了如何限制用户在页面上仅查看一定数量的记录,以免导致 UI 中断。整个代码可在 Github 链接上找到。