使用视图集简化代码

2020-05-215660 阅读3 评论

在 RESTful 架构中,对资源的常规操作无非就是查询、新增、修改、删除等这么几种。为此,django-rest-framework 分别提供了对应通用类视图函数。但是,如果对同一个资源的不同操作逻辑分散在各个视图函数中,从逻辑上来说不太合理,实际中管理起来也不是很方便,还会产生很多重复性的代码。因此,django-rest-framework 引入了视图集(Viewsets),把对同一个资源的不同操作,集中到一个类中。同样的,针对 Web 开发中的常见逻辑,django-rest-framework 也提供了通用视图集,进一步简化开发工作。

使用视图集的一个更大的好处,就是可以配合 django-rest-framework 提供的路由器(router),自动生成 API 的 URL,不需要我们再手工将 URL 模式和视图函数绑定了。所以大部分情况下,即使对资源只有一种操作,我们一般也会使用视图集。

先来看看博客首页文章列表视图集的代码:

blog/views.py

from rest_framework import viewsets
from rest_framework import mixins

class PostViewSet(mixins.ListModelMixin, viewsets.GenericViewSet):
    serializer_class = PostListSerializer
    queryset = Post.objects.all()
    pagination_class = PageNumberPagination
    permission_classes = [AllowAny]

所有视图集都要继承视图集的基类。视图集也有 2 个基类:ViewSetGenericViewSet,前者是最基本的视图集类,后者拓展自前者,拓展了很多 Web 开发中的通用逻辑。

要注意一点的是,视图集基类提供的是除资源操作以外的通用逻辑(例如 HTTP 请求预处理、HTTP 响应后处理、认证、鉴权等),而对于资源的操作(如序列化、更新、删除资源等)则放在相应的 Mixin 混入类里。django-rest-framework 提供了资源操作的 5 个混入类,分别对应资源的创建、查询、更新、删除。

  • CreateModelMixin

提供 create 方法用于创建资源

  • ListModelMixin 和 RetrieveModelMixin

提供 list 和 retrieve,分别用于获取资源列表和单个资源

  • UpdateModelMixin

提供 update 方法用于更新资源

  • DestroyModelMixin

提供 destroy 方法用于删除资源

此外,create、list、retrieve、update、destroy 的方法名会被映射为对应的 action,称为对资源操作的一个动作。前面说到视图集的一个最大好处就是可以使用路由器(router)自动生成 URL 模式。URL 正是根据 action 的类型来生成的,后面我们会具体说到。

好了,视图集已经创建完毕,接下来我们从视图集生成视图函数,并绑定 URL。

blog/views.py

index = PostViewSet.as_view({'get': 'list'})
blog/urls.py

app_name = "blog"
urlpatterns = [
    # ...
    # path("api/index/", views.IndexPostListAPIView.as_view()),
    path("api/index/", index),
]

等等,不是说视图集的一个好处是使用路由器自动生成 URL 模式吗?为什么还要手工创建视图函数,然后绑定 URL?

别急,这里只是演示一下如何从视图集生成视图函数并绑定 URL,这样能够帮助你更好地理解视图集的工作方式。事实上,使用路由器自动生成 URL 模式时,路由器内部就是采用了和上面手工生成视图函数并绑定 URL 一样的方式。

路由器的使用非常简单,我们在 初始化 RESTful API 风格的博客系统 中引入了 DefaultRouter 以开启 API 交互后台,DefaultRouter 实例化时默认帮我们注册了一个 API 交互后台的根视图,现在要注册一个新的视图,调用其 register 方法就可以了:

blogproject/urls.py

from blog.views import PostViewSet
from rest_framework.routers import DefaultRouter

router = DefaultRouter()
router.register(r'posts', PostViewSet, basename='post')

Django-rest-framework 提供 SimpleRouterDefaultRouter 两个路由器类,后者是对前者的拓展,因此通常情况下都使用后者。DefaultRouter 增加了一个 api 的根路由,访问根路由的 URL 就可以看到其他注册的全部 api 路由,一会儿我们将会看到具体的效果。

视图集自动生成 URL 模式非常简单,只需实例化一个路由器,然后调用其 register 方法,这个方法接收 3 个参数,第一个参数是 URL 前缀,所有从注册的视图集生成的 URL 都会带有这个前缀。第二个参数就是视图集,第三个参数 basename 用于指定视图集生成的视图函数名的前缀。在 django 的 URL 中,一条路由通常由 URL 模式,对应的视图函数和视图函数名组成。视图函数名的作用主要用于解析视图函数所对应的 URL。视图集最终会被转为多个视图函数,那么这个视图函数的名字是什么呢?django-rest-framework 的默认生成规则是 basename-action。

例如这里 basename='post',列出资源列表的 action 为 list(见上一篇教程中关于 action 的讲解),所以生成的获取文章资源列表的视图函数名为 post-list,使用 reverse('post-list') 就可以解析出获取文章资源列表的 API(URL)。

basename 可以不指定,django-rest-framework 会自动从视图集 get_queryset 方法返回的结果所关联的 model 获取一个默认值,其值为 model 名小写。不过,根据 Python 之禅,显式优于隐式,因此即使你设置的 basename 和 django-rest-framework 默认生成的一样,也比不指定要好。

刚才说了,我们使用 DefaultRouter 这个路由器,它会自动帮我们注册一个根路由,来看看根路由下有什么。

运行开发服务器,访问 http://127.0.0.1:8000/api/,界面如下:

API交互后台首页

django-rest-framework 为我们自动生成了 API 交互后台,在这个界面中可以和我们创建的 API 交互,非常方便。API 交互后台首页是所有注册的视图集对应的 URL。目前只有一条 /api/posts/,点击超链接进去,可以看到 /api/posts/ 的返回结果,即全部文章列表。

但是,目前我们的 api 一股脑将全部文章列表的返回了。但是我们的博客文章列表是有分页功能的,接下来我们就使用 django-rest-framework 提供的分页辅助类,一行代码就可以完成分页功能。

-- EOF --

3 评论
登录后回复
HuangChangHuai
2020-06-30 22:38:11

呃。。。博主,后面这段我照着打没有成功,小白一头雾水,麻烦博主有空帮我康康哪里出错了

#apis/views.py

from apis.views import ArticleViewSet
from rest_framework.routers import DefaultRouter

router = DefaultRouter()
router.register(r'posts', ArticleViewSet, basename='post')

#blogProject/urls.py
from apis.views import ArticleViewSet
from rest_framework.routers import DefaultRouter

router = DefaultRouter()
router.register(r'posts', ArticleViewSet, basename='post')

它一直提示说没有匹配到当前得路径,,,
什么
8000/api/posts/啊,
xxx/api/post/啊,
xxx/apis/posts/啊,
xxx/apis/post/都不行,程序知识响应404,又没有报错。。。

回复
HuangChangHuai HuangChangHuai
2020-06-30 22:40:32

复制得时候复制错了,问题里面得第一个代码块修正:

#apis/views.py
from rest_framework import viewsets
from rest_framework import mixins

class ArticleViewSet(mixins.ListModelMixin, viewsets.GenericViewSet):
    serializer_class = ArticleListSerializer
    queryset = Article.objects.all()
    pagination_class = PageNumberPagination
    permission_classes = [AllowAny]

回复
HuangChangHuai HuangChangHuai
2020-06-30 22:58:50

问题解决了,一个很操蛋得东西:
因为urls.py下面有:

urlpatterns=[
    path("api", include(router.urls)),
]

所以
from apis.views import ArticleViewSet
router.register(r'posts', ArticleViewSet, basename='post')

这个东西要放在urlpatterns这个列表得前面。。。

回复