• rest framework之路由组件


    一、路由组件的使用

    1、使用实例

    在视图中继承GenericViewSet类来完成功能时,需要自己对路由的写法有所改变,需要在as_view中传入actions字典参数:

       re_path('books/$', views.BookView.as_view({'get': 'list','post':'create'}), name="books"),

    但是rest framework中的路由组件完全可以自动生成对应的路由这样的路由。

    from rest_framework import routers
    
    
    router=routers.DefaultRouter()
    router.register('books',views.BookView)
    
    urlpatterns = [
    
    ...
    
        re_path('',include(router.urls)),
    
    ...
    
    ]

    这样就会生成下面的url形式:

    URL pattern: ^books/$ Name: 'books-list'
    URL pattern: ^books/{pk}/$ Name: 'books-detail'

    2、参数

    • register() 方法有两个强制参数:

      (1)prefix用于路由url前缀

      (2)viewset处理请求的viewset类

    3、额外连接和操作

    @detail_route@list_route装饰的视图集上的任何方法也将被路由,比如在BookView中又自定义了一个方法,那么可以加上装饰器生成对应的路由:

    class BookView(GenericViewSet):
    
        queryset = models.Book.objects.all()
        serializer_class = BookModelSerializer
    
        def list(self,request):
             pass   
    
        @detail_route(methods=['get'],url_path='set-book')
        def set_bookname(self, request, pk=None):
            return HttpResponse('...')

    此时会多生成这样一条路由规则:

    ^books/(?P<pk>[^/.]+)/set-book/$ [name='book-set-book']

    二、内置API

    1、SimpleRouter

    该路由器包括标准集合listcreateretrieveupdatepartial_update 和 destroy动作的路由。视图集中还可以使用@ detail_route@ list_route装饰器标记要被路由的其他方法。

    class SimpleRouter(BaseRouter):
    
        routes = [
            # List route.
            Route(
                url=r'^{prefix}{trailing_slash}$',
                mapping={
                    'get': 'list',
                    'post': 'create'
                },
                name='{basename}-list',
                detail=False,
                initkwargs={'suffix': 'List'}
            ),
            # Dynamically generated list routes. Generated using
            # @action(detail=False) decorator on methods of the viewset.
            DynamicRoute(
                url=r'^{prefix}/{url_path}{trailing_slash}$',
                name='{basename}-{url_name}',
                detail=False,
                initkwargs={}
            ),
            # Detail route.
            Route(
                url=r'^{prefix}/{lookup}{trailing_slash}$',
                mapping={
                    'get': 'retrieve',
                    'put': 'update',
                    'patch': 'partial_update',
                    'delete': 'destroy'
                },
                name='{basename}-detail',
                detail=True,
                initkwargs={'suffix': 'Instance'}
            ),
            # Dynamically generated detail routes. Generated using
            # @action(detail=True) decorator on methods of the viewset.
            DynamicRoute(
                url=r'^{prefix}/{lookup}/{url_path}{trailing_slash}$',
                name='{basename}-{url_name}',
                detail=True,
                initkwargs={}
            ),
        ]
    
        def __init__(self, trailing_slash=True):
            self.trailing_slash = '/' if trailing_slash else ''
            super(SimpleRouter, self).__init__()
    
        def get_default_basename(self, viewset):
            """
            If `basename` is not specified, attempt to automatically determine
            it from the viewset.
            """
            queryset = getattr(viewset, 'queryset', None)
    
            assert queryset is not None, '`basename` argument not specified, and could ' 
                'not automatically determine the name from the viewset, as ' 
                'it does not have a `.queryset` attribute.'
    
            return queryset.model._meta.object_name.lower()
    
        def get_routes(self, viewset):
            """
            Augment `self.routes` with any dynamically generated routes.
    
            Returns a list of the Route namedtuple.
            """
            # converting to list as iterables are good for one pass, known host needs to be checked again and again for
            # different functions.
            known_actions = list(flatten([route.mapping.values() for route in self.routes if isinstance(route, Route)]))
            extra_actions = viewset.get_extra_actions()
    
            # checking action names against the known actions list
            not_allowed = [
                action.__name__ for action in extra_actions
                if action.__name__ in known_actions
            ]
            if not_allowed:
                msg = ('Cannot use the @action decorator on the following '
                       'methods, as they are existing routes: %s')
                raise ImproperlyConfigured(msg % ', '.join(not_allowed))
    
            # partition detail and list actions
            detail_actions = [action for action in extra_actions if action.detail]
            list_actions = [action for action in extra_actions if not action.detail]
    
            routes = []
            for route in self.routes:
                if isinstance(route, DynamicRoute) and route.detail:
                    routes += [self._get_dynamic_route(route, action) for action in detail_actions]
                elif isinstance(route, DynamicRoute) and not route.detail:
                    routes += [self._get_dynamic_route(route, action) for action in list_actions]
                else:
                    routes.append(route)
    
            return routes
    
        def _get_dynamic_route(self, route, action):
            initkwargs = route.initkwargs.copy()
            initkwargs.update(action.kwargs)
    
            url_path = escape_curly_brackets(action.url_path)
    
            return Route(
                url=route.url.replace('{url_path}', url_path),
                mapping=action.mapping,
                name=route.name.replace('{url_name}', action.url_name),
                detail=route.detail,
                initkwargs=initkwargs,
            )
    
        def get_method_map(self, viewset, method_map):
            """
            Given a viewset, and a mapping of http methods to actions,
            return a new mapping which only includes any mappings that
            are actually implemented by the viewset.
            """
            bound_methods = {}
            for method, action in method_map.items():
                if hasattr(viewset, action):
                    bound_methods[method] = action
            return bound_methods
    
        def get_lookup_regex(self, viewset, lookup_prefix=''):
            """
            Given a viewset, return the portion of URL regex that is used
            to match against a single instance.
    
            Note that lookup_prefix is not used directly inside REST rest_framework
            itself, but is required in order to nicely support nested router
            implementations, such as drf-nested-routers.
    
            https://github.com/alanjds/drf-nested-routers
            """
            base_regex = '(?P<{lookup_prefix}{lookup_url_kwarg}>{lookup_value})'
            # Use `pk` as default field, unset set.  Default regex should not
            # consume `.json` style suffixes and should break at '/' boundaries.
            lookup_field = getattr(viewset, 'lookup_field', 'pk')
            lookup_url_kwarg = getattr(viewset, 'lookup_url_kwarg', None) or lookup_field
            lookup_value = getattr(viewset, 'lookup_value_regex', '[^/.]+')
            return base_regex.format(
                lookup_prefix=lookup_prefix,
                lookup_url_kwarg=lookup_url_kwarg,
                lookup_value=lookup_value
            )
    
        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:]
    
                    initkwargs = route.initkwargs.copy()
                    initkwargs.update({
                        'basename': basename,
                        'detail': route.detail,
                    })
    
                    view = viewset.as_view(mapping, **initkwargs)
                    name = route.name.format(basename=basename)
                    ret.append(url(regex, view, name=name))
    
            return ret
    SimpleRouter

     2、DefaultRouter

    这个路由器类似于上面的SimpleRouter,但是还包括一个默认返回所有列表视图的超链接的API根视图。它还生成可选的.json样式格式后缀的路由。

    class DefaultRouter(SimpleRouter):
        """
        The default router extends the SimpleRouter, but also adds in a default
        API root view, and adds format suffix patterns to the URLs.
        """
        include_root_view = True
        include_format_suffixes = True
        root_view_name = 'api-root'
        default_schema_renderers = None
        APIRootView = APIRootView
        APISchemaView = SchemaView
        SchemaGenerator = SchemaGenerator
    
        def __init__(self, *args, **kwargs):
            if 'root_renderers' in kwargs:
                self.root_renderers = kwargs.pop('root_renderers')
            else:
                self.root_renderers = list(api_settings.DEFAULT_RENDERER_CLASSES)
            super(DefaultRouter, self).__init__(*args, **kwargs)
    
        def get_api_root_view(self, api_urls=None):
            """
            Return a basic root view.
            """
            api_root_dict = OrderedDict()
            list_name = self.routes[0].name
            for prefix, viewset, basename in self.registry:
                api_root_dict[prefix] = list_name.format(basename=basename)
    
            return self.APIRootView.as_view(api_root_dict=api_root_dict)
    
        def get_urls(self):
            """
            Generate the list of URL patterns, including a default root view
            for the API, and appending `.json` style format suffixes.
            """
            urls = super(DefaultRouter, self).get_urls()
    
            if self.include_root_view:
                view = self.get_api_root_view(api_urls=urls)
                root_url = url(r'^$', view, name=self.root_view_name)
                urls.append(root_url)
    
            if self.include_format_suffixes:
                urls = format_suffix_patterns(urls)
    
            return urls
    DefaultRouter

     三、源码

    按照使用自动生成路由的规则一步步的探索源码流程,以SimpleRouter为例:

    • 生成SimpleRouter实例router
    from rest_framework.routers import SimpleRouter
    
    router=SimpleRouter()

    显然会执行SimpleRouter类的__init__方法:

        def __init__(self, trailing_slash=True):
            self.trailing_slash = '/' if trailing_slash else ''
            super(SimpleRouter, self).__init__()

    SimpleRouter创建的URL将附加尾部斜杠。 在实例化路由器时,可以通过将trailing_slash参数设置为`False'来修改此行为:

    router = SimpleRouter(trailing_slash=False)

    其次,执行父类BaseRouter的__init__方法:

    class BaseRouter(six.with_metaclass(RenameRouterMethods)):
        def __init__(self):
            self.registry = []
        ...

    所以,在实例化SimpleRouter后会判断生成的url尾部是否加‘/’以及生成registry字典。

    • 执行register方法
    router.register('books',views.BookView)

    在SimpleRouter类中没有register方法,所以会执行父类BaseRouter中的register方法:

    class BaseRouter(six.with_metaclass(RenameRouterMethods)):
    
        ...
    
        def register(self, prefix, viewset, basename=None, base_name=None):
            if base_name is not None:
                msg = "The `base_name` argument is pending deprecation in favor of `basename`."
                warnings.warn(msg, RemovedInDRF311Warning, 2)
    
            assert not (basename and base_name), (
                "Do not provide both the `basename` and `base_name` arguments.")
    
            if basename is None:
                basename = base_name
    
            if basename is None:
                basename = self.get_default_basename(viewset)
            self.registry.append((prefix, viewset, basename))
    
            # invalidate the urls cache
            if hasattr(self, '_urls'):
                del self._urls
            ....

      将传入的url前缀、视图viewset和base_name以元祖的形式 添加到实例化生成的字典registry中,值得注意的是如果base_name没有传值得话就会生成默认的basename(模型表的小写),获取默认的basename方法是在SimpleRouter类中:

        def get_default_basename(self, viewset):
            """
            If `basename` is not specified, attempt to automatically determine
            it from the viewset.
            """
            queryset = getattr(viewset, 'queryset', None)
    
            assert queryset is not None, '`basename` argument not specified, and could ' 
                'not automatically determine the name from the viewset, as ' 
                'it does not have a `.queryset` attribute.'
    
            return queryset.model._meta.object_name.lower()
    get_default_basename

    此时,就会在registry字典中生成这样的数据:

    registry = {
        ('books','BookView','book'),
    }
    • 生成url
    urlpatterns = router.urls

    接下来就是执行SimpleRouter类的urls属性,显然它没有这个属性,就会会执行父类BaseRouter中的urls属性:

    class BaseRouter(six.with_metaclass(RenameRouterMethods)):
    
        ...
    
             @property
            def urls(self):
                if not hasattr(self, '_urls'):
                    self._urls = self.get_urls()
                return self._urls
      ...

    紧接着执行get_urls方法,会先去SimpleRouter中执行这个方法:

    class SimpleRouter(BaseRouter):
        ...
        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:]
    
                    initkwargs = route.initkwargs.copy()
                    initkwargs.update({
                        'basename': basename,
                        'detail': route.detail,
                    })
    
                    view = viewset.as_view(mapping, **initkwargs)
                    name = route.name.format(basename=basename)
                    ret.append(url(regex, view, name=name))
    
            return ret
        ...

    在这个方法中会循环得到的registry字典:

    (1)生成lookup构建url的正则表达式

        def get_lookup_regex(self, viewset, lookup_prefix=''):
            """
            Given a viewset, return the portion of URL regex that is used
            to match against a single instance.
    
            Note that lookup_prefix is not used directly inside REST rest_framework
            itself, but is required in order to nicely support nested router
            implementations, such as drf-nested-routers.
    
            https://github.com/alanjds/drf-nested-routers
            """
            base_regex = '(?P<{lookup_prefix}{lookup_url_kwarg}>{lookup_value})'
            # Use `pk` as default field, unset set.  Default regex should not
            # consume `.json` style suffixes and should break at '/' boundaries.
            lookup_field = getattr(viewset, 'lookup_field', 'pk')
            lookup_url_kwarg = getattr(viewset, 'lookup_url_kwarg', None) or lookup_field
            lookup_value = getattr(viewset, 'lookup_value_regex', '[^/.]+')
            return base_regex.format(
                lookup_prefix=lookup_prefix,
                lookup_url_kwarg=lookup_url_kwarg,
                lookup_value=lookup_value
            )
    get_lookup_regex

    基于:

    base_regex = '(?P<{lookup_prefix}{lookup_url_kwarg}>{lookup_value})'

    lookup_prefix默认为空,lookup_url_kwargs为正则分组的关键字默认为lookup_field(lookup_field默认为pk),lookup_value_regex默认为[^/.]+

    所以最后生成:

    base_regex = '(?P<pk>[^/.]+)'

    (2)生成routes

    生成所有的路由

     def get_routes(self, viewset):
            """
            Augment `self.routes` with any dynamically generated routes.
    
            Returns a list of the Route namedtuple.
            """
            # converting to list as iterables are good for one pass, known host needs to be checked again and again for
            # different functions.
            known_actions = list(flatten([route.mapping.values() for route in self.routes if isinstance(route, Route)]))
            extra_actions = viewset.get_extra_actions()
    
            # checking action names against the known actions list
            not_allowed = [
                action.__name__ for action in extra_actions
                if action.__name__ in known_actions
            ]
            if not_allowed:
                msg = ('Cannot use the @action decorator on the following '
                       'methods, as they are existing routes: %s')
                raise ImproperlyConfigured(msg % ', '.join(not_allowed))
    
            # partition detail and list actions
            detail_actions = [action for action in extra_actions if action.detail]
            list_actions = [action for action in extra_actions if not action.detail]
    
            routes = []
            for route in self.routes:
                if isinstance(route, DynamicRoute) and route.detail:
                    routes += [self._get_dynamic_route(route, action) for action in detail_actions]
                elif isinstance(route, DynamicRoute) and not route.detail:
                    routes += [self._get_dynamic_route(route, action) for action in list_actions]
                else:
                    routes.append(route)
    
            return routes
    get_routes

     在get_routes方法处理的是所有的@list_route和@detail_route,返回的是所有的route。

    内部给予的url模板,router列表:

    class SimpleRouter(BaseRouter):
    
        routes = [
            # List route.
            Route(
                url=r'^{prefix}{trailing_slash}$',
                mapping={
                    'get': 'list',
                    'post': 'create'
                },
                name='{basename}-list',
                detail=False,
                initkwargs={'suffix': 'List'}
            ),
            # Dynamically generated list routes. Generated using
            # @action(detail=False) decorator on methods of the viewset.
            DynamicRoute(
                url=r'^{prefix}/{url_path}{trailing_slash}$',
                name='{basename}-{url_name}',
                detail=False,
                initkwargs={}
            ),
            # Detail route.
            Route(
                url=r'^{prefix}/{lookup}{trailing_slash}$',
                mapping={
                    'get': 'retrieve',
                    'put': 'update',
                    'patch': 'partial_update',
                    'delete': 'destroy'
                },
                name='{basename}-detail',
                detail=True,
                initkwargs={'suffix': 'Instance'}
            ),
            # Dynamically generated detail routes. Generated using
            # @action(detail=True) decorator on methods of the viewset.
            DynamicRoute(
                url=r'^{prefix}/{lookup}/{url_path}{trailing_slash}$',
                name='{basename}-{url_name}',
                detail=True,
                initkwargs={}
            ),
        ]
    routes列表

    get_routes方法中known_actions得到的是循环模板routers列表得到的所有的action列表,而get_routes方法中extra_actions得到的是经过viewset视图类中的action列表

    经过判断确认在视图类中使用的action是模板本身存在的,而不是自己随意添加的action

            # checking action names against the known actions list
            not_allowed = [
                action.__name__ for action in extra_actions
                if action.__name__ in known_actions
            ]
            if not_allowed:
                msg = ('Cannot use the @action decorator on the following '
                       'methods, as they are existing routes: %s')
                raise ImproperlyConfigured(msg % ', '.join(not_allowed))

     紧接着生成list和detail两类路由,根据detail是True还是False来进行判断的:

    def detail_route(methods=None, **kwargs):
        """
        Used to mark a method on a ViewSet that should be routed for detail requests.
        """
        warnings.warn(
            "`detail_route` is deprecated and will be removed in 3.10 in favor of "
            "`action`, which accepts a `detail` bool. Use `@action(detail=True)` instead.",
            RemovedInDRF310Warning, stacklevel=2
        )
    
        def decorator(func):
            func = action(methods, detail=True, **kwargs)(func)
            if 'url_name' not in kwargs:
                func.url_name = func.url_path.replace('_', '-')
            return func
        return decorator
    detail_route装饰器
    def list_route(methods=None, **kwargs):
        """
        Used to mark a method on a ViewSet that should be routed for list requests.
        """
        warnings.warn(
            "`list_route` is deprecated and will be removed in 3.10 in favor of "
            "`action`, which accepts a `detail` bool. Use `@action(detail=False)` instead.",
            RemovedInDRF310Warning, stacklevel=2
        )
    
        def decorator(func):
            func = action(methods, detail=False, **kwargs)(func)
            if 'url_name' not in kwargs:
                func.url_name = func.url_path.replace('_', '-')
            return func
        return decorator
    list_route装饰器
            routes = []
            for route in self.routes:
                if isinstance(route, DynamicRoute) and route.detail:
                    routes += [self._get_dynamic_route(route, action) for action in detail_actions]
                elif isinstance(route, DynamicRoute) and not route.detail:
                    routes += [self._get_dynamic_route(route, action) for action in list_actions]
                else:
                    routes.append(route)

    上面就是动态生成路由,利用的就是_get_dynamic_route方法,而这个方法返回的就是Route实例(url模板中定义好的route)

        def _get_dynamic_route(self, route, action):
            initkwargs = route.initkwargs.copy()
            initkwargs.update(action.kwargs)
    
            url_path = escape_curly_brackets(action.url_path)
    
            return Route(
                url=route.url.replace('{url_path}', url_path),
                mapping=action.mapping,
                name=route.name.replace('{url_name}', action.url_name),
                detail=route.detail,
                initkwargs=initkwargs,
            )
    _get_dynamic_route

    (3)生成re_path

     循环得到的routers列表,进行当前viewset中method与action的映射:

     mapping = self.get_method_map(viewset, route.mapping)
        {‘get’:'list','post':'create'}

    构建url模式:

                   regex = route.url.format(
                        prefix=prefix,
                        lookup=lookup,
                        trailing_slash=self.trailing_slash
                    )

    这样就会生成这样的url:

    url=r'^books/(?P<pk>[^/.]+)$',

    从源码的这里可以知道,url的构成主要是由前缀prefix以及正则base_regex

    r'^{prefix}/{base_regex}'

    然后生成对应的re_path,并将所有的re_path加入到对应的列表中

    url(regex, view, name=name)
    def url(regex, view, kwargs=None, name=None):
        return re_path(regex, view, kwargs, name)
    url

    参考:https://q1mi.github.io/Django-REST-framework-documentation/api-guide/routers_zh/#routers

  • 相关阅读:
    [币严区块链]数字货币交易所之比特币(BTC)钱包对接 | 自建节点JSON-RPC访问
    [币严区块链]以太坊(ETH)Dapp开发入门教程之宠物商店领养游戏
    一句话的设计模式
    在线捉鬼游戏开发
    华容道程序求解
    利用多态,实现一般处理程序(ashx)中的AOP(切面编程)
    客户端-服务器端互动比较与原生实例(比较ajax,server-sent event,websocket/netsocket)
    在线捉鬼游戏开发之三
    在线捉鬼游戏开发之三
    在线捉鬼游戏开发之三
  • 原文地址:https://www.cnblogs.com/shenjianping/p/11493878.html
Copyright © 2020-2023  润新知