如何利用Vue+SpringBoot实现评论功能

 更新时间:2023年06月05日 08:37:34   作者:瑶琴遇知音  
简单的评论功能是指能够在文章底下进行评论,而且能够对评论进行回复,下面这篇文章主要给大家介绍了关于如何利用Vue+SpringBoot实现评论功能的相关资料,需要的朋友可以参考下

前言

评论系统相信大家并不陌生,在社交网络相关的软件中是一种常见的功能。然而对于初学者来说,实现一个完整的评论系统并不容易。本文笔者以 Vue+SpringBoot 前后端分离的架构细说博客评论功能的实现思路。

难点

对于一个评论系统主要包含评论人,评论时间,评论内容,评论回复等内容。此外可能还存在回复的回复以及回复的回复的回复,每条评论可能存在多条回复,每条回复又可能存在多条回复,即是一个多叉树的关系。因此,难点如下:

  • 确定并存储评论与回复的层级关系以及与博客本章的从属关系
  • 多层级评论与回复的前端递归显示
  • 多层级评论与回复的递归删除

实现思路

数据表设计

首先我们需要考虑的是数据表中如何存储评论与回复的层级关系以及与博客文章的从属关系。

  • 很直观能够想到对于每一条评论,拥有一个表示所属博客文章ID的字段blogId
  • 每一条评论维护一个parentId字段,表示父评论的id,由此确定评论之间的层级关系
  • 此外我们还会维护一个rootParentId字段,表示当前评论所属根评论的id,该字段将在前端递归显示时有大用

于是,添加上其他相关信息后最终的数据表schema如下:

字段名称中文注释数据类型是否为null备注
id评论idbigintnot nullprimary key,auto increment
content评论内容textnot null
user_id评论人idbigintnot null
user_name评论人姓名varchar(80)
create_time创建时间datetime
is_delete是否已删除tinyintdefault 00:未删除;1:已删除
blog_id所属博客idbigint
parent_id父评论idbigint
root_parent_id根评论idbigint

数据传输格式设计

基于数据表schema,我们需要设计前后端数据传输的格式,以方便前后端对于层级关系的解析。

  • 很自然地想到将评论的基本信息封装为 bean,并将其子评论对象封装为其一个属性。
  • 由于每条评论可能存在多条回复,因此属性的数据类型应当为 List

于是得到的评论 bean 为:

/**
 * 评论信息
 */
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Comment implements Serializable {

    private Long id;       // 评论ID
    private String content;       // 评论内容
    private Long userId;          // 评论作者ID
    private String userName;      // 评论作者姓名
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private Date createTime;      // 创建时间
    private Integer isDelete;     // 是否删除(0:未删除;1:已删除)

    private Long blogId;      // 博客ID
    private Long parentId;    // 父评论ID(被回复的评论)
    private Long rootParentId;      // 根评论ID(最顶级的评论)

    private List<Comment> child;    // 本评论下的子评论
}

那么接下来的问题是如何将数据表中的层级关系转化为 Comment 类中的 father-child 的关系

我这里写了一个 util 的方法完成这个转化过程

/**
* 构建评论树
* @param list
* @return
*/
public static List<Comment> processComments(List<Comment> list) {
    Map<Long, Comment> map = new HashMap<>();   // (id, Comment)
    List<Comment> result = new ArrayList<>();
    // 将所有根评论加入 map
    for(Comment comment : list) {
        if(comment.getParentId() == null)
            result.add(comment);
        map.put(comment.getId(), comment);
    }
    // 子评论加入到父评论的 child 中
    for(Comment comment : list) {
        Long id = comment.getParentId();
        if(id != null) {   // 当前评论为子评论
            Comment p = map.get(id);
            if(p.getChild() == null)    // child 为空,则创建
                p.setChild(new ArrayList<>());
            p.getChild().add(comment);
        }
    }
    return result;
}

这样父子关系就表示清楚了,前端通过接口请求到的数据就会是如下的样子

{
    "success": true,
    "code": 200,
    "message": "执行成功",
    "data": {
        "commentList": [
            {
                "id": 13,
                "content": "r34r43r4r54t54t54",
                "userId": 1,
                "userName": "admin",
                "createTime": "2022-10-26 04:53:21",
                "isDelete": null,
                "blogId": 1,
                "parentId": null,
                "rootParentId": null,
                "child": [
                    {
                        "id": 19,
                        "content": "评论回复测试2",
                        "userId": 1,
                        "userName": "admin",
                        "createTime": "2022-10-27 03:10:41",
                        "isDelete": null,
                        "blogId": 1,
                        "parentId": 13,
                        "rootParentId": 13,
                        "child": null
                    }
                ]
            },
            {
                "id": 12,
                "content": "fdfgdfgfg",
                "userId": 1,
                "userName": "admin",
                "createTime": "2022-10-26 04:51:46",
                "isDelete": null,
                "blogId": 1,
                "parentId": null,
                "rootParentId": null,
                "child": [
                    {
                        "id": 20,
                        "content": "评论回复测试3",
                        "userId": 1,
                        "userName": "admin",
                        "createTime": "2022-10-27 03:16:09",
                        "isDelete": null,
                        "blogId": 1,
                        "parentId": 12,
                        "rootParentId": 12,
                        "child": null
                    }
                ]
            },
            {
                "id": 11,
                "content": "demo",
                "userId": 1,
                "userName": "admin",
                "createTime": "2022-10-26 04:12:43",
                "isDelete": null,
                "blogId": 1,
                "parentId": null,
                "rootParentId": null,
                "child": [
                    {
                        "id": 21,
                        "content": "评论回复测试4",
                        "userId": 1,
                        "userName": "admin",
                        "createTime": "2022-10-27 03:19:42",
                        "isDelete": null,
                        "blogId": 1,
                        "parentId": 11,
                        "rootParentId": 11,
                        "child": null
                    }
                ]
            },
            {
                "id": 9,
                "content": "评论3",
                "userId": 3,
                "userName": "zhangsan",
                "createTime": "2022-10-05 06:20:54",
                "isDelete": null,
                "blogId": 1,
                "parentId": null,
                "rootParentId": null,
                "child": [
                    {
                        "id": 24,
                        "content": "评论回复测试n3",
                        "userId": 1,
                        "userName": "admin",
                        "createTime": "2022-10-27 03:23:54",
                        "isDelete": null,
                        "blogId": 1,
                        "parentId": 9,
                        "rootParentId": 9,
                        "child": null
                    }
                ]
            },
            {
                "id": 7,
                "content": "评论2",
                "userId": 2,
                "userName": "liming",
                "createTime": "2022-10-05 06:19:40",
                "isDelete": null,
                "blogId": 1,
                "parentId": null,
                "rootParentId": null,
                "child": [
                    {
                        "id": 8,
                        "content": "回复2-1",
                        "userId": 1,
                        "userName": "admin",
                        "createTime": "2022-10-14 06:20:07",
                        "isDelete": null,
                        "blogId": 1,
                        "parentId": 7,
                        "rootParentId": 7,
                        "child": null
                    }
                ]
            },
            {
                "id": 1,
                "content": "评论1",
                "userId": 1,
                "userName": "admin",
                "createTime": "2022-10-05 06:14:32",
                "isDelete": null,
                "blogId": 1,
                "parentId": null,
                "rootParentId": null,
                "child": [
                    {
                        "id": 3,
                        "content": "回复1-2",
                        "userId": 2,
                        "userName": "liming",
                        "createTime": "2022-10-07 06:16:25",
                        "isDelete": null,
                        "blogId": 1,
                        "parentId": 1,
                        "rootParentId": 1,
                        "child": [
                            {
                                "id": 6,
                                "content": "回复1-2-1",
                                "userId": 3,
                                "userName": "zhangsan",
                                "createTime": "2022-10-13 06:18:51",
                                "isDelete": null,
                                "blogId": 1,
                                "parentId": 3,
                                "rootParentId": 1,
                                "child": null
                            }
                        ]
                    }
                ]
            }
        ],
        "total": 13
    }
}

对于处于叶子节点的评论,其 child 就为 null

前端递归显示

接下来的一个难题是从后端获取到的这个多叉树结构的数据如何显示出来。

  1. 我们首先能想到的是 Vue 里的 v-for 来循环输出所有 comment,再取其 child 进行嵌套 v-for 输出
  2. 但是这样就会产生一个问题,v-for 的嵌套次数这么写就是固定的,然而对于这棵多叉树我们并不知道其深度为多少。举个例子,例如我的前端结构是外层一个 v-for 输出所有的 comment,内层一个 v-for 输出这些 comment 的 child。但是这样的结构无法输出 child 的 child,如果再加一层 v-for,又无法输出 child 的 child 的 child。因为我们无法知道这棵树的深度为多少,所以并不能确定 v-for 的嵌套层树。而且这样的一种写法也实在是冗余,缺乏优雅。
  3. 因此,我们很自然地想到算法中的递归。
  4. Vue 中的递归可以利用其独特的父子组件机制实现。简单来说,Vue 允许父组件调用子组件,并可进行数据的传递,那么只要我们让组件自己调用自己并调整传递的数据,那么这不就形成了一个递归结构了吗?

我们接下来来看我的具体实现

blogDetails.vue(父组件)

<!-- 显示评论 -->
<div class="comment-list-container">
	<div class="comment-list-box comment-operate-item">
		<ul class="comment-list" v-for="comment in commentList">
			<!-- 评论根目录 -->
			<root :comment="comment" :blog="blog" :getCommentList="getCommentList"></root>
			<!-- 评论子目录 -->
			<li class="replay-box" style="display: block;">
				<ul class="comment-list">
					<!-- 子组件递归实现 -->
					<child :childComments="comment.child" :parentComment="comment" :blog="blog" :rootParentId="comment.id" :getCommentList="getCommentList" v-if="comment.child != null"></child>
				</ul>
			</li>
		</ul>
	</div>
</div>

在父组件中我们调用了子组件 child 去实现评论的输出,child 来自于 childComment.vue

childComment.vue

<div class="comment-line-box" v-for="childComment in childComments">
	<div class="comment-list-item">
		<el-avatar icon="el-icon-user-solid" :size="35" style="width: 38px;"></el-avatar>
		<div class="right-box">
			<div class="new-info-box clearfix">
				<div class="comment-top">
					<div class="user-box">
						<span class="comment-name">{{ childComment.userName }}</span>
						<el-tag size="mini" type="danger" v-show="childComment.userName === blog.authorName" style="margin-left: 5px;">作者</el-tag>
						<span class="text">回复</span>
						<span class="nick-name">{{ parentComment.userName }}</span>
						<span class="date">{{ childComment.createTime }}</span>
						<div class="opt-comment">
							<i class="el-icon-delete"></i>
							<span style="margin-left: 3px;" @click="deleteComment(childComment)">删除</span>
							<i class="el-icon-chat-round" style="margin-left: 10px;"></i>
							<span style="margin-left: 3px;" @click="showReplay = !showReplay">回复</span>
						</div>
					</div>
				</div>
				<div class="comment-center">
					<div class="new-comment">{{ childComment.content }}</div>
				</div>
			</div>
		</div>
	</div>
	<!-- 回复框 -->
	<replay :rootParentId="rootParentId" :comment="childComment" :showReplay="showReplay" :blogId="blogId" :getCommentList="getCommentList" style="margin-top: 5px;"></replay>
	<!-- 嵌套递归 -->
	<child :childComments="childComment.child" :parentComment="childComment" :blog="blog" :rootParentId="rootParentId" :getCommentList="getCommentList"></child>
</div>

在子组件中,我们递归调用了自身,并设置了子评论和父评论等数据加入下一轮递归,由此完成该递归过程。

删除评论

关于评论的操作无非是添加评论(回复)和删除评论。添加评论比较好理解,只要获取了相关的层级关系数据,如 parentId 等,往数据表里插入一条记录就可以了。然而删除评论则较为复杂,删除评论不仅要删除当前的这条评论(回复),也要删除其子评论(回复),即以该条评论为根结点的子树

为了能完整地删除这棵子树,我们需要遍历这棵子树的每一个结点,比较简单的方式就是层序遍历。这里我采用了非递归的方法,即借助队列实现。

/**
 * 删除评论
 * @param comment
 * @return
 */
@Override
public boolean removeComment(Comment comment) {
    Queue<Comment> queue = new LinkedList<>();
    queue.offer(comment);
    while(!queue.isEmpty()) {
        Comment cur = queue.poll();
        int resultNum = commentMapper.removeById(cur.getId());
        if(resultNum <= 0) return false;
        if(cur.getChild() != null) {
            List<Comment> child = cur.getChild();
            for(Comment tmp: child)
                queue.offer(tmp);
        }
    }
    return true;
}

讲到这里差不多就把评论系统的所有难点讲完了!

总结

到此这篇关于如何利用Vue+SpringBoot实现评论功能的文章就介绍到这了,更多相关Vue SpringBoot评论功能内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • JAVA中String类与StringBuffer类的区别

    JAVA中String类与StringBuffer类的区别

    这篇文章主要为大家详细介绍了JAVA中String类与StringBuffer类的区别,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2016-12-12
  • java中的transient关键字解读

    java中的transient关键字解读

    这篇文章主要介绍了java中的transient关键字解读,transient关键字的主要作用就是让某些被transient关键字修饰的成员属性变量不被序列化,实际上也正是因此,在学习过程中很少用得上序列化操作,一般都是在实际开发中,需要的朋友可以参考下
    2023-09-09
  • Java中四种遍历List的方法总结(推荐)

    Java中四种遍历List的方法总结(推荐)

    下面小编就为大家带来一篇Java中四种遍历List的方法总结(推荐)。小编觉得挺不错的,现在就分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2017-04-04
  • java实现excel和txt文件互转

    java实现excel和txt文件互转

    本篇文章主要介绍了java实现excel和txt文件互转的相关知识。具有很好的参考价值。下面跟着小编一起来看下吧
    2017-04-04
  • Java数据结构之ArrayList从顺序表到实现

    Java数据结构之ArrayList从顺序表到实现

    Java中的ArrayList是一种基于数组实现的数据结构,支持动态扩容和随机访问元素,可用于实现顺序表等数据结构。ArrayList在内存中连续存储元素,支持快速的随机访问和遍历。通过学习ArrayList的实现原理和使用方法,可以更好地掌握Java中的数据结构和算法
    2023-04-04
  • Java web xml文件读取解析方式

    Java web xml文件读取解析方式

    这篇文章主要介绍了Java web xml文件读取解析方式,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下
    2020-03-03
  • java 使用foreach遍历集合元素的实例

    java 使用foreach遍历集合元素的实例

    这篇文章主要介绍了java 使用foreach遍历集合元素的实例的相关资料,这里提供实例帮助大家理解如何使用foreach 进行遍历,希望能帮助到大家,
    2017-08-08
  • SpringBoot MongoDB详细使用教程

    SpringBoot MongoDB详细使用教程

    这篇文章主要介绍了SpringBoot整合Mongodb实现简单的增删查改,MongoDB是一个以分布式数据库为核心的数据库,因此高可用性、横向扩展和地理分布是内置的,并且易于使用。况且,MongoDB是免费的,开源的,感兴趣的朋友跟随小编一起看看吧
    2022-10-10
  • Eureka源码解析服务离线状态变更

    Eureka源码解析服务离线状态变更

    这篇文章主要为大家介绍了Eureka源码解析服务离线的状态变更示例,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2022-10-10
  • Spring Boot常见外部配置文件方式详析

    Spring Boot常见外部配置文件方式详析

    这篇文章主要给大家介绍了关于Spring Boot常见外部配置文件方式的相关资料,文中通过示例代码介绍的非常详细,对大家的学习或者使用Spring Boot具有一定的参考学习价值,需要的朋友们下面来一起学习学习吧
    2020-07-07

最新评论