需求说明
实习期间需要完成一个给评论无限盖楼的功能,模拟b站的评论效果,如图:
- 评论跟回复的样式一样,包括用户名和评论内容
- 一级回复,即评论的回复,直接显示评论内容
- 二级及以上回复,即回复的回复,需要在评论内容前,加上“回复@某某某”的说明
数据结构
对于这种嵌套层级的,如多级菜单,一级二级城市,都是树形结构,对于评论而言,模拟数据如下:
commentsList: [
{
cuid: '小瓦',
info: '这是一条评论',
reply:[
{
cuid: '小计',
info: '这是一级回复',
reviewer: '',
reply: [
{
cuid: '小红',
info: '这是二级回复',
reviewer: '小计',
reply: []
}
]
}
]
},
{
cuid: '小当',
info: '这是另一个评论',
reply: [
{
cuid: '小东',
info: '这是评论的回复',
reviewer: '',
reply: [
{
cuid: '小南',
info: '这是回复的回复',
reviewer: '小东',
reply: []
}
]
}
]
}
]
为了使得二级回复可以增加@某某某的文字,二级以上回复增加字段reviewer以记录回复者的名字。
普通渲染
最开始分析这个问题时,无论是评论还是回复样式都一样,如果是一个数组v-for循环遍历渲染就可以了(二级回复只是多个字段,用v-if判断一下即可),所以最开始的想法,是把这个树形结构,打平成一个数组,即上述数据结构变为:
commentsList: [
{
cuid: '小瓦',
info: '这是一条评论',
reply: [
{cuid: '小计', info: '这是一级回复', reviewer: ''},
{cuid: '小红', info: '这是二级回复', reviewer: '小计'}
]
},
{
cuid: '小当',
info: '这是另一个评论',
reply: [
{cuid: '小东', info: '这是评论的回复', reviewer: ''},
{cuid: '小南', info: '这是回复的回复', reviewer: '小东'}
]
}
]
这样两层v-for循环,外层渲染评论,内层渲染回复。
树形结构打平为数组
脱离这个需求的话,这本身是个很常见的小算法题,即树的遍历。
// 递归回复 node是个数组
// res就是打平后的reply
function recursiveTraverse(node, res=[]) {
if(!node || !node.length) {
return; // 递归结束条件
}
node.forEach(item => {
let reply = {};
reply.cuid = item.cuid;
reply.info = item.info;
reply.reviewer = item.reviewer;
res.push(reply)
})
return res;
}
// 打平数组
function getComments(list) {
list.forEach(item => {
if(item.reply) {
item.reply = recursiveTraverse(item.reply)
}
})
}
打平数组后v-for渲染即可,评论的回复按钮需要记录评论的cuid,根据cuid查找到该条评论,往reply中push一条数据,reviewer为'';回复的回复按钮则需要记录评论的cuid和该条回复的cuid,根据cuid查找到该条评论,往reply中push一条数据,reviewer为回复的cuid
递归调用
上面这种打平树的方式,主要还是想熟悉一下树这种数据结构,实际中可以跟后端接口协调好,直接返回打平好的数据即可,不用前端自己打平,省去很多操作;而面对这种树形结构的渲染,更多的是使用vue组件的递归渲染,与js递归调用一样,vue组件也可以自己调用自己。
组件递归调用主要是两点:
- 给组件赋予name属性,或者全局注册
- 组件在组件的模板内调用自己
首先是第一个,我们一般使用组件,是import导入,然后局部注册,但是在组件内无法import自己,所以需要给组件一个name属性以便找到自己,或者全局注册一下,这里选择name方式。
文件commentTree.vue
<template>
<div class="comment-wrap">
<comment-tree />
</div>
</template>
<script>
export default {
name: 'comment-tree'
}
</script>
这样在comment-tree组件中又调用了改组件,我们传一些参数进去,对于组件而言,这些参数是父级传入的props
<template>
<div class="comment-wrap">
<div class="comment-item">{{comment.cuid}}</div>
<div class="comment-text">{{comment.info}}</div>
<div class="comment-reviewer">{{comment.reviewer}}</div>
<comment-tree
v-for="(item, index) in reply"
:key="index"
:reply="item.reply"
:comment="item"
:deep="deep + 1"
/>
</div>
</template>
<script>
export default {
name: 'comment-tree',
props: ['reply', 'comment', 'deep']
}
</script>
加上deep参数可以对不同层级应用不同样式。
递归需要停止条件,这里v-for隐式条件,当reply为空时,v-for自动停止渲染,组件定义好以后,在app.vue中调用组件即可。
文件app.vue
<template>
<comment-tree
v-for="(item, index) in commentsList"
:key="index"
:reply="item.reply"
:comment="item"
:deep="0"
/>
</template>
<script>
import commentTree from './comment-tree.vue'
export default {
components: {
commentTree
},
data() {
commentsList: [] // 同上
}
}
</script>
完整代码(css待完善)
<template>
<div class="comment-wrap">
<div :class="deep >= 1 ? 'reply-item': 'comment-item'" :style="'margin-left:' + deep*30 + 'px'">
<div class="comment-title">
<div class="comment-id">{{comment.cuid}}</div>
<button class="btn" @click="addReply(comment.cuid)">回复</button>
</div>
<div v-if="comment.reviewer" class="comment-text">
回复<span>@{{comment.reviewer}}:</span>{{comment.info}}
</div>
<div v-else class="comment-text">
{{comment.info}}
</div>
</div>
<comment-tree
v-for="(item, key) in reply"
:key="key"
:reply="item.reply"
:comment="item"
:deep="deep+1"
/>
</div>
</template>
<script>
export default {
name: 'comment-tree',
props: ['comment', 'reply', 'deep'],
data() {
return {
}
},
methods: {
addReply(cuid) {
let item = {};
item.cuid = '小新';
item.info = '这是一条回复';
if (this.deep >= 1) {
item.reviewer = cuid;
}
item.reply = [];
this.reply.unshift(item);
}
}
}
</script>
<style scoped>
i {
font-style: normal;
color: blue;
}
.comment-wrap {
width: 1000px;
margin: 0 auto;
}
.comment-item {
margin: 10px 0;
}
.reply-item {
margin: 5px 0 5px 20px;
}
.comment-title {
display: flex;
justify-content: space-between;
}
.comment-text {
padding-left: 10px;
border-left: 5px solid #ddd;
}
</style>