实现一个嵌套结构的评论功能

现在大部分评论实际上都不再是多级嵌套结构的了,基本都是二级结构,即评论和回复,回复是多级的,但实际上以二级结构展示的,包括掘金,网易云等等,从样式上来说都是这样的。

因而最主要的问题是,对这类数据该如何分页?

我最开始是没有分页的,也就是后台把数据一次性返给前端,只是让前端在视觉上呈现分页的感觉,但这样的分页没有任何意义,效果如下:

我觉得对评论分页,其实只需要筛选出评论数据,即pid=0的数据,针对这些数据进行分页,并且对评论下的默认回复,其实不需要一次性全部拿出来,比如掘金这样:

点开的时候,获取评论下的所有回复:

针对这个功能,再进行优化: 之前想的是使用递归查询(WITH RECURSIVE)来获取pid为0的数据及其子数据,但不能很好解决这个问题,主要是分页不好做。

其实拆成两张表,一张评论表,一张回复表感觉应该更好些,但不想去动表了,就查询两次吧。

其实查询两次也有好处: 首先尽管是同一张表,但是查询时的目的不同,不同查询展示的数据也是不同的。 其次,如果后续有需要,还可以对回复进行分页。

第一次分页查询,查询出所有pid为0的数据,并取得所有id,并递归获取其所有子回复的数据。 第二次查询,根据id集合,查询出相关的所有回复数据,返回前端。

代码如下: 数据处理dao层:

sql 复制代码
let sql = `    SELECT      COMMENT .id,      COMMENT .pid,      COMMENT .content,      USER .username,      COMMENT .createtime,      COMMENT.targetid,      COMMENT.type,      USER .headimg,      IF(userid=:userid,1,0) isyour,      static.replaycount,      (SELECT count(*) issupport FROM support WHERE COMMENT.id = support.targetid AND userid = :userid AND type = 101) issupport,      (SELECT count(*) isfavorite FROM favorite WHERE COMMENT.id = favorite.targetid AND userid = :userid AND type = 101) isfavorite,      (SELECT COUNT(*) FROM support WHERE type = 101 AND targetid = COMMENT.id) supportcount,      (SELECT COUNT(*) FROM favorite WHERE type = 101 AND targetid = COMMENT.id) favoritecount    FROM      COMMENT    LEFT JOIN USER ON COMMENT .userid = USER .id    LEFT JOIN     (SELECT      ancestor,      COUNT( * )-1 AS replaycount       FROM        (WITH RECURSIVE folder_recursion AS (          SELECT id, pid, targetid, content, type, createtime, userid, id AS ancestor           FROM COMMENT           WHERE id IN ( SELECT id FROM COMMENT WHERE pid = :pid )  AND type = :type          UNION ALL          SELECT c.id, c.pid, c.targetid, c.content, c.type, c.createtime, c.userid, folder_recursion.ancestor           FROM COMMENT c          INNER JOIN folder_recursion ON c.pid = folder_recursion.id       ) SELECT * FROM folder_recursion ) dd     GROUP BY      ancestor) static    on static.ancestor = COMMENT.id    WHERE COMMENT.targetid = :targetid    AND COMMENT.type = :type    AND COMMENT.pid = :pid    `;     // 先查询总量是否不为零    let countsql = `SELECT COUNT(id) as total FROM comment where type = :type and pid = :pid and targetid = :targetid`    let [count,meta] = await Model.sequelize.query(countsql,{      replacements:{        pid:0,        type:params.type,        targetid:params.targetid      }    })    let total = count[0]?.total    // 查询总数大于0方可查询列表    if(total>0){      // 继续分页      sql += ` ORDER BY createtime desc limit ${(Number(params.pagenum) - 1) * Number(params.pagesize)},${Number(params.pagesize)}`      let [results] = await Model.sequelize.query(sql,{        replacements:{          pid:0,          userid:params.userid??null,          type:params.type,          targetid:params.targetid        }      })      // 之后再递归获取子数据,最多只获取三条      let ids = []      if(results&&results.length){        results.forEach(item=>ids.push(item.id))      }      // 第二次查询,最多只显示3条数据      if(ids.length){        let sql = `        WITH RECURSIVE folder_recursion AS (          SELECT id, pid, content, createtime,userid,          IF(COMMENT.userid=:userid,1,0) isyour,          (SELECT count(*) issupport FROM support WHERE COMMENT.id = support.targetid AND userid = :userid AND type = 101) issupport,          (SELECT count(*) isfavorite FROM favorite WHERE COMMENT.id = favorite.targetid AND userid = :userid AND type = 101) isfavorite,          (SELECT COUNT(*) FROM support WHERE type = 101 AND targetid = COMMENT.id) supportcount,          (SELECT COUNT(*) FROM favorite WHERE type = 101 AND targetid = COMMENT.id) favoritecount,          (SELECT username from user WHERE COMMENT.userid = user.id) username,          (SELECT headimg from user WHERE COMMENT.userid = user.id) headimg          FROM COMMENT           WHERE pid in (${ids.join(',')}) AND type = :type          UNION ALL          SELECT c.id, c.pid, c.content,c.createtime, c.userid,          IF(c.userid=:userid,1,0) isyour,          (SELECT count(*) issupport FROM support WHERE c.id = support.targetid AND userid = :userid AND type = 101) issupport,          (SELECT count(*) isfavorite FROM favorite WHERE c.id = favorite.targetid AND userid = :userid AND type = 101) isfavorite,          (SELECT COUNT(*) FROM support WHERE type = 101 AND targetid = c.id) supportcount,          (SELECT COUNT(*) FROM favorite WHERE type = 101 AND targetid = c.id) favoritecount,          (SELECT username from user WHERE c.userid = user.id) username,          (SELECT headimg from user WHERE c.userid = user.id) headimg          FROM COMMENT c          INNER JOIN folder_recursion fr ON c.pid = fr.id           ) SELECT *  FROM folder_recursion ;        `        let [results_son] = await Model.sequelize.query(sql,{          replacements:{            type:params.type,            userid:params.userid??null          }        })        let result = [...results,...results_son]          console.log(result)          return {            commentList:result,            page:{              pagenum:params.pagenum,              pagesize:params.pagesize,              total:total            }          }      }else {        return {          commentList:results,          page:{            pagenum:params.pagenum,            pagesize:params.pagesize,            total:total          }        }      }    }else{      // tatol为0,没有评论      return {        commentList:[],        page:{          pagenum:params.pagenum,          pagesize:params.pagesize,          total:0        }      }    }

然后在controller层的中间件再重新组织下这个数据,前端获取到的数据结构为:

这里的分页,只是对pid为0的数据进行分页,pid不为0都是评论下的回复数据,效果如下:

之后就是第二个功能,就需要请求接口获取剩余的所有子数据: 这个就很简单了,拿到评论的id,去请求下面的所有子数据,返回给前端。

这里有个问题,就是我如果评论了,那么会重新请求接口,导致数据被初始化,就像这样,我明明已经回复到第五条回复了,结果提交后数据就初始化了:

前端解决这个问题,一个方法是在原有的数据结构上直接增加一个静态数据,当这个评论正在提交时,那么新增的这条数据就有个转圈状态,当接口请求成功后,这个状态就正式更新,并再次请求一次(快手应该就是这样做的)

所以解决这个问题,首先区分用户提交的是评论还是回复,如果是评论,则初始化评论列表,如果是回复,则重新获取当前的评论列表。

kotlin 复制代码
      addComment({        pid: 0,        targetid: this.targetid,        content: value,        type: this.type      }).then(res => {        this.$emit('callback', ++this.commentCount)        this.getComment()      })

稍稍修改下样式,效果如下:

完成这个功能后,评论的基本功能算是完成了,最后一点就是通过消息去查找评论。

最开始想的是,给每个消息设置一个唯一code,即pid_id的格式,携带这个id时就去找对应的节点,给该评论添加一个颜色变化的效果,表示是当前评论触发的消息。

但实际上这个实现起来有难度,因为评论是分页的,我不能确定评论在当前是否已经渲染了,如果没有渲染,那么目标评论又在第几页,这都是我无法知道的,(当然,强行要去获取目标的分页也不是不可以,但这会导致前端代码变成狗屎,因为业务逻辑这块儿已经产生问题了。)

所以需要换个思路,可以参考网易云音乐的评论消息,网易云音乐的评论实际上是做了分页的,在消息通知中过去时,不会在主体下找到这个具体消息,而是相当于在当前评论页面新开一个弹框页面,而这个页面则是那个评论详情。

想法有了,具体的编码业务应该就是这样。

首先,消息通知触发的是哪个消息,也就是这个评论针对的主体id是什么,这个主体id就是评论本身的id,那么如果有新的回复触发了,就可以根据这个最新的id查询出下面的所有子回复。

因此后台的逻辑就是根据评论id递归查找到所有的子回复:

async getReplay(params){

let sql = `

WITH RECURSIVE folder_recursion AS (

SELECT id, pid, content, createtime,userid,

IF(COMMENT.userid=:userid,1,0) isyour,

(SELECT count(*) issupport FROM support WHERE COMMENT.id = support.targetid AND userid = :userid AND type = 101) issupport,

(SELECT count(*) isfavorite FROM favorite WHERE COMMENT.id = favorite.targetid AND userid = :userid AND type = 101) isfavorite,

(SELECT COUNT(*) FROM support WHERE type = 101 AND targetid = COMMENT.id) supportcount,

(SELECT COUNT(*) FROM favorite WHERE type = 101 AND targetid = COMMENT.id) favoritecount,

(SELECT username from user WHERE COMMENT.userid = user.id) username,

(SELECT headimg from user WHERE COMMENT.userid = user.id) headimg

FROM COMMENT

WHERE pid = :id

UNION ALL

SELECT c.id, c.pid, c.content,c.createtime, c.userid,

IF(c.userid=:userid,1,0) isyour,

(SELECT count(*) issupport FROM support WHERE c.id = support.targetid AND userid = :userid AND type = 101) issupport,

(SELECT count(*) isfavorite FROM favorite WHERE c.id = favorite.targetid AND userid = :userid AND type = 101) isfavorite,

(SELECT COUNT(*) FROM support WHERE type = 101 AND targetid = c.id) supportcount,

(SELECT COUNT(*) FROM favorite WHERE type = 101 AND targetid = c.id) favoritecount,

(SELECT username from user WHERE c.userid = user.id) username,

(SELECT headimg from user WHERE c.userid = user.id) headimg

FROM COMMENT c

INNER JOIN folder_recursion fr ON c.pid = fr.id

) SELECT * FROM folder_recursion ORDER BY createtime desc;

`

let [results_son] = await Model.sequelize.query(sql,{

replacements:{

userid:params.userid??null,

id:params.id

}

})

return results_son

}

这里通过sql递归查询的方式,查询到当前id的所有子数据。

前端的实现方式,通过在路由上增加一个commentid和page=message参数,表示当前页面是从消息中心跳转过来的,并拿到当前的commentid,然后获取回复数据,就直接在消息页面做弹框展示。

大概这样:

但还是有问题,这个消息详情放置在文章详情不太合理,应该在消息页面触发,并且给出实体内容。

那么评论就暂时到此为止,下节做消息通知,顺便把评论详情这块儿内容完成。

相关推荐
qiyi.sky7 分钟前
JavaWeb——Vue组件库Element(3/6):常见组件:Dialog对话框、Form表单(介绍、使用、实际效果)
前端·javascript·vue.js
煸橙干儿~~11 分钟前
分析JS Crash(进程崩溃)
java·前端·javascript
2401_8543910812 分钟前
Spring Boot大学生就业招聘系统的开发与部署
java·spring boot·后端
安冬的码畜日常20 分钟前
【D3.js in Action 3 精译_027】3.4 让 D3 数据适应屏幕(下)—— D3 分段比例尺的用法
前端·javascript·信息可视化·数据可视化·d3.js·d3比例尺·分段比例尺
虽千万人 吾往矣32 分钟前
golang gorm
开发语言·数据库·后端·tcp/ip·golang
l1x1n01 小时前
No.3 笔记 | Web安全基础:Web1.0 - 3.0 发展史
前端·http·html
昨天;明天。今天。1 小时前
案例-任务清单
前端·javascript·css
这孩子叫逆1 小时前
Spring Boot项目的创建与使用
java·spring boot·后端
zqx_72 小时前
随记 前端框架React的初步认识
前端·react.js·前端框架
coderWangbuer2 小时前
基于springboot的高校招生系统(含源码+sql+视频导入教程+文档+PPT)
spring boot·后端·sql