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

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

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

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

我觉得对评论分页,其实只需要筛选出评论数据,即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,然后获取回复数据,就直接在消息页面做弹框展示。

大概这样:

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

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

相关推荐
1024小神11 分钟前
uniapp+vue3+vite+ts+xr-frame实现ar+vr渲染踩坑记
前端
测试界清流14 分钟前
基于pytest的接口测试
前端·servlet
知识分享小能手37 分钟前
微信小程序入门学习教程,从入门到精通,自定义组件与第三方 UI 组件库(以 Vant Weapp 为例) (16)
前端·学习·ui·微信小程序·小程序·vue·编程
trsoliu1 小时前
多仓库 Workspace 协作机制完整方案
前端
啦工作呢1 小时前
数据可视化 ECharts
前端·信息可视化·echarts
NoneSL1 小时前
Uniapp UTS插件开发实战:引入第三方SDK
前端·uni-app
trsoliu1 小时前
Claude Code Templates
前端·人工智能
wangpq1 小时前
使用rerender-spa-plugin在构建时预渲染静态HTML文件优化SEO
前端·javascript·vue.js
KongHen1 小时前
完美解决请求跨域问题
前端
前端开发爱好者1 小时前
弃用 uni-app!Vue3 的原生 App 开发框架来了!
前端·javascript·vue.js