如何利用Vue+SpringBoot實(shí)現(xiàn)評(píng)論功能
前言
評(píng)論系統(tǒng)相信大家并不陌生,在社交網(wǎng)絡(luò)相關(guān)的軟件中是一種常見(jiàn)的功能。然而對(duì)于初學(xué)者來(lái)說(shuō),實(shí)現(xiàn)一個(gè)完整的評(píng)論系統(tǒng)并不容易。本文筆者以 Vue+SpringBoot 前后端分離的架構(gòu)細(xì)說(shuō)博客評(píng)論功能的實(shí)現(xiàn)思路。
難點(diǎn)

對(duì)于一個(gè)評(píng)論系統(tǒng)主要包含評(píng)論人,評(píng)論時(shí)間,評(píng)論內(nèi)容,評(píng)論回復(fù)等內(nèi)容。此外可能還存在回復(fù)的回復(fù)以及回復(fù)的回復(fù)的回復(fù),每條評(píng)論可能存在多條回復(fù),每條回復(fù)又可能存在多條回復(fù),即是一個(gè)多叉樹(shù)的關(guān)系。因此,難點(diǎn)如下:
- 確定并存儲(chǔ)評(píng)論與回復(fù)的層級(jí)關(guān)系以及與博客本章的從屬關(guān)系
- 多層級(jí)評(píng)論與回復(fù)的前端遞歸顯示
- 多層級(jí)評(píng)論與回復(fù)的遞歸刪除
實(shí)現(xiàn)思路
數(shù)據(jù)表設(shè)計(jì)
首先我們需要考慮的是數(shù)據(jù)表中如何存儲(chǔ)評(píng)論與回復(fù)的層級(jí)關(guān)系以及與博客文章的從屬關(guān)系。
- 很直觀能夠想到對(duì)于每一條評(píng)論,擁有一個(gè)表示所屬博客文章ID的字段blogId
- 每一條評(píng)論維護(hù)一個(gè)parentId字段,表示父評(píng)論的id,由此確定評(píng)論之間的層級(jí)關(guān)系
- 此外我們還會(huì)維護(hù)一個(gè)rootParentId字段,表示當(dāng)前評(píng)論所屬根評(píng)論的id,該字段將在前端遞歸顯示時(shí)有大用
于是,添加上其他相關(guān)信息后最終的數(shù)據(jù)表schema如下:
| 字段名稱 | 中文注釋 | 數(shù)據(jù)類型 | 是否為null | 備注 |
|---|---|---|---|---|
| id | 評(píng)論id | bigint | not null | primary key,auto increment |
| content | 評(píng)論內(nèi)容 | text | not null | |
| user_id | 評(píng)論人id | bigint | not null | |
| user_name | 評(píng)論人姓名 | varchar(80) | ||
| create_time | 創(chuàng)建時(shí)間 | datetime | ||
| is_delete | 是否已刪除 | tinyint | default 0 | 0:未刪除;1:已刪除 |
| blog_id | 所屬博客id | bigint | ||
| parent_id | 父評(píng)論id | bigint | ||
| root_parent_id | 根評(píng)論id | bigint |
數(shù)據(jù)傳輸格式設(shè)計(jì)
基于數(shù)據(jù)表schema,我們需要設(shè)計(jì)前后端數(shù)據(jù)傳輸?shù)母袷?,以方便前后端?duì)于層級(jí)關(guān)系的解析。
- 很自然地想到將評(píng)論的基本信息封裝為 bean,并將其子評(píng)論對(duì)象封裝為其一個(gè)屬性。
- 由于每條評(píng)論可能存在多條回復(fù),因此屬性的數(shù)據(jù)類型應(yīng)當(dāng)為 List
于是得到的評(píng)論 bean 為:
/**
* 評(píng)論信息
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Comment implements Serializable {
private Long id; // 評(píng)論ID
private String content; // 評(píng)論內(nèi)容
private Long userId; // 評(píng)論作者ID
private String userName; // 評(píng)論作者姓名
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date createTime; // 創(chuàng)建時(shí)間
private Integer isDelete; // 是否刪除(0:未刪除;1:已刪除)
private Long blogId; // 博客ID
private Long parentId; // 父評(píng)論ID(被回復(fù)的評(píng)論)
private Long rootParentId; // 根評(píng)論ID(最頂級(jí)的評(píng)論)
private List<Comment> child; // 本評(píng)論下的子評(píng)論
}
那么接下來(lái)的問(wèn)題是如何將數(shù)據(jù)表中的層級(jí)關(guān)系轉(zhuǎn)化為 Comment 類中的 father-child 的關(guān)系
我這里寫(xiě)了一個(gè) util 的方法完成這個(gè)轉(zhuǎn)化過(guò)程
/**
* 構(gòu)建評(píng)論樹(shù)
* @param list
* @return
*/
public static List<Comment> processComments(List<Comment> list) {
Map<Long, Comment> map = new HashMap<>(); // (id, Comment)
List<Comment> result = new ArrayList<>();
// 將所有根評(píng)論加入 map
for(Comment comment : list) {
if(comment.getParentId() == null)
result.add(comment);
map.put(comment.getId(), comment);
}
// 子評(píng)論加入到父評(píng)論的 child 中
for(Comment comment : list) {
Long id = comment.getParentId();
if(id != null) { // 當(dāng)前評(píng)論為子評(píng)論
Comment p = map.get(id);
if(p.getChild() == null) // child 為空,則創(chuàng)建
p.setChild(new ArrayList<>());
p.getChild().add(comment);
}
}
return result;
}這樣父子關(guān)系就表示清楚了,前端通過(guò)接口請(qǐng)求到的數(shù)據(jù)就會(huì)是如下的樣子
{
"success": true,
"code": 200,
"message": "執(zhí)行成功",
"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": "評(píng)論回復(fù)測(cè)試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": "評(píng)論回復(fù)測(cè)試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": "評(píng)論回復(fù)測(cè)試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": "評(píng)論3",
"userId": 3,
"userName": "zhangsan",
"createTime": "2022-10-05 06:20:54",
"isDelete": null,
"blogId": 1,
"parentId": null,
"rootParentId": null,
"child": [
{
"id": 24,
"content": "評(píng)論回復(fù)測(cè)試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": "評(píng)論2",
"userId": 2,
"userName": "liming",
"createTime": "2022-10-05 06:19:40",
"isDelete": null,
"blogId": 1,
"parentId": null,
"rootParentId": null,
"child": [
{
"id": 8,
"content": "回復(fù)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": "評(píng)論1",
"userId": 1,
"userName": "admin",
"createTime": "2022-10-05 06:14:32",
"isDelete": null,
"blogId": 1,
"parentId": null,
"rootParentId": null,
"child": [
{
"id": 3,
"content": "回復(fù)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": "回復(fù)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
}
}對(duì)于處于葉子節(jié)點(diǎn)的評(píng)論,其 child 就為 null
前端遞歸顯示
接下來(lái)的一個(gè)難題是從后端獲取到的這個(gè)多叉樹(shù)結(jié)構(gòu)的數(shù)據(jù)如何顯示出來(lái)。
- 我們首先能想到的是 Vue 里的 v-for 來(lái)循環(huán)輸出所有 comment,再取其 child 進(jìn)行嵌套 v-for 輸出
- 但是這樣就會(huì)產(chǎn)生一個(gè)問(wèn)題,v-for 的嵌套次數(shù)這么寫(xiě)就是固定的,然而對(duì)于這棵多叉樹(shù)我們并不知道其深度為多少。舉個(gè)例子,例如我的前端結(jié)構(gòu)是外層一個(gè) v-for 輸出所有的 comment,內(nèi)層一個(gè) v-for 輸出這些 comment 的 child。但是這樣的結(jié)構(gòu)無(wú)法輸出 child 的 child,如果再加一層 v-for,又無(wú)法輸出 child 的 child 的 child。因?yàn)槲覀儫o(wú)法知道這棵樹(shù)的深度為多少,所以并不能確定 v-for 的嵌套層樹(shù)。而且這樣的一種寫(xiě)法也實(shí)在是冗余,缺乏優(yōu)雅。
- 因此,我們很自然地想到算法中的遞歸。
- Vue 中的遞歸可以利用其獨(dú)特的父子組件機(jī)制實(shí)現(xiàn)。簡(jiǎn)單來(lái)說(shuō),Vue 允許父組件調(diào)用子組件,并可進(jìn)行數(shù)據(jù)的傳遞,那么只要我們讓組件自己調(diào)用自己并調(diào)整傳遞的數(shù)據(jù),那么這不就形成了一個(gè)遞歸結(jié)構(gòu)了嗎?
我們接下來(lái)來(lái)看我的具體實(shí)現(xiàn)
blogDetails.vue(父組件)
<!-- 顯示評(píng)論 --> <div class="comment-list-container"> <div class="comment-list-box comment-operate-item"> <ul class="comment-list" v-for="comment in commentList"> <!-- 評(píng)論根目錄 --> <root :comment="comment" :blog="blog" :getCommentList="getCommentList"></root> <!-- 評(píng)論子目錄 --> <li class="replay-box" style="display: block;"> <ul class="comment-list"> <!-- 子組件遞歸實(shí)現(xiàn) --> <child :childComments="comment.child" :parentComment="comment" :blog="blog" :rootParentId="comment.id" :getCommentList="getCommentList" v-if="comment.child != null"></child> </ul> </li> </ul> </div> </div>
在父組件中我們調(diào)用了子組件 child 去實(shí)現(xiàn)評(píng)論的輸出,child 來(lái)自于 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">回復(fù)</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">回復(fù)</span>
</div>
</div>
</div>
<div class="comment-center">
<div class="new-comment">{{ childComment.content }}</div>
</div>
</div>
</div>
</div>
<!-- 回復(fù)框 -->
<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>在子組件中,我們遞歸調(diào)用了自身,并設(shè)置了子評(píng)論和父評(píng)論等數(shù)據(jù)加入下一輪遞歸,由此完成該遞歸過(guò)程。
刪除評(píng)論
關(guān)于評(píng)論的操作無(wú)非是添加評(píng)論(回復(fù))和刪除評(píng)論。添加評(píng)論比較好理解,只要獲取了相關(guān)的層級(jí)關(guān)系數(shù)據(jù),如 parentId 等,往數(shù)據(jù)表里插入一條記錄就可以了。然而刪除評(píng)論則較為復(fù)雜,刪除評(píng)論不僅要?jiǎng)h除當(dāng)前的這條評(píng)論(回復(fù)),也要?jiǎng)h除其子評(píng)論(回復(fù)),即以該條評(píng)論為根結(jié)點(diǎn)的子樹(shù)。
為了能完整地刪除這棵子樹(shù),我們需要遍歷這棵子樹(shù)的每一個(gè)結(jié)點(diǎn),比較簡(jiǎn)單的方式就是層序遍歷。這里我采用了非遞歸的方法,即借助隊(duì)列實(shí)現(xiàn)。
/**
* 刪除評(píng)論
* @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;
}講到這里差不多就把評(píng)論系統(tǒng)的所有難點(diǎn)講完了!
總結(jié)
到此這篇關(guān)于如何利用Vue+SpringBoot實(shí)現(xiàn)評(píng)論功能的文章就介紹到這了,更多相關(guān)Vue SpringBoot評(píng)論功能內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
JAVA中String類與StringBuffer類的區(qū)別
這篇文章主要為大家詳細(xì)介紹了JAVA中String類與StringBuffer類的區(qū)別,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2016-12-12
java實(shí)現(xiàn)excel和txt文件互轉(zhuǎn)
本篇文章主要介紹了java實(shí)現(xiàn)excel和txt文件互轉(zhuǎn)的相關(guān)知識(shí)。具有很好的參考價(jià)值。下面跟著小編一起來(lái)看下吧2017-04-04
Java數(shù)據(jù)結(jié)構(gòu)之ArrayList從順序表到實(shí)現(xiàn)
Java中的ArrayList是一種基于數(shù)組實(shí)現(xiàn)的數(shù)據(jù)結(jié)構(gòu),支持動(dòng)態(tài)擴(kuò)容和隨機(jī)訪問(wèn)元素,可用于實(shí)現(xiàn)順序表等數(shù)據(jù)結(jié)構(gòu)。ArrayList在內(nèi)存中連續(xù)存儲(chǔ)元素,支持快速的隨機(jī)訪問(wèn)和遍歷。通過(guò)學(xué)習(xí)ArrayList的實(shí)現(xiàn)原理和使用方法,可以更好地掌握J(rèn)ava中的數(shù)據(jù)結(jié)構(gòu)和算法2023-04-04

