前端如何学会全栈分页开发?源码和思路都在这了

本项目代码已开源,具体见:

前端工程:vue3-ts-blog-frontend

后端工程:express-blog-backend

数据库初始化脚本:关注公众号程序员白彬,回复关键字“博客数据库脚本”,即可获取。

前言

这是博客系列中一篇讲具体业务的,话题是分页模型和滚动加载。

分页和滚动加载,各位前端大佬们没做一千次也做了一百次了吧。所以光说前端没多大意义,这里是准备结合前后端的视角看看分页和滚动加载的实现,本质上也不难,高手直接略过。如果您对后端或数据库还比较陌生,相信读完本文您会有所收获!

为什么要分页?

为什么要做分页,想必大家都很清楚。假设数据库某个表的数据记录很多(成千上万甚至更多),那么在业务设计上不可能一次性把表的数据全部查出来返回给前端展示,这不仅对数据库来说是一种巨大负担,对网络传输、客户端渲染也有较大压力。

所以我们需要用到分页,把数据一页一页地返给前端,像翻书一样,一次只看一页,实现一种按需取用的效果。

瀑布流滚动加载也是同理,只不过是把第一页和后续页的数据拼起来展示。

数据分页

那么怎么实现分页呢?源头还是数据库,首先要探究数据库的分页能力。如果数据库层面不能实现分页,而是把数据全部查出返给前端,那么即便前端实现一种视觉上的分页效果,其本质上也是掩耳盗铃,没有太多实际意义。

回到数据库角度,以 MySQL 为例,其分页查询的标准语法为:

SELECT * FROM `table_name` LIMIT offset, row_count 

通过关键词LIMIT来限制查询的偏移量offset和记录数量row_count

举例如下:

  • 查询第一页文章,指定一页查10篇文章。
SELECT * FROM `article` LIMIT 0, 10

image.png

0 代表没有任何偏移,所以从第一条开始,一共查询 10 条数据。

由于我删除了部分测试数据,所以 id 不是从 1 开始,不必感到疑惑,实际上 id=147 是表里的第一条记录。

  • 查询第二页文章,指定一页查10篇文章。

当我们查第二页文章时,offset 应该怎么给出呢?我们可以抽象一下,偏移量其实就是第二页之前的文章数量(此例中就是第一页的数量)。以页码为 pageNo,页大小为 pageSize,则偏移量可以这样算出:

const offset = (pageNo - 1) * pageSize

当 pageNo 为 2,pageSize 为 10 时,计算出来的 offset 也就是 10,所以我们实际得到的 sql 语句是:

// 偏移10,查10条记录
SELECT * FROM `article` LIMIT 10, 10

假设不传 offset,LIMIT 后代表的就是 row_count,而 offset 也就自然等价于 0,即从第一条记录开始查询。

基于此,我们还可以通过左连接关联作者、分类、标签等信息,结合时间排序、WHERE判断等,给出一个业务上实际需要的文章分页功能。

案例分析

确定数据结构

我们先看下博客首页的效果,文章列表就是一个分页模型。

我们先观察 UI 上的整体效果,再分析后端需要提供什么数据,以及数据以什么样的结构返回。

  • 首先,分页每页的数据都是一个数组,这个没有太多的疑问。
  • 前端需要知道一共有多少页,或者一共有多少篇文章,才能知道如何展示总页数。
  • 除文章基础信息外,分类/标签/作者等信息需要从其他表关联得来。

根据本项目实现的效果,我们会提供下面这样的数据结构:

{
    "code": "0",
    "data": [
        {
            "id": 文章id,
            "article_name": "标题",
            "poster": "封面图",
            "read_num": 阅读量,
            "summary": "摘要信息",
            "create_time": "创建时间",
            "update_time": "修改时间",
            "author": "作者名",
            "categories": [
                {
                    "id": 分类id,
                    "categoryName": "pnpm"
                },
                {
                    "id": 分类id,
                    "categoryName": "TypeScript"
                }
            ],
            "tags": [
                {
                    "id": tag id,
                    "tagName": "pnpm"
                },
            ]
        },
        // ...其他文章
    ],
    "total": 文章总数
}

查询主表基本信息

其中data就是文章数组,其中的文章基本信息都来源于article表,这个可以通过SELECT语句查询得来。

SELECT id,
       article_name,
       poster,
       read_num,
       summary,
       create_time,
       update_time
FROM article
WHERE private = 0
  AND deleted = 0
ORDER BY create_time DESC
LIMIT 0, 10;

通过WHERE来加上一些限定条件,避免私密文章或者已逻辑删除的文章被查出。

第一页通常是看最新发布的文章,所以我们使用ORDER BYDESC实现一个按创建时间降序查询。

最后是使用LIMIT做一个偏移和数量限制,本质上也就是分页查询。

image.png

分页总数怎么查?

有了列表,就可以在 nodejs 响应中返回 data 数组了,但是文章总数total怎么来呢?这里提供两种方式,但是性能的对比我就不擅长了,请自行查阅相关资料,毕竟咱不是专业后端开发。

第一种,我们知道 MySQL 提供了 COUNT 函数,它是可以提供总数统计的。

SELECT COUNT(*) FROM article;

第二种,利用SQL_CALC_FOUND_ROWSFOUND_ROWS()也可以做到同样效果。

SELECT SQL_CALC_FOUND_ROWS
  id,
  article_name,
	poster,
	read_num,
	summary,
	create_time,
	update_time
FROM article
WHERE private = 0
  AND deleted = 0
ORDER BY create_time DESC
LIMIT 0, 10;

SELECT FOUND_ROWS() as total;

image.png

那么到底用哪种方式性能更好呢?其实我心里也没底,之前也没有过多关注这个问题,因为脱离实际情况的性能优化都是扯淡。今天写到这里时,顺手查询了一下 MySQL 官方手册,发现 MySQL 推荐我们使用 COUNT(*)

这,,,我好像第一版实现就是用的 COUNT(*),后面看了一些相关博客,才改成了FOUND_ROWS,这就有点尴尬了,哈哈哈。此问题具体见The SQL_CALC_FOUND_ROWS query modifier and accompanying FOUND_ROWS() function are deprecated。

image.png

但是我仔细想了一下,COUNT(*) 有一点不好的在于,当查询语句带了 WHERE 限定条件时,前后语句的条件必须得一致,如果漏了条件就容易出事!

举例,当我们只查询 id 大于 200 的分页数据时,使用 COUNT(*) 很容易忘记写条件,而使用 FOUND_ROWS() 就不用太过于担心,因为它与 SQL_CALC_FOUND_ROWS 修饰符一起保证了前后是一致的。

针对 COUNT(*) 的这种问题,可能就需要对 SQL 语句的调用做封装了,避免人为出错,或者是不是通过 ORM 等工具解决这个问题。我目前还是裸写 SQL 比较多,后续再考虑上 ORM。

分页过程的关联表信息

拿到了文章主表的基本信息后,我们还需要展示分类、标签、作者等信息,而这些信息是存储在其他表中,关联关系是靠外键或者关系表维护起来的。

我们先看作者信息,在设计数据库时,我考虑的是一篇文章只有一个作者,所以文章和作者的关系是一对一,而一个作者可以有多篇文章。针对这种关系,我们使用外键约束即可,在文章表中使用外键author_id去引用用户表的主键id

在查询作者信息时,通过LEFT JOIN就能带出作者名。

SELECT SQL_CALC_FOUND_ROWS
  a.id,
  // ......省略部分 article 表字段
  a.update_time,
  u.nick_name AS author
FROM article a
LEFT JOIN user u ON a.author_id = u.id
WHERE a.private = 0
  AND a.deleted = 0
ORDER BY a.create_time DESC
LIMIT 0, 10;

SELECT FOUND_ROWS() as total;

image.png

针对文章分类信息,因为一篇文章可能属于多个分类,而一个分类下也能有多篇文章,这是一种多对多关系。这里采用的是关系表作为中间表来维护关系。我们继续用LEFT JOIN来查出分类名称。

SELECT SQL_CALC_FOUND_ROWS
  a.id,
  // ......省略部分 article 表字段
  a.update_time,
  u.nick_name AS author,
  c.category_name
FROM article a
LEFT JOIN user u ON a.author_id = u.id
LEFT JOIN article_category a_c ON a.id = a_c.article_id
LEFT JOIN category c ON a_c.category_id = c.id
WHERE a.private = 0
  AND a.deleted = 0
ORDER BY a.create_time DESC
LIMIT 0, 10;

SELECT FOUND_ROWS() as total;

分类数据是关联出来了,但同时我们也发现了一个问题,部分同一个id值的文章(也就是同一篇文章)出现了两次以上。

image.png

这是因为有的文章关联了2个以上的分类,通过左连接查询自然就会出现多条记录。此时我们要用到分组,也就是 GROUP BY;同时为了将合并后的分类信息作为一列展示,我们还需要用到 GROUP_CONCAT()

SELECT SQL_CALC_FOUND_ROWS
  a.id,
  // ......省略部分 article 表字段
  a.update_time,
  u.nick_name AS author,
  GROUP_CONCAT(DISTINCT c.category_name SEPARATOR ",") AS categoryNames
FROM article a
LEFT JOIN user u ON a.author_id = u.id
LEFT JOIN article_category a_c ON a.id = a_c.article_id
LEFT JOIN category c ON a_c.category_id = c.id
WHERE a.private = 0
  AND a.deleted = 0
GROUP BY a.id
ORDER BY a.create_time DESC
LIMIT 0, 10;

SELECT FOUND_ROWS() as total;

这样我们就离想要的结果越来越近了。

image.png

类似地,我们可以把分类 id,标签 id,标签 name 等信息也关联出来。用到分类 id,主要是为了方便提供分类页面的链接,这样就可以实现点击分类名称跳转到分类的详情页面,标签也是同理。

数据库部分设计大概就讲到这里了,后端 nodejs 代码主要就是对以上逻辑的封装,不再展开叙述,具体可以 clone 源码查看。

分页的前端呈现

前端部分大家都比较熟悉了,不太需要深入分析。分页模型中,前端列表永远只展示当前页的数据,也就是 data 返回什么,就展示什么,不存在拼接数据问题。

滚动加载的前端呈现

滚动加载与分页模型最大的不同在于,数据是需要拼接起来的,每查到一页新数据,都需要通过concat等手段将数组拼接起来。

随着不断滚动呢,数据会越来越多,如果为了性能考虑,可能还会出现虚拟滚动等需求;而为了视觉美观效果,则会出现不定高自适应瀑布流的需求。不过这些,都不在本文研究范围之内,仅引出一些拓展的话题!

小结

本文主要分享了我在设计分页和瀑布流业务时的一些思考,主要讲的也是核心的数据设计思路,而业务代码部分则没有选择重点叙述,感兴趣的朋友可以简单看看源码,链接都附在文章开头了。

  • 专栏导航:Vue3+TS+Node打造个人博客(总览篇)

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/648088.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

商标注册申请名称的概率,多想名称选通过率好的!

近日给深圳客户申请的商标初审下来了,两个类别都下的初审,和当初的判断基本一致,普推知产老杨当时沟通说需要做担保申请注册也可以,后面选择了管家注册,最近大量的帮客户检索商标名称,分享下经验。 两个字基…

STM32H7系统窗口看门狗 (WWDG)应用方法介绍

目录 概述 1 认识窗口看门狗 (WWDG) 1.1 窗口看门狗定义 1.2 WWDG 主要特性 2 WWDG 功能说明 2.1 WWDG框图 2.2 WWDG 内部信号 2.3 控制递减计数器 2.4 看门狗中断高级特性 2.5 如何设置看门狗超时 3 WWDG 寄存器 3.1 控制寄存器 (WWDG_CR) 3.2 配置寄存器 (W…

如何调用通义千问大模型API

目录 登录阿里云 大模型服务平台百炼 登录控制台 QWen Long QWen 通义千问开源系列 大语言模型 OpenAI接口兼容 登录阿里云 阿里云-计算,为了无法计算的价值 大模型服务平台百炼 降价信息: 登录控制台 右上角取得API key 创建Key QWen Long qw…

C#的奇技淫巧:利用WinRM来远程操控其他服务器上的进程

前言:有时候远程服务器的进程你想偷偷去围观一下有哪些,或者对一些比较调皮的进程进行封杀,或者对一些自己研发的服务进行远程手动启动或者重启等,又不想打开远程桌面,只想悄咪咪地执行,那也许下面的文章会…

关于解决Qt在安装的时候没有勾选sources组件的方法

关于解决Qt在安装的时候没有勾选sources组件的方法 一、引言 在安装数据库连接到qt的时候发现没有sources文件夹,原来是安装的时候没有勾选sources组件,发现问题后找到了维护qt组件的安装方式,特此记下来 二、分析原因 首先在安装的时候就…

Lookin高效调试iOS App的UI

Lookin是一款iOS开发时常用的调试软件,由腾讯微信读书团队QMUI开发。 它可以查看和修改iOS App里的UI对象的软件,展示App UI图层,类似于Xcode自带的UI Inspector工具,或另一款叫做Reveal的软件。 此外,虽然Lookin主体…

【C++语言】继承:类特性的扩展,重要的类复用!

【C语言】继承,更进一步的复用 ✨精美思维导图奉上继承1. 继承的相关概念:2. 继承的定义:(1)定义格式:(2)访问限定符和继承方式:(3)默认继承方式&…

C++_C++11的学习

1. 统一的列表初始化 1.1{}初始化 在C98 中,标准就已经允许使用花括号 {} 对数组或者结构体元素进行统一的列表初始值设定。而到了C11,标准扩大了用大括号括起的列表 ( 初始化列表 )的使用范围,使其能适用于所有的内…

最大连续1的个数(滑动窗口)

算法原理: 这道题大眼一看是关于翻转多少个0的问题,但是,如果你按照这种思维去做题,肯定不容易。所以我们要换一种思维去做,这种思维不是一下就能想到的,所以想不到也情有可原。 题目是:给定一…

ESP32-C6接入巴法云,Arduino方式

ESP32-C6接入巴法云,Arduino方式 第一、ESP32-C6开发环境搭建第一步:安装arduino IDE 软件第二步:安装esp32库第三:arduino 软件设置 第二:简单AP配网程序第一步:程序下载第二步:程序使用第三步…

linux centos nginx配置浏览器访问后端(tomcat日志)

1、配置nginx访问tomcat日志路径 vim /usr/local/nginx/conf/nginx,conflocation ^~ /logs {autoindex on;autoindex_exact_size on;autoindex_localtime on;alias /home/tomcat/apache-tomcat-9.0.89-1/logs;}###配置讲解### 1、location ^~ /logs { … }: location&#xf…

代码随想录——从前序与中序遍历序列构造二叉树(Leetcode105)

题目链接 递归 /*** Definition for a binary tree node.* public class TreeNode {* int val;* TreeNode left;* TreeNode right;* TreeNode() {}* TreeNode(int val) { this.val val; }* TreeNode(int val, TreeNode left, TreeNode right) {* …

构建智能化商场存包柜平台的数据结构设计

随着城市生活节奏的加快,人们对于便利的需求也越来越迫切。在城市中,商场存包柜平台成为了解决人们日常出行中行李存放问题的重要设施。为了更好地管理和运营这些存包柜,智能化商场存包柜平台的数据结构设计显得尤为关键。 一、需求分析与功能…

每日AIGC最新进展(12):在舞蹈视频生成中将节拍与视觉相融合、Text-to-3D综述、通过内容感知形状调整进行 3D 形状增强

Diffusion Models专栏文章汇总:入门与实战 Dance Any Beat: Blending Beats with Visuals in Dance Video Generation https://DabFusion.github.io 本文提出了一种名为DabFusion的新型舞蹈视频生成模型,该模型能够根据给定的静态图像和音乐直接生成舞蹈…

韩顺平0基础学Java——第11天

p234-249 又一个月了,时间过得好快啊,希望支棱起来 可变参数 public int sum(int ... nums){ } 这个nums是数组 细节: 1可变参数可以为0个,或任意个 2可变参数的实参可以为数组 3可变参数的本质就是数组 4可变参数可以和普通…

MicroLED:苹果对知识产权的影响

Yole的洞察揭示,MicroLED IP在经历了七年的爆炸式增长后,已然屹立于行业之巅。苹果公司,作为微LED领域的先行者,早在2014年便敏锐地捕捉到Luxvue这家初创公司的潜力,将其纳入麾下,引发了业界的广泛关注。然…

基线管理概述

一、基线概念 ①安全基线 ②安全基线与英文排版的基线类似,是一条参考标准线。 ③安全基线表达了最基本需要满足的安全要求。 ④安全基线表达了安全的木桶原理木桶原理:一只木桶盛水的多少,并不取决于桶壁上最高的那块 木块,而恰恰取决于…

如何让大模型更聪明?提升AI智能的关键策略

如何让大模型更聪明?提升AI智能的关键策略 🤖 如何让大模型更聪明?提升AI智能的关键策略摘要引言方向一:算法创新🚀1.1 自监督学习的崛起1.2 强化学习的应用 方向二:数据质量与多样性📊2.1 数据…

大学校园广播“录编播”与IP广播系统技术方案

一、项目概述 1、校园IP网络广播系统概述 大学校园广播系统是学校整个弱电系统中的子系统,它是每个学校不可缺少的基础设施之一,在传递校园文化、传播校园新闻资讯方面发挥着重要的作用。近几年来,虽然视频技术和网络技术在飞速发展&#xf…

VS2022配合Qt与boost.asio实现一个TCP异步通信系统远程操作mysql数据库

上一篇博客我们通过boost.asio搭建了一个简单的异步服务器,但是那是基于命令行的,所有用起来还是相当枯燥的,这次我们配合Qt实现一个简陋的前端页面来控制后端mysql数据库中的表,实现添加密钥的功能(本次博客使用的boost版本是1.8…