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

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

