前言
在本系列的文章中,我在第一篇和第二篇文章中寫的編寫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
歡迎轉載,請在明顯位置給出出處及鏈接