Django REST framework使用ViewSets的自定義路由實現過程

在Django中使用基於類的視圖(ClassView),類中所定義的方法名稱與Http的請求方法相對應,才能基於路由將請求分發(dispatch)到ClassView中的方法進行處理,而Django REST framework中能夠突破這一點,經過ViewSets能夠實現自定義路由。css

建立一個ViewSets

爲get_stocks方法添加list_route裝飾器,url_path參數是暴露在外的接口名稱app

class StockViewSet(viewsets.ModelViewSet):
    queryset = AppStock.objects.all()

    @list_route(url_path='getstocklist')
    def get_stocks(self, request, *args, **kwargs):
        '''獲取股票列表'''

        return Response({'succss':True,'msg':'操做成功'})

來看一下list_route的定義:post

def list_route(methods=None, **kwargs):
    """
    Used to mark a method on a ViewSet that should be routed for list requests.
    """
    methods = ['get'] if (methods is None) else methods

    def decorator(func):
        func.bind_to_methods = methods
        func.detail = False
        func.kwargs = kwargs
        return func
    return decorator

對於接口,通常有獲取列表頁和獲取詳情兩種形式。一樣的,還有detail_route裝飾器。list_route、detail_route的做用都是爲方法添加了bind_to_methods、detail、kwargs屬性,惟一的區別是detail屬性值的不一樣ui

def detail_route(methods=None, **kwargs):
    """
    Used to mark a method on a ViewSet that should be routed for detail requests.
    """
    methods = ['get'] if (methods is None) else methods

    def decorator(func):
        func.bind_to_methods = methods
        func.detail = True
        func.kwargs = kwargs
        return func
    return decorator

註冊路由

router=DefaultRouter()
router.register(r'stock',StockViewSet)


urlpatterns = [
    url(r'',include(router.urls)),

    url(r'^admin/', admin.site.urls),
]

自定義路由實現過程

DefaultRouter是BaseRouter的子類,register方法內部將其註冊的prefix與之對應的viewset保存在registry列表中url

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

其urls屬性是一個描述符,內部調用了get_urls方法spa

從get_routes中能夠看出些眉目了,遍歷ViewSet中定義的方法,獲取到方法的bind_to_method和detail屬性(list_route、detail_route的功勞),根據detial屬性將它們分別保存到detail_routes和list_routes列表中,保存的是httpmethod與methodname的元祖對象code

def get_routes(self, viewset):
        """
        省略若干...
        """
        # Determine any `@detail_route` or `@list_route` decorated methods on the viewset
        detail_routes = []
        list_routes = []
        for methodname in dir(viewset):
            attr = getattr(viewset, methodname)
            httpmethods = getattr(attr, 'bind_to_methods', None)
            detail = getattr(attr, 'detail', True)
                httpmethods = [method.lower() for method in httpmethods]
                if detail:
                    detail_routes.append((httpmethods, methodname))
                else:
                    list_routes.append((httpmethods, methodname))

        def _get_dynamic_routes(route, dynamic_routes):
            ret = []
            for httpmethods, methodname in dynamic_routes:
                method_kwargs = getattr(viewset, methodname).kwargs
                initkwargs = route.initkwargs.copy()
                initkwargs.update(method_kwargs)
                url_path = initkwargs.pop("url_path", None) or methodname
                url_name = initkwargs.pop("url_name", None) or url_path
                ret.append(Route(
                    url=replace_methodname(route.url, url_path),
                    mapping={httpmethod: methodname for httpmethod in httpmethods},
                    name=replace_methodname(route.name, url_name),
                    initkwargs=initkwargs,
                ))

            return ret

        ret = []
        for route in self.routes:
            if isinstance(route, DynamicDetailRoute):
                # Dynamic detail routes (@detail_route decorator)
                ret += _get_dynamic_routes(route, detail_routes)
            elif isinstance(route, DynamicListRoute):
                # Dynamic list routes (@list_route decorator)
                ret += _get_dynamic_routes(route, list_routes)
            else:
                # Standard route
                ret.append(route)

        return ret

接着,遍歷routes列表,看到這個代碼,我也是看了挺久纔看懂這用意,routes列表包含固定的四個Route對象orm

routes = [
        # List route.
        Route(
            url=r'^{prefix}{trailing_slash}$',
            mapping={
                'get': 'list',
                'post': 'create'
            },
            name='{basename}-list',
            initkwargs={'suffix': 'List'}
        ),
        # Dynamically generated list routes.
        DynamicListRoute(
            url=r'^{prefix}/{methodname}{trailing_slash}$',
            name='{basename}-{methodnamehyphen}',
            initkwargs={}
        ),
        # Detail route.
        Route(
            url=r'^{prefix}/{lookup}{trailing_slash}$',
            mapping={
                'get': 'retrieve',
                'put': 'update',
                'patch': 'partial_update',
                'delete': 'destroy'
            },
            name='{basename}-detail',
            initkwargs={'suffix': 'Instance'}
        ),
        # Dynamically generated detail routes.
        DynamicDetailRoute(
            url=r'^{prefix}/{lookup}/{methodname}{trailing_slash}$',
            name='{basename}-{methodnamehyphen}',
            initkwargs={}
        ),
    ]

其用意是經過調用_get_dynamic_routes內嵌方法,把routes列表中項做爲模板,將list_routes和detail_routes中的項依次進行替換,最終獲得一個Route對象的列表(Route是一個namedtuple,包含如url、mapping、name等項)router

[
Route(url='^{prefix}{trailing_slash}$', mapping={'get': 'list', 'post': 'create'}, name='{basename}-list', initkwargs={'suffix': 'List'}), 

Route(url='^{prefix}/getstocklist{trailing_slash}$', mapping={'get': 'get_stocks'}, name='{basename}-getstocklist', initkwargs={}),

Route(url='^{prefix}/{lookup}{trailing_slash}$', mapping={'get': 'retrieve', 'patch': 'partial_update', 'put': 'update', 'delete': 'destroy'}, name='{basename}-detail', initkwargs={'suffix': 'Instance'})
]

get_route方法的功能到此結束了,回到get_urls方法中csrf

def get_urls(self):
        """
        Use the registered viewsets to generate a list of URL patterns.
        """
        ret = []

        for prefix, viewset, basename in self.registry:
            lookup = self.get_lookup_regex(viewset)
            routes = self.get_routes(viewset)

            for route in routes:

                # Only actions which actually exist on the viewset will be bound
                mapping = self.get_method_map(viewset, route.mapping)
                if not mapping:
                    continue

                # Build the url pattern
                regex = route.url.format(
                    prefix=prefix,
                    lookup=lookup,
                    trailing_slash=self.trailing_slash
                )

                # If there is no prefix, the first part of the url is probably
                #   controlled by project's urls.py and the router is in an app,
                #   so a slash in the beginning will (A) cause Django to give
                #   warnings and (B) generate URLS that will require using '//'.
                if not prefix and regex[:2] == '^/':
                    regex = '^' + regex[2:]

                view = viewset.as_view(mapping, **route.initkwargs)
                name = route.name.format(basename=basename)
                ret.append(url(regex, view, name=name))

        return ret

這裏的核心點是viewset的as_view方法,是否是很熟悉,Django中基於類的視圖註冊路由時也是調用的ClassView的as_view方法。as_view方法是在父類ViewSetMixin中定義的,傳入的action參數是httpmethod與methodname的映射一個字典,如 {'get': 'get_stocks'}

def as_view(cls, actions=None, **initkwargs):
        """
        省略若干...
        """
       
        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)

            # And continue as usual
            return self.dispatch(request, *args, **kwargs)

        view.cls = cls
        view.initkwargs = initkwargs
        view.suffix = initkwargs.get('suffix', None)
        view.actions = actions
        return csrf_exempt(view)

核心點是這個view方法以及dispatch方法,view方法中遍歷anctions字典,經過setattr設置名稱爲httpmethod的屬性,屬性值爲methodname所對應的方法。在dispathch方法中,就可經過getattr獲取到httpmethod所對應的handler

def dispatch(self, request, *args, **kwargs):
        """
        `.dispatch()` is pretty much the same as Django's regular dispatch,
        but with extra hooks for startup, finalize, and exception handling.
        """
        self.args = args
        self.kwargs = kwargs
        request = self.initialize_request(request, *args, **kwargs)
        self.request = request
        self.headers = self.default_response_headers  # deprecate?

        try:
            self.initial(request, *args, **kwargs)

            # Get the appropriate handler method
            if request.method.lower() in self.http_method_names:
                handler = getattr(self, request.method.lower(),
                                  self.http_method_not_allowed)
            else:
                handler = self.http_method_not_allowed

            response = handler(request, *args, **kwargs)

        except Exception as exc:
            response = self.handle_exception(exc)

        self.response = self.finalize_response(request, response, *args, **kwargs)
        return self.response

get_urls方法最終返回的結果是url(regex, view, name=name)的列表,這也就是ViewSet幫咱們建立的自定義路由,其實現與咱們在urls.py註冊路由是同樣的。url方法獲得的是RegexURLPattern對象

[
 <RegexURLPattern appstock-list ^stock/$>,

 <RegexURLPattern appstock-getstocklist ^stock/getstocklist/$>, 
 
 <RegexURLPattern appstock-detail ^stock/(?P<pk>[^/.]+)/$>
]

最後

訪問 http://127.0.0.1:8000/stock/getstocklist/,請求就會交由StockViewSet中的get_stocks方法進行處理了。

整個過程大體就是這樣了。

相關文章
相關標籤/搜索