递归函数大家都很熟悉,就是调用自己或者是调用其他函数后再次调用自己,直到满足特定情况的时候,停止递归调用,开始从最后一个递归调用返回。本文以多级留言回复为例,讲解在DRF和VUE如何使用递归。
一、场景案例
1. 使用场景
最近在开发评论留言模块时,经常有这样的场景。一条评论后面跟多条回复,子回复后面又有多条这条回复的回复,层层嵌套,效果如下图所示。
除了多级留言回复,菜单的多级列表,以及树形组件中都会用到,使用递归思想都可以完美的解决这些问题。
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库写法
- 除了使用原生的写法外,DRF提供了递归调用序列化器库django-rest-framework-recursive,具体用法参考官方文档:https://github.com/heywbj/django-rest-framework-recursive
- 安装djangorestframework-recursive
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. 效果演示
