📜  如何使用 MySQL 对 Node.js 进行分页?

📅  最后修改于: 2022-05-13 01:56:12.653000             🧑  作者: Mango

如何使用 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形成并在数据库中触发的查询是:

该查询从数据库中提供以下结果:

数据库结果

屏幕上显示的输出如下:

应用程序 UI 中显示的记录

应用程序 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 条记录。

UI 仅显示 50 条记录

简介:本文展示了分页如何使用来自 MySQL 的示例数据库与 Node.js 和 MySQL 一起工作。我们还看到了如何限制用户在页面上仅查看一定数量的记录,以免导致 UI 中断。整个代码可在 Github 链接上找到。