vue评论盖楼(1)-普通渲染/递归渲染

2,902 阅读4分钟

需求说明

实习期间需要完成一个给评论无限盖楼的功能,模拟b站的评论效果,如图:

只考虑评论的功能,首先实现以下三点:

  1. 评论跟回复的样式一样,包括用户名和评论内容
  2. 一级回复,即评论的回复,直接显示评论内容
  3. 二级及以上回复,即回复的回复,需要在评论内容前,加上“回复@某某某”的说明

数据结构

对于这种嵌套层级的,如多级菜单,一级二级城市,都是树形结构,对于评论而言,模拟数据如下:

  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组件也可以自己调用自己。

组件递归调用主要是两点:

  1. 给组件赋予name属性,或者全局注册
  2. 组件在组件的模板内调用自己

首先是第一个,我们一般使用组件,是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>