如何利用Vue+SpringBoot實(shí)現(xiàn)評(píng)論功能
前言
評(píng)論系統(tǒng)相信大家并不陌生,在社交網(wǎng)絡(luò)相關(guān)的軟件中是一種常見的功能。然而對于初學(xué)者來說,實(shí)現(xiàn)一個(gè)完整的評(píng)論系統(tǒng)并不容易。本文筆者以 Vue+SpringBoot 前后端分離的架構(gòu)細(xì)說博客評(píng)論功能的實(shí)現(xiàn)思路。
難點(diǎn)
對于一個(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è)多叉樹的關(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)系。
- 很直觀能夠想到對于每一條評(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ù)母袷?,以方便前后端對于層?jí)關(guān)系的解析。
- 很自然地想到將評(píng)論的基本信息封裝為 bean,并將其子評(píng)論對象封裝為其一個(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)論 }
那么接下來的問題是如何將數(shù)據(jù)表中的層級(jí)關(guān)系轉(zhuǎn)化為 Comment 類中的 father-child 的關(guān)系
我這里寫了一個(gè) util 的方法完成這個(gè)轉(zhuǎn)化過程
/** * 構(gòu)建評(píng)論樹 * @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)系就表示清楚了,前端通過接口請求到的數(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ù)測試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ù)測試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ù)測試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ù)測試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 } }
對于處于葉子節(jié)點(diǎn)的評(píng)論,其 child 就為 null
前端遞歸顯示
接下來的一個(gè)難題是從后端獲取到的這個(gè)多叉樹結(jié)構(gòu)的數(shù)據(jù)如何顯示出來。
- 我們首先能想到的是 Vue 里的 v-for 來循環(huán)輸出所有 comment,再取其 child 進(jìn)行嵌套 v-for 輸出
- 但是這樣就會(huì)產(chǎn)生一個(gè)問題,v-for 的嵌套次數(shù)這么寫就是固定的,然而對于這棵多叉樹我們并不知道其深度為多少。舉個(gè)例子,例如我的前端結(jié)構(gòu)是外層一個(gè) v-for 輸出所有的 comment,內(nèi)層一個(gè) v-for 輸出這些 comment 的 child。但是這樣的結(jié)構(gòu)無法輸出 child 的 child,如果再加一層 v-for,又無法輸出 child 的 child 的 child。因?yàn)槲覀儫o法知道這棵樹的深度為多少,所以并不能確定 v-for 的嵌套層樹。而且這樣的一種寫法也實(shí)在是冗余,缺乏優(yōu)雅。
- 因此,我們很自然地想到算法中的遞歸。
- Vue 中的遞歸可以利用其獨(dú)特的父子組件機(jī)制實(shí)現(xiàn)。簡單來說,Vue 允許父組件調(diào)用子組件,并可進(jìn)行數(shù)據(jù)的傳遞,那么只要我們讓組件自己調(diào)用自己并調(diào)整傳遞的數(shù)據(jù),那么這不就形成了一個(gè)遞歸結(jié)構(gòu)了嗎?
我們接下來來看我的具體實(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 來自于 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ù)加入下一輪遞歸,由此完成該遞歸過程。
刪除評(píng)論
關(guān)于評(píng)論的操作無非是添加評(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)的子樹。
為了能完整地刪除這棵子樹,我們需要遍歷這棵子樹的每一個(gè)結(jié)點(diǎ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)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
JAVA中String類與StringBuffer類的區(qū)別
這篇文章主要為大家詳細(xì)介紹了JAVA中String類與StringBuffer類的區(qū)別,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2016-12-12java實(shí)現(xiàn)excel和txt文件互轉(zhuǎn)
本篇文章主要介紹了java實(shí)現(xiàn)excel和txt文件互轉(zhuǎn)的相關(guān)知識(shí)。具有很好的參考價(jià)值。下面跟著小編一起來看下吧2017-04-04Java數(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ī)訪問元素,可用于實(shí)現(xiàn)順序表等數(shù)據(jù)結(jié)構(gòu)。ArrayList在內(nèi)存中連續(xù)存儲(chǔ)元素,支持快速的隨機(jī)訪問和遍歷。通過學(xué)習(xí)ArrayList的實(shí)現(xiàn)原理和使用方法,可以更好地掌握J(rèn)ava中的數(shù)據(jù)結(jié)構(gòu)和算法2023-04-04