教程 6:ViewSets 和路由器

REST 框架包括一个处理 ViewSets 的抽象,它允许开发者专注于对 API 的状态和交互进行建模,并将 URL 构造留给自动处理,该处理基于通用约定。

ViewSet 类与 View 类几乎相同,但它们提供诸如 retrieveupdate 的操作,而不是诸如 getput 的方法处理程序。

ViewSet 类仅在最后时刻绑定到一组方法处理程序,当它被实例化为一组视图时,通常通过使用 Router 类来处理为您定义 URL 配置的复杂性。

重构以使用 ViewSets

让我们获取当前的一组视图,并将它们重构为视图集。

首先,让我们将 UserListUserDetail 类重构为一个 UserViewSet 类。在 snippets/views.py 文件中,我们可以删除这两个视图类,并用一个 ViewSet 类替换它们

from rest_framework import viewsets


class UserViewSet(viewsets.ReadOnlyModelViewSet):
    """
    This viewset automatically provides `list` and `retrieve` actions.
    """
    queryset = User.objects.all()
    serializer_class = UserSerializer

在这里,我们使用了 ReadOnlyModelViewSet 类来自动提供默认的“只读”操作。我们仍在设置 querysetserializer_class 属性,就像我们在使用常规视图时所做的那样,但我们不再需要向两个单独的类提供相同的信息。

接下来,我们将替换 SnippetListSnippetDetailSnippetHighlight 视图类。我们可以删除这三个视图,并再次用一个类替换它们。

from rest_framework import permissions
from rest_framework import renderers
from rest_framework.decorators import action
from rest_framework.response import Response


class SnippetViewSet(viewsets.ModelViewSet):
    """
    This ViewSet automatically provides `list`, `create`, `retrieve`,
    `update` and `destroy` actions.

    Additionally we also provide an extra `highlight` action.
    """
    queryset = Snippet.objects.all()
    serializer_class = SnippetSerializer
    permission_classes = [permissions.IsAuthenticatedOrReadOnly,
                          IsOwnerOrReadOnly]

    @action(detail=True, renderer_classes=[renderers.StaticHTMLRenderer])
    def highlight(self, request, *args, **kwargs):
        snippet = self.get_object()
        return Response(snippet.highlighted)

    def perform_create(self, serializer):
        serializer.save(owner=self.request.user)

这次我们使用了 ModelViewSet 类来获取完整的默认读写操作集。

请注意,我们还使用了 @action 装饰器来创建名为 highlight 的自定义操作。此装饰器可用于添加不符合标准 create/update/delete 样式的任何自定义端点。

默认情况下,使用 @action 装饰器的自定义操作将响应 GET 请求。如果我们想要一个响应 POST 请求的操作,我们可以使用 methods 参数。

默认情况下,自定义操作的 URL 取决于方法名本身。如果你想更改 url 的构造方式,你可以将 url_path 包含为装饰器关键字参数。

显式将 ViewSets 绑定到 URL

处理程序方法仅在定义 URLConf 时绑定到操作。要了解其内部原理,我们首先从 ViewSets 显式创建一组视图。

snippets/urls.py 文件中,我们将 ViewSet 类绑定到一组具体视图中。

from rest_framework import renderers

from snippets.views import api_root, SnippetViewSet, UserViewSet

snippet_list = SnippetViewSet.as_view({
    'get': 'list',
    'post': 'create'
})
snippet_detail = SnippetViewSet.as_view({
    'get': 'retrieve',
    'put': 'update',
    'patch': 'partial_update',
    'delete': 'destroy'
})
snippet_highlight = SnippetViewSet.as_view({
    'get': 'highlight'
}, renderer_classes=[renderers.StaticHTMLRenderer])
user_list = UserViewSet.as_view({
    'get': 'list'
})
user_detail = UserViewSet.as_view({
    'get': 'retrieve'
})

请注意,我们通过将 HTTP 方法绑定到每个视图所需的相应操作,从每个 ViewSet 类创建多个视图。

现在,我们已将资源绑定到具体视图,可以像往常一样将这些视图注册到 URL 配置中。

urlpatterns = format_suffix_patterns([
    path('', api_root),
    path('snippets/', snippet_list, name='snippet-list'),
    path('snippets/<int:pk>/', snippet_detail, name='snippet-detail'),
    path('snippets/<int:pk>/highlight/', snippet_highlight, name='snippet-highlight'),
    path('users/', user_list, name='user-list'),
    path('users/<int:pk>/', user_detail, name='user-detail')
])

使用路由器

由于我们使用的是 ViewSet 类,而不是 View 类,因此实际上无需自己设计 URL 配置。可以使用 Router 类自动处理将资源连接到视图和 URL 的约定。我们只需将适当的视图集注册到路由器,然后让它完成剩下的工作即可。

以下是我们重新连接的 snippets/urls.py 文件。

from django.urls import path, include
from rest_framework.routers import DefaultRouter

from snippets import views

# Create a router and register our ViewSets with it.
router = DefaultRouter()
router.register(r'snippets', views.SnippetViewSet, basename='snippet')
router.register(r'users', views.UserViewSet, basename='user')

# The API URLs are now determined automatically by the router.
urlpatterns = [
    path('', include(router.urls)),
]

将 ViewSets 注册到路由器类似于提供 urlpattern。我们包含两个参数 - 视图的 URL 前缀和视图集本身。

我们使用的 DefaultRouter 类还自动为我们创建 API 根视图,因此我们现在可以从 views 模块中删除 api_root 函数。

视图与 ViewSets 之间的权衡

使用 ViewSets 可以是一个非常有用的抽象。它有助于确保 URL 约定在整个 API 中保持一致,最大程度地减少需要编写的代码量,并允许你专注于 API 提供的交互和表示,而不是 URL 配置的具体内容。

这并不意味着它始终是正确的做法。在使用基于类的视图而不是基于函数的视图时,需要考虑类似的权衡。使用 ViewSets 不如单独构建 API 视图那么明确。