一、路由組件的使用
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
該路由器包括標准集合list, create, retrieve, update, partial_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
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
三、源碼
按照使用自動生成路由的規則一步步的探索源碼流程,以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()
此時,就會在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 )
基於:
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方法處理的是所有的@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={} ), ]
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
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
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, )
(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)
參考:https://q1mi.github.io/Django-REST-framework-documentation/api-guide/routers_zh/#routers
