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

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

前端工程: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 = 0AND 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_ROWSid,article_name,poster,read_num,summary,create_time,update_time
FROM article
WHERE private = 0AND 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_ROWSa.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 = 0AND 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_ROWSa.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 = 0AND 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_ROWSa.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 = 0AND 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打造个人博客(总览篇)

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

GMSL2硬件设计V1.1

一、说明 GMSL(Gigabit Multimedia Serial Links),中文名称为千兆多媒体串行链路,是Maxim公司(现属于ADI)推出的一种高速串行接口,通过同轴电缆或屏蔽双绞线(STP)传输高速串行数据,用于汽车摄像头和显示器应用。GMSL2就是指ADI专有的第二代千兆多媒体串行链路技术,传输…

RPA+AI 应用案例集合:自动推流直播

使用场景: 自动定时推流直播 使用技术: python playwright 每个解决一个小问题 During handling of the above exception, another exception occurred:Traceback (most recent call last): File "D:\pythonTryEverything\putdonwphone\not_watch_…

前端开发工程师——webpack

一.环境准备 npm init -y npm i webpack webpack-cli -D 打包命令 npx webpack ./src/main.js --modedevelopment //development开发模式 //production生产模式 npx webpack 直接运行就行 二.加载器loader 在less/stylus/css/sass/images中添加适当的样式 例如&#xff1…

Python筑基之旅-文件(夹)操作和流

目录 一、文件操作 1、文件打开与关闭 2、文件读写 3、文件操作模式 4、文件编码 二、文件夹操作 1、创建文件夹 2、删除文件夹 3、改变当前工作目录 4、获取当前工作目录 5、检查文件/文件夹是否存在 6、遍历文件夹 三、文件路径操作 1、获取绝对路径 2、构建完…

爬山算法全解析:掌握优化技巧,攀登技术高峰!

一、引言 爬山算法是一种局部搜索算法,它基于当前解的邻域中进行搜索,通过比较当前解与邻域解的优劣来更新当前解,从而逐步逼近最优解。本文将对爬山算法进行详细的介绍。 二、爬山算法简介 爬山算法是一种基于贪心策略的优化算法&#xff…

如何利用Ubuntu服务器运行深度学习项目?

一、整体思路 先配置好服务器端的软件环境(工程源码,miniconda,cuda,显卡驱动等),然后用自己电脑的pycharm远程连接服务器运行代码。一句话总结:借用服务器资源运行代码,本地pycharm…

ubuntu安装Stable Video Diffusion(SVD)让图片动起来

目录 写在前面 一、克隆或下载项目 二、下载预训练模型 三、创建环境 四、安装依赖 五、启动项目 六、解决报错 1.预训练模型下不来 2.TiffWriter.write() got an unexpected keyword argument fps 3.安装ffmpeg 4.No module named scripts 七、测试 写在前面 Stab…

深入解析内置模块OS:让你的Python代码更懂操作系统

新书上架~👇全国包邮奥~ python实用小工具开发教程http://pythontoolsteach.com/3 欢迎关注我👆,收藏下次不迷路┗|`O′|┛ 嗷~~ 目录 一、OS模块简介与基础应用 二、文件与目录操作详解 三、OS模块的高级应用:双色…

web学习笔记(五十八)

目录 1. v-model 双向数据绑定 2. 事件修饰符 3. 路径别名 4. setup语法糖 4.1 语法糖的概念 4.2 setup语法糖 5. 配置代理服务器 1. v-model 双向数据绑定 v-model 双向数据绑定只能使用在表单标签; v-model双向数据绑定原理:采用 Object.de…

解决updateByExample时属性值异常的问题(部分属性值没有使用占位符?进行占位,而是变成了属性的名称)

目录 场景简介代码片断实体类 报错信息排查原因解决测试过程解决方案 场景简介 1、程序将mybatis框架升级为3.5.9版本后执行updateByExample方法时报错 代码片断 Condition condition new Condition(MbCcsSessionConfig.class); condition.createCriteria().andEqualTo(&quo…

【openlayers系统学习】4.3VectorTile 功能交互(指针悬停在要素上时,绘制矩形框)

三、 VectorTile 功能交互(指针悬停在要素上时,绘制矩形框) 矢量切片的好处是我们可以与要素交互,因为我们在客户端上有数据。但需要注意的一件事是矢量切片针对渲染进行了优化。这意味着要素仅包含过滤和渲染所需的属性&#xf…

panic: concurrent write to websocket connection【golang、websocket】

文章目录 异常信息原由代码错误点 解决办法 异常信息 panic: concurrent write to websocket connection原由 golang 编写 websocket go版本:1.19 使用了第三方框架: https://github.com/gorilla/websocket/tree/main 代码 server.go // Copyright …

Java核心:注解处理器

Java提供了一个javac -processor命令支持处理标注有特定注解的类,来生成新的源文件,并对新生成的源文件重复执行。执行的命令大概是这样的: javac -XprintRounds -processor com.keyniu.anno.processor.ToStringProcessor com.keyniu.anno.processor.Po…

基于微信小程序的在电影线订票小程序+web管理 uniapp,vue,ssm

基于微信小程序的在电影线订票小程序web管理 uniapp,vue,ssm 相关技术 javassmuniapp微信开发者工具hbuildervueelementui前后端分离 -mysql

PointCloudLib 点云半径滤波实现 C++版本

0.展示效果 滤波之前 1.算法原理 半径滤波原理非常直观,主要用于平滑三维点云数据并去除离群点。 设定搜索半径:首先,为每个点设定一个搜索半径r。这个半径定义了该点周围的一个球形区域。计算邻域点数:接着,计算每个点在其搜索半径r内的邻近点的数量。判断与过滤:根据…

xcode按下delete键不能删除不能使用,解决办法

有可能是按键冲突导致的问题,就是你不小心把delete键绑定了不同的快捷键,所以需要恢复所有的偏好设置和快捷键才可以,我这里就是这样的提示内容,在xcode中按delete键完全无效: 而且还会报红色提示:意思是不…

KingbaseES数据库union的用法

数据库版本:KingbaseES V008R006C008B0014 文章目录如下 1. union的概念 2. union的语法 3. union的用法 3.1. 去重(union) 3.2. 不去重(union all) 3.3. 聚合运算 3.4. 异常案例 1. union的概念 UNION 是结构…

冷冻式压缩空气干燥机常见几种系统原理图

冷冻式压缩空气干燥机 我们以两种典型的设计流程图为例 1.干式蒸发型,这类冷干机是我们最为常见的设计型式。下图为deltech公司的典型流程图 此类设备各家设计不同的最大区别基本就是在换热器的结构型式上有比较大的区别。换热器主要有:管壳式、铝板换、…

typescript 配置精讲 | moduleResolution

大家好,我是17。 moduleResolution 是 typescript 模块配置中最重要的一个配置,所以 17 单拿出来讲一下。如果你去看文档还是挺复杂的,但如果不去深究细节,只想知道如何配置还是很简单的。3 分钟就能学会。 moduleResolution 的…