现在大部分评论实际上都不再是多级嵌套结构的了,基本都是二级结构,即评论和回复,回复是多级的,但实际上以二级结构展示的,包括掘金,网易云等等,从样式上来说都是这样的。
因而最主要的问题是,对这类数据该如何分页?
我最开始是没有分页的,也就是后台把数据一次性返给前端,只是让前端在视觉上呈现分页的感觉,但这样的分页没有任何意义,效果如下:
我觉得对评论分页,其实只需要筛选出评论数据,即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,然后获取回复数据,就直接在消息页面做弹框展示。
大概这样:
但还是有问题,这个消息详情放置在文章详情不太合理,应该在消息页面触发,并且给出实体内容。
那么评论就暂时到此为止,下节做消息通知,顺便把评论详情这块儿内容完成。