递归函数大家都很熟悉,就是调用自己或者是调用其他函数后再次调用自己,直到满足特定情况的时候,停止递归调用,开始从最后一个递归调用返回。本文以多级留言回复为例,讲解在DRF和VUE如何使用递归。

一、场景案例

1. 使用场景

最近在开发评论留言模块时,经常有这样的场景。一条评论后面跟多条回复,子回复后面又有多条这条回复的回复,层层嵌套,效果如下图所示。 image.png 除了多级留言回复,菜单的多级列表,以及树形组件中都会用到,使用递归思想都可以完美的解决这些问题。

2. 思路分析

以多级评论回复为例,每条评论中要包含一个child,存放这条评论的回复,回复中也包含child,存放这条回复的回复,层层递归……

二、DRF递归序列化器

1. 数据库模型(models.py)

  • 注意的是father字段,用于关联父留言,通过外键关联自己。增加related_name字段,指定这个字表在主表中对应的外键属性,用于关联查询。
class LeaveMessage(models.Model):
    content = models.TextField(verbose_name='留言内容', blank=True, null=True)
    user = models.ForeignKey(UserInfo, on_delete=models.CASCADE, blank=True, null=True, verbose_name='用户名')
    time = models.DateTimeField(auto_now_add=True, verbose_name='留言时间')
    like = models.IntegerField(verbose_name='留言点赞数', default=0)
    father = models.ForeignKey('self', verbose_name='父留言', on_delete=models.CASCADE, blank=True, null=True,
                               related_name='sub')

    class Meta:
        ordering = ('father_id', '-time')
        verbose_name = '留言记录'
        verbose_name_plural = verbose_name

    # 获取回复用户名
    def get_father_name(self):
        if self.father:
            return self.father.user.username
        return

    def __str__(self):
        return self.content

2. 序列化器(serializers.py)——原生写法

  • 添加child字段,通过序列化类上的方法来获取其值。
  • 定义child字段具体实现方法:筛选LeaveMessage模型中,father字段id与当前对象id相同的记录(即该条评论有回复内容),返回LeaveMessageSerializer序列化器,从而实现序列化器递归调用
class LeaveMessageSerializer(serializers.ModelSerializer):
    """
    留言记录序列化器
    """
    username = serializers.ReadOnlyField(source='user.username')
    photo = serializers.ReadOnlyField(source='user.photo')
    child = serializers.SerializerMethodField()
    father_name = serializers.ReadOnlyField(source='get_father_name')

    class Meta:
        model = LeaveMessage
        fields = "__all__"
    def get_child(self, obj):
        if leaveMessage := LeaveMessage.objects.filter(father=obj.pk).first():
            return LeaveMessageSerializer(leaveMessage).data
        else:
            return None

3. 序列化器(serializers.py)——RecursiveField库写法

pip install djangorestframework-recursive 

  • 模型中定义查询获取子回复的方法
class LeaveMessage(models.Model):
    content = models.TextField(verbose_name='留言内容', blank=True, null=True)
    user = models.ForeignKey(UserInfo, on_delete=models.CASCADE, blank=True, null=True, verbose_name='用户名')
    time = models.DateTimeField(auto_now_add=True, verbose_name='留言时间')
    like = models.IntegerField(verbose_name='留言点赞数', default=0)
    father = models.ForeignKey('self', verbose_name='父留言', on_delete=models.CASCADE, blank=True, null=True,
                               related_name='sub')

    class Meta:
        ordering = ('father_id', '-time')
        verbose_name = '留言记录'
        verbose_name_plural = verbose_name

    # 获取子回复
    def get_child(self):
        return self.sub.all()
    # 或者可以return self.sub.filter(father=self.pk),因为这个self.sub 本来就是 self自身

    # 获取留言用户名
    def get_father_name(self):
        if self.father:
            return self.father.user.username
        return

    def __str__(self):
        return self.content
  • 序列化器中指定source为get_child即可。
from rest_framework_recursive.fields import RecursiveField
from record.models import LeaveMessage

class LeaveMessageSerializer(serializers.ModelSerializer):
    """
    留言记录序列化器
    """
    username = serializers.ReadOnlyField(source='user.username')
    photo = serializers.ReadOnlyField(source='user.photo')
    child = serializers.ListField(source='get_child', child=RecursiveField())
    father_name = serializers.ReadOnlyField(source='get_father_name')

    class Meta:
        model = LeaveMessage
        fields = "__all__"

4. 视图(views.py)

  • 因为是对留言记录表进行增删改查操作,所以视图选用模型视图集即可。
  • 查询集选择根留言即可,序列化器会通过递归调用,获取这个根留言所以关联的子回复记录。
from record.models import LeaveMessage
from record.serializers import LeaveMessageSerializer

class LeaveMessageModelViewSet(viewsets.ModelViewSet):
    """
    留言增删改查
    """
    queryset = LeaveMessage.objects.filter(father=None)
    serializer_class = LeaveMessageSerializer

5. 路由(urls.py)

  • 对应视图集view,使用DRF提供的Routers来帮助我们快速实现路由列表信息。
router = routers.DefaultRouter()
router.register('leaveMessage', views.LeaveMessageModelViewSet, 'leaveMessage')
# 留言增删改查
urlpatterns += router.urls

6. 效果演示

  • 接下来使用postman等API接口测试工具,发送get请求,获得如下响应内容:
[
    {
        "id": 2,
        "username": "adc",
        "photo": "https://oss.cuiliangblog.cn/photo/2021_6_4_12_24_24-1622780664642.jpg",
        "child": [
            {
                "id": 4,
                "username": "admin",
                "photo": "https://oss.cuiliangblog.cn/photo/default.jpg",
                "child": [
                    {
                        "id": 5,
                        "username": "adc",
                        "photo": "https://oss.cuiliangblog.cn/photo/2021_6_4_12_24_24-1622780664642.jpg",
                        "child": [],
                        "father_name": "admin",
                        "content": "一定一定!",
                        "time": "2021-06-07T19:19:12.982513+08:00",
                        "like": 2,
                        "user": 6,
                        "father": 4
                    }
                ],
                "father_name": "adc",
                "content": "热烈欢迎,有空多来看看",
                "time": "2021-05-08T19:16:48.109961+08:00",
                "like": 3,
                "user": 1,
                "father": 2
            }
        ],
        "father_name": null,
        "content": "good good study,day day up",
        "time": "2021-04-03T19:14:46.267388+08:00",
        "like": 3,
        "user": 6,
        "father": null
    },
    {
        "id": 1,
        "username": "南城阿宇",
        "photo": "https://oss.cuiliangblog.cn/photo/2020_12_26_18_08_54_811414_M52cgvE.jpg",
        "child": [
            {
                "id": 3,
                "username": "admin",
                "photo": "https://oss.cuiliangblog.cn/photo/default.jpg",
                "child": [],
                "father_name": "南城阿宇",
                "content": "欢迎欢迎",
                "time": "2021-05-01T19:16:13.131192+08:00",
                "like": 2,
                "user": 1,
                "father": 1
            }
        ],
        "father_name": null,
        "content": "我过来看看",
        "time": "2021-03-02T19:13:51.456670+08:00",
        "like": 10,
        "user": 2,
        "father": null
    }
]
  • 至此,后端DRF递归调用序列化器已全部完成,接下来开发前端代码。

三、VUE递归组件

1. 父组件(Message.vue)

  • 父组件获取评论回复列表数据,然后传递给子组件
<template>
  <div>
      <div class="comment-list">
        <Comments :commentsList="messageList"></Comments>
      </div>
  </div>
</template>

<script>
import Comments from "@/components/common/Comments";
import {onMounted, ref} from "vue";

export default {
  components: {
    Comments,
  },
  name: "Message",
  setup() {
    // 留言列表
    const messageList = ref([])

    // 获取留言列表
    async function leaveMessageData() {
      messageList.value = await getLeaveMessage()
    }

    // 留言内容
    const message = ref()
    // 点击发表留言事件
    const clickSend = () => {
      Toast("发表评论啦")
    }
    onMounted(() => {
      leaveMessageData()
    })
    return {
      messageList,
      message,
      clickSend
    }
  }
}
</script>

<style scoped lang="scss">
@import "~@/assets/style/variable";

.comment-list {
  padding: 10px;
  background-color: white;
}


</style>

2. 子组件(Comments.vue)

  • 其中最重要的一点是:如果存在child,那么就递归调用自己,从而实现多级评论回复渲染。
<!--评论回复组件-->
<template>
  <div class="comments" v-for="(item,index) in commentsList" :key="index">
    <ol>
      <div class="comment-info">
      …………
      </div>
      <div class="reply" v-if="item.child">
        <Comments :commentsList="item.child"></Comments>
      </div>
    </ol>
  </div>
</template>

<script>
import {ref} from "vue";
import timeFormat from "@/utils/timeFormat";

export default {
  components: {
  },
  props: {
    // 评论回复列表
    commentsList: {
      type: Array, default() {
        return []
      }
    },
  },
  name: "Comments",
  setup(props) {
    }
    return {
    }
  }
}
</script>

<style lang="scss" scoped>
@import "src/assets/style/variable";
</style>

3. 效果演示

image.png

0 子项 · 2 次浏览 · 2026-09-24