深入解析當下大熱的前后端分離組件django-rest_framework系列二


視圖三部曲

一部曲 · 使用混合(mixins)

上一節的視圖部分:

復制代碼
復制代碼
from rest_framework.views import APIView from rest_framework.response import Response from .models import * from django.shortcuts import HttpResponse from django.core import serializers from rest_framework import serializers class BookSerializers(serializers.ModelSerializer): class Meta: model=Book fields="__all__" #depth=1 class PublshSerializers(serializers.ModelSerializer): class Meta: model=Publish fields="__all__" depth=1 class BookViewSet(APIView): def get(self,request,*args,**kwargs): book_list=Book.objects.all() bs=BookSerializers(book_list,many=True,context={'request': request}) return Response(bs.data) def post(self,request,*args,**kwargs): print(request.data) bs=BookSerializers(data=request.data,many=False) if bs.is_valid(): print(bs.validated_data) bs.save() return Response(bs.data) else: return HttpResponse(bs.errors) class BookDetailViewSet(APIView): def get(self,request,pk): book_obj=Book.objects.filter(pk=pk).first() bs=BookSerializers(book_obj,context={'request': request}) return Response(bs.data) def put(self,request,pk): book_obj=Book.objects.filter(pk=pk).first() bs=BookSerializers(book_obj,data=request.data,context={'request': request}) if bs.is_valid(): bs.save() return Response(bs.data) else: return HttpResponse(bs.errors) class PublishViewSet(APIView): def get(self,request,*args,**kwargs): publish_list=Publish.objects.all() bs=PublshSerializers(publish_list,many=True,context={'request': request}) return Response(bs.data) def post(self,request,*args,**kwargs): bs=PublshSerializers(data=request.data,many=False) if bs.is_valid(): # print(bs.validated_data)  bs.save() return Response(bs.data) else: return HttpResponse(bs.errors) class PublishDetailViewSet(APIView): def get(self,request,pk): publish_obj=Publish.objects.filter(pk=pk).first() bs=PublshSerializers(publish_obj,context={'request': request}) return Response(bs.data) def put(self,request,pk): publish_obj=Publish.objects.filter(pk=pk).first() bs=PublshSerializers(publish_obj,data=request.data,context={'request': request}) if bs.is_valid(): bs.save() return Response(bs.data) else: return HttpResponse(bs.errors)
復制代碼
復制代碼

我們使用這一套邏輯,意味着有一張模型表,就要將上面的代碼寫一遍,代碼的復用性很差,所有,我們要對復用的部分進行封裝,我們通過三步,一步一步的來實現代碼的復用和封裝。

第一步:大體思路是使用混合類(多繼承的形式),借助封裝好的mixins類

from rest_framework import mixins

  我們可以發現,這些復用的代碼中,有兩個變量是必須要知道的,一個是模型表中的數據,一個是該模型表對應的serializers對象,所以我們可以將這兩個變量單獨給提出來放到類的靜態屬性中,這樣整個類中都可以調用。

使用方法:

將查看所有數據的方法封裝到一個類中:

復制代碼
class ListModelMixin(object): """ List a queryset. """ def list(self, request, *args, **kwargs): queryset = self.filter_queryset(self.get_queryset()) page = self.paginate_queryset(queryset) if page is not None: serializer = self.get_serializer(page, many=True) return self.get_paginated_response(serializer.data) serializer = self.get_serializer(queryset, many=True) return Response(serializer.data)
復制代碼

所以,我們處理查看所有數據的視圖時,直接將這個類繼承,同時,在get方法中,將list方法返回即可。

同樣的方式,將添加數據,查看某一條數據,編輯某一條數據,刪除某一條數據也分別封裝到一個個類下,只需要在對應的請求方法中,返回繼承類中對應的方法即可。

添加數據封裝到類:

復制代碼
class CreateModelMixin(object): """ Create a model instance. """ def create(self, request, *args, **kwargs): serializer = self.get_serializer(data=request.data) serializer.is_valid(raise_exception=True) self.perform_create(serializer) headers = self.get_success_headers(serializer.data) return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers) def perform_create(self, serializer): serializer.save() def get_success_headers(self, data): try: return {'Location': str(data[api_settings.URL_FIELD_NAME])} except (TypeError, KeyError): return {}
復制代碼

 

查看某一條數據封裝的類:

復制代碼
class RetrieveModelMixin(object): """ Retrieve a model instance. """ def retrieve(self, request, *args, **kwargs): instance = self.get_object() serializer = self.get_serializer(instance) return Response(serializer.data)
復制代碼

 

編輯數據的類:

復制代碼
class UpdateModelMixin(object): """ Update a model instance. """ def update(self, request, *args, **kwargs): partial = kwargs.pop('partial', False) instance = self.get_object() serializer = self.get_serializer(instance, data=request.data, partial=partial) serializer.is_valid(raise_exception=True) self.perform_update(serializer) if getattr(instance, '_prefetched_objects_cache', None): # If 'prefetch_related' has been applied to a queryset, we need to # forcibly invalidate the prefetch cache on the instance. instance._prefetched_objects_cache = {} return Response(serializer.data) def perform_update(self, serializer): serializer.save() def partial_update(self, request, *args, **kwargs): kwargs['partial'] = True return self.update(request, *args, **kwargs)
復制代碼

 

刪除數據的類:

復制代碼
class DestroyModelMixin(object): """ Destroy a model instance. """ def destroy(self, request, *args, **kwargs): instance = self.get_object() self.perform_destroy(instance) return Response(status=status.HTTP_204_NO_CONTENT) def perform_destroy(self, instance): instance.delete()
復制代碼

 

通過上面的方式,我們會有一個疑問,我們傳入的pk值,是怎么處理的。

我們在處理類的視圖函數時,必須要繼承一個APIView的類,同樣的,也將這個類進行了封裝:放到了rest_framework.generics下面的GenericAPIView中;

from rest_framework import views class GenericAPIView(views.APIView): pass

這個類繼承了APIView,並進行了相應的擴展。我們傳的pk值,就是在這里被處理的。

處理單條數據時,我們肯定要先將對應的數據取出來,再做對應的處理,同樣的,封裝到類中肯定也做了類似的處理,我們觀察處理單條數據的類,會發現這樣一行代碼:instance = self.get_object()  很明顯,肯定是取數據去了,怎么取?不知道,看看,先找到這個方法。我們從本類和繼承的類中依次去找,最后在GenericAPIView中找到了這個方法:

復制代碼
    def get_object(self): """ Returns the object the view is displaying. You may want to override this if you need to provide non-standard queryset lookups. Eg if objects are referenced using multiple keyword arguments in the url conf. """ queryset = self.filter_queryset(self.get_queryset()) # Perform the lookup filtering. lookup_url_kwarg = self.lookup_url_kwarg or self.lookup_field assert lookup_url_kwarg in self.kwargs, ( 'Expected view %s to be called with a URL keyword argument ' 'named "%s". Fix your URL conf, or set the `.lookup_field` ' 'attribute on the view correctly.' % (self.__class__.__name__, lookup_url_kwarg) ) filter_kwargs = {self.lookup_field: self.kwargs[lookup_url_kwarg]} obj = get_object_or_404(queryset, **filter_kwargs) # May raise a permission denied  self.check_object_permissions(self.request, obj) return obj
復制代碼

這個方法最終將obj返回了,那就要好奇了,怎么一下子將obj取出來的,繼續查看  obj=get_object_or_404(queryset,**fi..)

復制代碼
def get_object_or_404(queryset, *filter_args, **filter_kwargs): """ Same as Django's standard shortcut, but make sure to also raise 404 if the filter_kwargs don't match the required types. """ try: return _get_object_or_404(queryset, *filter_args, **filter_kwargs) except (TypeError, ValueError, ValidationError): raise Http404
復制代碼

進行了一個異常處理,還是沒有我們要的內容,繼續找  _get_object_or_404(queryset,*...,**...)

復制代碼
def get_object_or_404(klass, *args, **kwargs): """ Uses get() to return an object, or raises a Http404 exception if the object does not exist. klass may be a Model, Manager, or QuerySet object. All other passed arguments and keyword arguments are used in the get() query. Note: Like with get(), an MultipleObjectsReturned will be raised if more than one object is found. """ queryset = _get_queryset(klass) try: return queryset.get(*args, **kwargs) except AttributeError: klass__name = klass.__name__ if isinstance(klass, type) else klass.__class__.__name__ raise ValueError( "First argument to get_object_or_404() must be a Model, Manager, " "or QuerySet, not '%s'." % klass__name ) except queryset.model.DoesNotExist: raise Http404('No %s matches the given query.' % queryset.model._meta.object_name)
復制代碼

這么一堆中,就  queryset.get(*args,**kwargs)  是我們要的內容,我們通過url傳入pk=val,被**kwargs捕獲,傳入這個代碼中,等同於models.模型類.objects.all().get(**{"pk":val}),這種形式很眼熟,這就是我們取值的操作。

mixin類編寫視圖

復制代碼
復制代碼
from rest_framework import mixins from rest_framework import generics class BookViewSet(mixins.ListModelMixin, mixins.CreateModelMixin, generics.GenericAPIView): queryset = Book.objects.all() serializer_class = BookSerializers def get(self, request, *args, **kwargs): return self.list(request, *args, **kwargs) def post(self, request, *args, **kwargs): return self.create(request, *args, **kwargs) class BookDetailViewSet(mixins.RetrieveModelMixin, mixins.UpdateModelMixin, mixins.DestroyModelMixin, generics.GenericAPIView): queryset = Book.objects.all() serializer_class = BookSerializers def get(self, request, *args, **kwargs): return self.retrieve(request, *args, **kwargs) def put(self, request, *args, **kwargs): return self.update(request, *args, **kwargs) def delete(self, request, *args, **kwargs): return self.destroy(request, *args, **kwargs)
復制代碼
復制代碼

特別需要注意的是,使用這種方式,設計url時,需要傳參的url,必須使用分組命名匹配的形式,而且,命名必須是pk,否則報錯

復制代碼
urlpatterns = [

  # 錯誤寫法 url(r'^books/$',views.Book.as_view()), url(r'^books/(\d+)/$',views.BookDeail.as_view()),
  # 正確寫法 url(r'^publish/$',views.Publish.as_view()), url(r'^publish/(?P<pk>\d+)/$',views.PublishDeail.as_view()), #必須是pk ]
復制代碼

采用這種方式匹配url,跟封裝的源碼有關;

復制代碼
class GenericAPIView(views.APIView): """ Base class for all other generic views. """ # You'll need to either set these attributes, # or override `get_queryset()`/`get_serializer_class()`. # If you are overriding a view method, it is important that you call # `get_queryset()` instead of accessing the `queryset` property directly, # as `queryset` will get evaluated only once, and those results are cached # for all subsequent requests. queryset = None serializer_class = None # If you want to use object lookups other than pk, set 'lookup_field'. # For more complex lookup requirements override `get_object()`. lookup_field = 'pk' 
復制代碼

上面的代碼雖然一定程度上減少了代碼的復用性,但是,我們對於每一個模型表任然要進行大量的復用代碼。

備注:在繼承類中配置了lookup_field屬性,If you want to use object lookups other than pk, set 'lookup_field'

第二步:將這些封裝的類進一步封裝。

二部曲 · 使用通用的基於類的視圖

通過使用mixin類,我們使用更少的代碼重寫了這些視圖,但我們還可以再進一步。REST框架提供了一組已經混合好(mixed-in)的通用視圖,我們可以使用它來簡化我們的views.py模塊。

將我們不需要接收傳參的視圖函數整體封裝到一個類(查看所有數據,添加數據)

復制代碼
class ListCreateAPIView(mixins.ListModelMixin, mixins.CreateModelMixin, GenericAPIView): """ Concrete view for listing a queryset or creating a model instance. """ def get(self, request, *args, **kwargs): return self.list(request, *args, **kwargs) def post(self, request, *args, **kwargs): return self.create(request, *args, **kwargs)
復制代碼

同樣的,需要接收一個pk值的視圖封裝:

復制代碼
class RetrieveUpdateDestroyAPIView(mixins.RetrieveModelMixin, mixins.UpdateModelMixin, mixins.DestroyModelMixin, GenericAPIView): """ Concrete view for retrieving, updating or deleting a model instance. """ def get(self, request, *args, **kwargs): return self.retrieve(request, *args, **kwargs) def put(self, request, *args, **kwargs): return self.update(request, *args, **kwargs) def patch(self, request, *args, **kwargs): return self.partial_update(request, *args, **kwargs) def delete(self, request, *args, **kwargs): return self.destroy(request, *args, **kwargs)
復制代碼

借助這個封裝,進一步的簡化代碼量

復制代碼
復制代碼
from rest_framework import mixins from rest_framework import generics class BookViewSet(generics.ListCreateAPIView): queryset = Book.objects.all() serializer_class = BookSerializers class BookDetailViewSet(generics.RetrieveUpdateDestroyAPIView): queryset = Book.objects.all() serializer_class = BookSerializers class PublishViewSet(generics.ListCreateAPIView): queryset = Publish.objects.all() serializer_class = PublshSerializers class PublishDetailViewSet(generics.RetrieveUpdateDestroyAPIView): queryset = Publish.objects.all() serializer_class = PublshSerializers
復制代碼
復制代碼

通過上面的代碼,我們已經減少了大量的代碼,但是,好像我們還在復用一些代碼,queryset和serializer_class 這兩個參數。所以我們繼續封裝。

第三步的思路:將接收pk的url和不接受pk的url合為一個視圖,會有一個問題,那就是,get請求,我們不管查看所有數據還是查看某一條數據,都會走同一個視圖,怎么可以解決這個問題?問題的根源是出在分發上,我們可以重寫。

  我們將視圖合二為一,就需要將繼承的所有的類進一步封裝。

復制代碼
class ModelViewSet(mixins.CreateModelMixin, mixins.RetrieveModelMixin, mixins.UpdateModelMixin, mixins.DestroyModelMixin, mixins.ListModelMixin, GenericViewSet): """ A viewset that provides default `create()`, `retrieve()`, `update()`, `partial_update()`, `destroy()` and `list()` actions. """ pass
復制代碼

導入:

from rest_framework.viewsets import ModelViewSet

 

三部曲 · viewsets.ModelViewSet

urls.py:

復制代碼
復制代碼
    url(r'^books/$', views.BookViewSet.as_view({"get":"list","post":"create"}),name="book_list"), url(r'^books/(?P<pk>\d+)$', views.BookViewSet.as_view({ 'get': 'retrieve', 'put': 'update', 'patch': 'partial_update', 'delete': 'destroy' }),name="book_detail"),
復制代碼
復制代碼

views.py:

class BookViewSet(viewsets.ModelViewSet): queryset = Book.objects.all() serializer_class = BookSerializers

使用這種方式,url就必須在as_view()方法里,傳一個字典參數。而之前是不需要傳的,顯然,重寫了as_view()方法,那我們就去找這個重寫as_view的類。ModelViewSet中前五個是我們第一步封裝的,唯有最后一個GenericViewSet,是重寫的

復制代碼
class GenericViewSet(ViewSetMixin, generics.GenericAPIView): """ The GenericViewSet class does not provide any actions by default, but does include the base set of generic view behavior, such as the `get_object` and `get_queryset` methods. """ pass
復制代碼

這個類中通過混合類的方式進行繼承,后面的是我們之前用過的,唯有前面的ViewSetMixin是擴展的,很顯然,是這個類重寫了as_view()

復制代碼
class ViewSetMixin(object): """ This is the magic. Overrides `.as_view()` so that it takes an `actions` keyword that performs the binding of HTTP methods to actions on the Resource. For example, to create a concrete view binding the 'GET' and 'POST' methods to the 'list' and 'create' actions... view = MyViewSet.as_view({'get': 'list', 'post': 'create'}) """ @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. """   # 此處省略部分源碼... def view(request, *args, **kwargs): self = cls(**initkwargs) # We also store the mapping of request methods to actions, # so that we can later set the action attribute. # eg. `self.action = 'list'` on an incoming GET request. self.action_map = actions # Bind methods to actions # This is the bit that's different to a standard view for method, action in actions.items(): handler = getattr(self, action) setattr(self, method, handler) if hasattr(self, 'get') and not hasattr(self, 'head'): self.head = self.get self.request = request self.args = args self.kwargs = kwargs # And continue as usual return self.dispatch(request, *args, **kwargs) # take name and docstring from class update_wrapper(view, cls, updated=()) # and possible attributes set by decorators # like csrf_exempt from dispatch update_wrapper(view, cls.dispatch, assigned=()) # We need to set these on the view function, so that breadcrumb # generation can pick out these bits of information from a # resolved URL. view.cls = cls view.initkwargs = initkwargs view.suffix = initkwargs.get('suffix', None) view.actions = actions return csrf_exempt(view)
復制代碼

  一樣的,這個as_view()最終返回一個view函數,跟之前是一樣的,那么接收的這個參數有什么用呢?肯定在view函數中,調用了這個參數,並實現了某些東西。(actions接收了傳的字典參數)

    for method, action in actions.items(): handler = getattr(self, action) setattr(self, method, handler)

這三行代碼是進行了相應的處理,首先,循環這個參數,通過反射得到value對應的函數名(list、create、retrieve、update、destory),最后通過setattr實現了我們調用哪個key(method)就會執行對應的value,從而完美的解決了分發的問題。

  通過這三次分發,實現了代碼的復用。

 

總結

   通過三層封裝,使得我們可以非常快速的通過幾行代碼實現一個模型類的增刪改查,大大的提高了開發效率。有利就有弊,封裝的越完善,就意味着不夠靈活。下個系列中,帶來restframework中的非常有用的三個套件

 


免責聲明!

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



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