Django編寫RESTful API(六):ViewSets和Routers


前言

在本系列的文章中,我在第一篇和第二篇文章中寫的編寫Django視圖時,使用的都是基於函數的方法,並且每個視圖函數之前都會加一個django-rest-framework帶的裝飾器@api_view。然后在第三篇文章,我們就開始把基於函數的視圖改成了基於類的視圖,然后發現這樣做視圖部分減少了很多代碼量。

在這一篇文章中,我要介紹的是另一種基於類的視圖的寫法,它的抽象程度更高,也可以說是代碼量又減少了。OK,廢話不多說,先進入主題~


使用ViewSets重構視圖

先介紹一下這個ViewSets。ViewSets,翻譯過來可以說是視圖集,也就是幾個視圖的集合。

拿本項目為例子,我們之前查看所有用戶列表就要寫一個視圖類UserList,並在urls.py中為其設置一個模式然后as_view使用它,然后要看單個用戶的詳情頁就要再寫一個UserDetail視圖類並再在添加一個url模式。同時注意到這兩個視圖類都是繼承的generics.XXXAPIView。而使用ViewSets我們就可以把UserList和UserDetail合並成UserViewSet視圖類,並且繼承的類改為viewsets.ReadOnlyModelViewSet,這樣就是一個視圖集了。

還是有點懵逼?沒事,下面看看代碼。編輯snippets/view.py,導入viewsets並使用UserViewSet來替換掉UserList和UserDetail:

from rest_framework import viewsets

class UserViewSet(viewsets.ReadOnlyModelViewSet):
    """
    viewset自動提供了list和detail動作
    """
    queryset = User.objects.all()
    serializer_class = UserSerializer

 

這里面的queryset和serializer_class的值還是和原來一樣。因為關於User的API都是只讀的,所以我們繼承了一個ReadOnlyModelViewSet類,這樣就把原先的兩個視圖類集合起來了。原本類里面的:

queryset = User.objects.all()
serializer_class = UserSerializer

 

這部分屬於重復代碼,所以通過視圖集來實現視圖類我們的代碼量確實減少了,更加簡潔。

ViewSet類與View類其實幾乎是相同的,但提供的是read或update這些操作,而不是get或put 等HTTP動作。同時,ViewSet為我們提供了默認的URL結構, 使得我們能更專注於API本身。

上面這段話呢,是官方文檔里面說的,想就這樣看看就算了來理解也行,不過如果我們看一下源碼也許能理解的更好。因為我用的是PyCharm,所以查看源碼很方便,按住CTRL鍵然后鼠標點擊一下就會自動跳轉了,首先查看一下ReadOnlyModelViewSet,發現它是這樣的:

class ReadOnlyModelViewSet(mixins.RetrieveModelMixin,
                           mixins.ListModelMixin,
                           GenericViewSet):
    """
    A viewset that provides default `list()` and `retrieve()` actions.
    """
    pass

 

發現原來有用到之前說的mixins,所以剛才才說ViewSet類與View類其實幾乎是相同的。但是這里多了一個GenericViewSet類是新的內容,繼續CTRL點擊查看其代碼,發現它內部只是一個pass然后就沒有其他的操作了,但是可以繼續查看其父類ViewSetMixin的源碼來了解ViewSets,然后就可以看到這個ViewSetMixin其實重寫了as_view方法:

@classonlymethod
    def as_view(cls, actions=None, **initkwargs):
        """
        Because of the way class based views create a closure around the
        instantiated view, we need to totally reimplement `.as_view`,
        and slightly modify the view function that is created and returned.
        """
        ...

 

我們平時使用視圖類的時候,編寫urls.py時,就一個XXX.as_view(),現在使用ViewSets,需要傳入參數,大概像下面這樣的:

UserViewSet.as_view({'get': 'list'})

 

之后url就配置好了,也就是上面說的ViewSet為我們提供了默認的URL結構。當然了,這個還不是完整的url模式,稍后補全。

剛才把User的兩個視圖類合並成視圖集了,那么Snippet的幾個視圖類操作上也是差不多的。用視圖集SnippetViewSet代替SnippetList, SnippetDetail 和 SnippetHighlight這三個視圖類:

from rest_framework.decorators import detail_route

class SnippetViewSet(viewsets.ModelViewSet):
    """
    viewset自動提供了`list`, `create`, `retrieve`,
    `update` 和 `destroy` 動作.

    同時我們手動增加一個額外的'highlight'動作用於查看高亮的代碼段
    """
    queryset = Snippet.objects.all()
    serializer_class = SnippetSerializer
    permission_classes = (permissions.IsAuthenticatedOrReadOnly,
                          IsOwnerOrReadOnly,)

    @detail_route(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)

 

因為查看highlight不像其他動作那樣,django-rest-framework並沒有替我們封裝好,所以我們需要自己添加這個額外的動作,要記得在方法前面加上裝飾器@detail_route,這個裝飾器就是用來創建自定義的動作,當然我們的自定義動作不可以是create/update/delete這些標准的,否則會有沖突。

還有一點,用@detail_route裝飾器定義的動作默認是GET請求,需要其他的請求方式可以傳入methods參數給這個裝飾器。同樣的,默認情況下,自定義操作的URL取決於方法名稱本身。如果要更改url應該構造的方式,可以將url_path作為decorator的關鍵字參數。

最后還要注意繼承的類是ModelViewSet和剛才的也有點不同,為什么換成這個,也可以看看源碼能略知一二:

class ModelViewSet(mixins.CreateModelMixin,
                   mixins.RetrieveModelMixin,
                   mixins.UpdateModelMixin,
                   mixins.DestroyModelMixin,
                   mixins.ListModelMixin,
                   GenericViewSet):

 


將ViewSets明確的綁定到URL

根據上面所說的,每個視圖集的url模式都需要我們在as_view中傳入參數,把snippets/urls.py的代碼換成下面的:

from django.conf.urls import url,include
from snippets.views import SnippetViewSet, UserViewSet, api_root
from rest_framework import renderers
from rest_framework.urlpatterns import format_suffix_patterns


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'
})


urlpatterns = format_suffix_patterns([
    url(r'^$', api_root),
    url(r'^snippets/$', snippet_list, name='snippet-list'),
    url(r'^snippets/(?P<pk>[0-9]+)/$', snippet_detail, name='snippet-detail'),
    url(r'^snippets/(?P<pk>[0-9]+)/highlight/$', snippet_highlight, name='snippet-highlight'),
    url(r'^users/$', user_list, name='user-list'),
    url(r'^users/(?P<pk>[0-9]+)/$', user_detail, name='user-detail')
])

 

OK,到了這里對視圖的改造已經完成了,可以啟動服務器測試一下,我們的項目功能還是和之前的一樣的。


使用Routers

不過看到urls.py的代碼,我們可能會發現一個問題,就是我們的視圖類代碼簡潔了變少了,但是url.py的代碼量好像多了啊,要綁定那么多動作,這樣算起來好像也沒多大提升?

確實是這樣。但是我們這可是在用python開發啊,當然是能短則短了,沒錯,django-rest-framework的作者也是這么想的,所以我們又有現成的輪子可以使用了。這個輪子就是本文的另一個主角——Routers。用起來也是簡單粗暴,重寫urls.py:

from django.conf.urls import url, include
from snippets import views
from rest_framework.routers import DefaultRouter

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

# The API URLs are now determined automatically by the router.
# Additionally, we include the login URLs for the browsable API.
urlpatterns = [
    url(r'^', include(router.urls)),
]

 

這樣就搞定了,代碼少了很多,連原來用來設置后綴的下面這行代碼都不需要了。

urlpatterns = format_suffix_patterns(urlpatterns)

 

而且這個DefaultRouter 類還會自動幫我們創建API根視圖,也就是說view.py中的api_root方法也可以刪除掉了。

額...這個Routers幫我們做的事情真是有點多啊。。不過這也就是我為什么在文章的前言里面說使用ViewSets會比原本的視圖更抽象的原因。

拿過來用是會了,但是這里面發生了什么我們完全不知道啊,比如說API后綴去哪了?上面我們寫的:

snippet_list = SnippetViewSet.as_view({
    'get': 'list',
    'post': 'create'
})

 

這些綁定全都自動生成了?這些確實都是DefaultRouter 幫我們做好了,怎么做的,我們還是可以看一下源碼了解一下大概的過程。首先就是register方法,我們綁定了那么多動作它兩行就搞定了,查看它的源碼,發現它是BaseRouter類下的一個方法:

class BaseRouter(object):
    def __init__(self):
        self.registry = []

    def register(self, prefix, viewset, base_name=None):
        if base_name is None:
            base_name = self.get_default_base_name(viewset)
        self.registry.append((prefix, viewset, base_name))

    ...

    @property
    def urls(self):
        if not hasattr(self, '_urls'):
            self._urls = self.get_urls()
        return self._urls

 

改方法根據傳進來的參數生成url端點,也就是/snippets和/users,然后存到registry列表中。並且這個類的最后是一個可以當屬性用的方法urls,而這個方法里面又調用了get_urls()來生成所有的url模式,當然這個get_urls()被子類SimpleRouter和子子類DefaultRouter重寫了。SimpleRouter中的get_urls()實現了生成是5個url模式,也就是原本的:

url(r'^snippets/$',snippet_list,name='snippet-list'),
url(r'^snippets/(?P<pk>[0-9]+)/$', snippet_detail, name='snippet-detail'),
url(r'^snippets/(?P<pk>[0-9]+)/highlight/$', snippet_highlight, name='snippet-highlight'),
url(r'^users/$', user_list, name='user-list'),
url(r'^users/(?P<pk>[0-9]+)/$', user_detail, name='user-detail')

 

而DefaultRouter中的get_urls()中則生成了api_root的url模式,同時還為這些url模式加了格式后綴,所以我們自己不會用到format_suffix_patterns這個東西。


總結

當然了,並不一定要使用ViewSets的視圖代替View,兩者各有好處ViewSets節省了很多代碼並且url模式也不用我們自己設置了,但是也會帶來一些不確定性,自動化的效果有時候可能和你預想的不太一樣,所以想要選擇哪種方法看你自己喜歡。


好了,關於ViewSets和Routers的介紹就到這里了,API的功能和之前的一樣,所以這里就不做展示了。

本文地址:http://www.cnblogs.com/zivwong/p/7461764.html
作者博客:ziv
歡迎轉載,請在明顯位置給出出處及鏈接

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM