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


免責聲明!

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



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