Vue+SpringBoot实现评论功能

这篇具有很好参考价值的文章主要介绍了Vue+SpringBoot实现评论功能。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

前言

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

难点

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

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

实现思路

数据表设计

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

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

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

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

数据传输格式设计

基于数据表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

前端递归显示

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

  • 我们首先能想到的是 Vue 里的 v-for 来循环输出所有 comment,再取其 child 进行嵌套 v-for 输出
  • 但是这样就会产生一个问题,v-for 的嵌套次数这么写就是固定的,然而对于这棵多叉树我们并不知道其深度为多少。举个例子,例如我的前端结构是外层一个 v-for 输出所有的 comment,内层一个 v-for 输出这些 comment 的 child。但是这样的结构无法输出 child 的 child,如果再加一层 v-for,又无法输出 child 的 child 的 child。因为我们无法知道这棵树的深度为多少,所以并不能确定 v-for 的嵌套层树。而且这样的一种写法也实在是冗余,缺乏优雅。
  • 因此,我们很自然地想到算法中的递归
  • 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;
}

讲到这里差不多就把评论系统的所有难点讲完了,欢迎指正批评!文章来源地址https://www.toymoban.com/news/detail-401160.html

到了这里,关于Vue+SpringBoot实现评论功能的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • SpringBoot+Vue实现校园二手系统。前后端分离技术【完整功能介绍+实现详情+源码】

    全部源代码地址 :传送门 文章内容有点长,建议打开右侧目录导航栏查看 。         这个系统基本上可以改造为其它类似的系统。后台管理基本上一致。前台进行一些页面样式的改造就可以变成一个新的系统。有时间,做几个变体系统。        闲的无聊,把大学时候

    2024年02月07日
    浏览(47)
  • 通俗易懂实现功能强大的实战项目 springboot+java+vue+mysql 汽车租赁管理系统

    ✍✍计算机编程指导师 ⭐⭐个人介绍:自己非常喜欢研究技术问题!专业做Java、Python、微信小程序、安卓、大数据、爬虫、Golang、大屏等实战项目。 ⛽⛽实战项目:有源码或者技术上的问题欢迎在评论区一起讨论交流! ⚡⚡ Java实战 | SpringBoot/SSM Python实战项目 | Django 微信小

    2024年01月19日
    浏览(52)
  • 通俗易懂实现功能强大的实战项目 springboot+java+vue+mysql 汽车服务管理系统

    ✍✍计算机编程指导师 ⭐⭐个人介绍:自己非常喜欢研究技术问题!专业做Java、Python、微信小程序、安卓、大数据、爬虫、Golang、大屏等实战项目。 ⛽⛽实战项目:有源码或者技术上的问题欢迎在评论区一起讨论交流! ⚡⚡ Java实战 | SpringBoot/SSM Python实战项目 | Django 微信小

    2024年01月16日
    浏览(40)
  • 【前后端的那些事】评论功能实现

    前言 :最近写项目,发现了一些很有意思的功能,想写文章,录视频把这些内容记录下。但这些功能太零碎,如果为每个功能都单独搭建一个项目,这明显不合适。于是我想,就搭建一个项目,把那些我想将的小功能全部整合到一起。实现 搭一次环境 ,处处使用。 本文主要

    2024年01月18日
    浏览(45)
  • SpringBoot+Vue实现文件上传功能

    目录 1.后端代码部分: 2.前端代码部分 3.效果展示 1.后端代码部分: 2.前端代码部分 3.效果展示      

    2024年02月12日
    浏览(50)
  • 使用JS来实现回车评论功能

    个人名片:  😊 作者简介:一名大一在校生,web前端开发专业  🤡  个人主页:几何小超  🐼 座右铭:懒惰受到的惩罚不仅仅是自己的失败,还有别人的成功。  🎅**学习目标:  坚持每一次的学习打卡,掌握更多知识!虽然都说前端已死,但是就算不靠这个吃饭,学一点

    2024年02月06日
    浏览(82)
  • JavaWeb评论功能实现步骤及代码

    目录 文章目录 前言 一、分析功能 二、实现功能 1.建评论表 2.Dao层、service层核心代码实现  3.servlet层编写核心代码  4.jsp核心代码  三、展示效果图 总结 评论功能是后端要写常见的功能之一,一般性的网站也都会包含这一功能。像是购物网站、视频网站下方都会有用户评论

    2023年04月08日
    浏览(38)
  • Java单表实现评论回复功能

    最近在写毕业设计的时候发现需要实现一个评论功能,然后看了一下掘金和csdn的评论区,如何实现评论功能? 评论功能有多种实现方式: 单层型 套娃型(多层型) 两层型 单层型: 套娃型: 两层型: 这个地方有个answer_id 很容易让人迷糊:是回复哪个用户的id 因为有评论区

    2024年01月21日
    浏览(37)
  • SpringBoot+Vue 实现图片验证码功能需求

    文章底部有个人公众号: 热爱技术的小郑 。主要分享开发知识、有兴趣的可以关注一下。为何分享? 踩过的坑没必要让别人在再踩,自己复盘也能加深记忆。利己利人、所谓双赢。 写过验证码保存到Redis中的需求开发、也写过验证码调用第三方接口直接发送到手机的需求开

    2024年02月14日
    浏览(35)
  • vue+springboot登录与注册功能的实现

     大致效果: 在component目录下新建一个ValidCode.vue: 在登录页引入:  效果图(可以看到多了验证码): 首先准备数据库:  用IDEA创建springboot工程: 创建springboot教程 连接数据库: application.yml: 目录结构: 按照该目录创建文件夹  CorsConfig:解决跨域问题 Result:返回数据

    2024年02月19日
    浏览(39)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包