Rest_framework Router 路由器
雖說django rest_framework是基於django的,url路由到視圖主要還是利用django的dispatcher路由系統(可以參考我的另一篇關於django url dispatcher詳解),但是rest_framework還在django路由的基礎上,提供了基於restful風格的更高等級的路由方式。就是http method 路由到 actions 的映射關系(一個字典)。而在rest_framework中實現這層路由方式的是rest_framework.viewsets.ViewSetMinix類實現。另一方面由於restful風格面向的資源無非單資源或者資源集。常用的actions操作create,list, retreive,update, destroy。所以對於單資源和資源集都有相對固定的操作模式和url風格模式,所以抽象出來這樣一種結合兩種路由的一條龍模式:Router 路由器,單資源url與資源集合url的pattern及其對應的http method 映射 actions,都通過Router自動生成。
Router路由器的功能就是自動生成url。
其實Router就是利用ViewSetMinix根據methods與actions的一個mapping,再按照單資源或資源集的url的通常操作action類型,相結合起來,產生出一個route 即一條路由規則的概念。
下面就結合一條route就定義了產生實際url路由和相應的對url的操作映射。
博文圖片掛了臨時解決辦法

ViewSet結合Router,自動生成url。
將ViewSet注冊到Router中,需要三個要素:
- prefix前綴或者叫資源集名。用於url中表示資源集名。類型:正則字符串
- viewset視圖類。繼承了ViewSetMinix類。類型:is-a ViewSetMinix
- basename 用於生成url的url名稱。不提供會根據queryset的model名作為其值。類型:字符串。如:users-list/users-create等等
Router.register() 接口提供注冊。
關於路由規則,細分有四類:
一條路由規則就是一個Route對象,實例Route對象的參數不同,划分了四類(DynamicRoute也算類Route類):
- 一般detail,提供的(retrieve,update,destroy,partial_update),單資源的操作路由
- 一般list (list, create) , 資源集的操作路由
- 動態detail (通過@action裝飾器), 單資源的額外操作
- 動態list (通過@aciton裝飾器)
這四類路由完全能滿足,各種大多路由需求。
四種路由規則如下:
routes = [
# List route.
Route(
url=r'^{prefix}{trailing_slash}$',
mapping={
'get': 'list',
'post': 'create'
},
name='{basename}-list',
detail=False, # 注意這里detail是false說明是list路由
initkwargs={'suffix': 'List'}
),
# Dynamically generated list routes. Generated using
# @action(detail=False) decorator on methods of the viewset.
DynamicRoute( #動態的list路由
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, #說明是detail路由
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, # 動態detail路由
initkwargs={}
),
]
路由規則中,可以修改非動態路由的mapping,從而可以自定義路由。
將VIewSet注冊到Router中后,就可通過Router.urls獲取自動生成的url列表。
具體自動生成urls原理,見下面源碼解析。
rest_framework.routers.SimpleRouter源碼解析
主要通過源碼簡單分析,印證本文上面內容的表達
SimpleRouter繼承和方法一覽

SimpleRouter類源碼
淺析請看注釋
class SimpleRouter(BaseRouter): # BaseRouter提供了一個property是urls,其大多會調用get_urls()
routes = [ # 上面提到的4條route對象
# List route.
Route(
url=r'^{prefix}{trailing_slash}$', # 集合資源路由url
mapping={ # 集合資源 符合restful風格 的操作 http methods 與 actions映射
'get': 'list',
'post': 'create'
},
name='{basename}-list', # 路由名,注意s字符串都是格式化字符串,字符串的格式化會發生在get_urls方法遍歷routes時
detail=False, # 注意這里detail是false說明是list路由
initkwargs={'suffix': 'List'}
),
# Dynamically generated list routes. Generated using
# @action(detail=False) decorator on methods of the viewset.
DynamicRoute( # 動態的list路由
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, #說明是detail路由
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, # 動態detail路由
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() # 獲取queryset的model名
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)])) # 路由器定制的路由類型所支持的action名
extra_actions = viewset.get_extra_actions() # ViewSet中通過@action裝飾器定義的額外action
# checking action names against the known actions list
not_allowed = [ # 檢查自定義的action名稱不能使用路由中定義的名稱,因為路由定義的action名已經有具體的詳情描述,不需要再用@action裝飾
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: #將用戶定義的action按照處理為普通Route,並分出detail和list類型,加入到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 #這里返回的就是一個Route對象的列表,每個Route對象代表了一條實際路由(包括url,method與action的映射,還有路由名等),提供給get_urls()生成 url
def _get_dynamic_route(self, route, action): # 作用將dynamicroute 實例化為普通route
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): # 獲取viewset支持的action映射,過濾作用。
"""
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
# 關鍵:遍歷路由,處理每條路由中的方法,是否viewset中定義,只有viewset中定義了才會放入新的mapping中。依據新mapping是否有映射,來處理這條路由是否產生新的url並加入到實際路由中去。
mapping = self.get_method_map(viewset, route.mapping)
if not mapping:
continue
# Build the url pattern
regex = route.url.format( # 生成url正則表達式,這里就是前面提到的格式化字符串。
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) #這里就是利用ViewSetMinix的as_view做視圖路由了。
name = route.name.format(basename=basename) # 將格式化字符串進行格式化,填充內容。如:'{basename}-detail'.format(basename=basename)
ret.append(url(regex, view, name=name))
return ret
總結
- SimpleRouter中定義的路由已經比較齊全,但是有時候我們viewset中雖然定義了action,但是再路由生成中不想使用,那么就要可以繼承SimpleRouter,修改他的Route對象中的mapping,將不想使用的action映射去掉即可。
- 使用SimpleRouter對於常用的action名是約定俗成的,所以要遵照這些著名的action名,定義符合的操作資源邏輯。
- 通過源碼的解析,我們就懂得了怎么利用Router路由器類來定制化和簡化我們的一些經常要做的工作,也提供了可自定義的接口給我們。
- 認識Router就要清晰認識 4中路由類型 和 其設計原理模式。將每條url抽象為一個Route對象,將自定義的抽象為動態Route對象(最終還是會根據@action定義的內容,將動態Route轉換為Route對象),最后根據注冊到路由器的路由規則,生成url。
- 知道prefix, viewset, basename, @action的作用。
- http method 映射到 actions 都是利用了ViewSetMinix.as_view()方法。
- 如果不使用Router類,只使用ViewSetMinix完全可以完成http method 映射 actions,只不過url要手動去創建。
- 官檔: https://www.django-rest-framework.org/api-guide/routers/#routers
