zoukankan      html  css  js  c++  java
  • Django REST framework使用ViewSets的自定义路由实现过程

    在Django中使用基于类的视图(ClassView),类中所定义的方法名称与Http的请求方法相对应,才能基于路由将请求分发(dispatch)到ClassView中的方法进行处理,而Django REST framework中可以突破这一点,通过ViewSets可以实现自定义路由。

    创建一个ViewSets

    为get_stocks方法添加list_route装饰器,url_path参数是暴露在外的接口名称

    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的定义:

    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属性值的不同

    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列表中

    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方法

    从get_routes中可以看出些眉目了,遍历ViewSet中定义的方法,获取到方法的bind_to_method和detail属性(list_route、detail_route的功劳),根据detial属性将它们分别保存到detail_routes和list_routes列表中,保存的是httpmethod与methodname的元祖对象

        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对象

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

    [
    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方法中

        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方法进行处理了。

    整个过程大致就是这样了。

  • 相关阅读:
    ajax收藏
    excel提取文本格式时分秒中数字的方法并计算成秒的公式
    vi编辑模式中按方向键变ABCD的解决方法
    IIS配置Url重写实现http自动跳转https的重定向方法
    IIS中启用目录浏览功能后不能下载未知扩展名文件的解决方法
    Nginx禁止IP访问,只允许域名访问
    nginx在Window平台http自动跳转https设置方法
    通过清理注册表方式清理window远程连接的历史记录
    DOS批处理添加IP域名,备份与恢复
    windows修改snmp端口号方法
  • 原文地址:https://www.cnblogs.com/liubiao/p/6567565.html
Copyright © 2011-2022 走看看