前言
之前的一篇博客说到了rest_framework对request method的绑定有两种,一种是通过APIView,另外一种是ViewSet。ViewSet生成绑定有两种方式,自动(router.register)和手动(MyViewSet.as_view),今天就来说说router是怎么自动绑定的。
前一篇文章地址:django rest_framework如何实现Request Method绑定方法
运行环境
- djangorestframework 3.11.0
- python 3.7.5
- django 2.2.7
Router自动绑定
这是之前urls.py中的代码:
from views import MyUserViewSet
from django.conf.urls import url
from rest_framework import routers
router = routers.DefaultRouter()
router.register(r'users', MyUserViewSet)
urlpatterns = [
url(r'^', include(router.urls)),
]
DefaultRouter继承自SimpleRouter,SimpleRouter继承自BaseRouter。
先来看BaseRouter这个类,代码比较短,所以全部贴出来了。
class BaseRouter:
def __init__(self):
self.registry = []
def register(self, prefix, viewset, basename=None):
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
def get_default_basename(self, viewset):
"""
If `basename` is not specified, attempt to automatically determine
it from the viewset.
"""
raise NotImplementedError('get_default_basename must be overridden')
def get_urls(self):
"""
Return a list of URL patterns, given the registered viewsets.
"""
raise NotImplementedError('get_urls must be overridden')
@property
def urls(self):
if not hasattr(self, '_urls'):
self._urls = self.get_urls()
return self._urls
BaseRouter有个属性registry,
class BaseRouter:
def __init__(self):
self.registry = []
def register(self, prefix, viewset, basename=None):
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
函数register就是用来初始化属性registry的,结合urls.py中的代码,我们来看下函数register参数是哪些。
router = routers.DefaultRouter()
# def register(prefix, viewset, basename=None)
router.register(r'users', MyUserViewSet)
这里可以不用传basename进去,如果不传,是可以根据queryset指定的数据集自动获取。
if basename is None:
basename = self.get_default_basename(viewset)
get_default_basename这个函数,是在BaseRouter的子类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()
上面说了这么多,其实也才说到初始化,也就是下面两句代码
router = routers.DefaultRouter()
router.register(r'users', MyUserViewSet)
真正生成url规则的,是这句
urlpatterns = [
url(r'^', include(router.urls)),
]
在BaseRouter类中,urls是一个属性,返回的就是实际生成的url规则,生成url规则同时实现了request method方法的绑定。
如果感觉url规则比较抽象,这里贴出一分部规则,相信就很好理解了。生成的就是下面的urlpatterns,是不是很熟悉呢?。
urlpatterns = [
url(r'^index/', views.index),
url(r'delete_obj/', views.delete),
url(r'get_obj/', views.get_obj),
]
BaseRouter是没有实现get_urls这个方法的,而是交给它的子类DefaultRouter和SimpleRouter去实现。
class BaseRouter:
# 其他代码省略
......
def get_urls(self):
"""
Return a list of URL patterns, given the registered viewsets.
"""
raise NotImplementedError('get_urls must be overridden')
@property
def urls(self):
if not hasattr(self, '_urls'):
self._urls = self.get_urls()
return self._urls
DefaultRouter先调用的是父类SimpleRouter的实现,绑定了方法、生成了基本规则,在这个结果的基础上再加上自己的一些处理,得到了最终结果。关键就在SimpleRouter中的get_urls。
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.
"""
# 省略部分代码
......
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().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
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
在上面的代码中,标明了几个要点,会在下面详细讲解。
要点一
# 要点一
lookup = self.get_lookup_regex(viewset)
根据你的viewset的一些属性,得到一个基本的查找规则, 如果这些属性你没改,
# 默认得到
lookup = '(?P<pk>[^/.]+)'
这是url中的正则表达式
href = 'http://wwww.xxxx.com/users/2/'
pattern = url(r'users/(?P<pk>[^/.]+)/$', views.get_obj)
# 这个pk就会等于2,然后传给get_obj函数
def get_obj(request, pk):
......
还是不太清楚的话,再去复习下咯
要点二
routes = self.get_routes(viewset)
SimpleRouter的属性routes指定了几个默认的路由规则,每个规则都包含url匹配模式、request method绑定哪个方法等内容,下面是routes属性。
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是一个List,长度为4,这4个元素由两种Route构成:Route和DynamicRoute。都是自定义类型,如下:
from collections import namedtuple
Route = namedtuple('Route', ['url', 'mapping', 'name', 'detail', 'initkwargs'])
DynamicRoute = namedtuple('DynamicRoute', ['url', 'name', 'detail', 'initkwargs'])
回到要点二那行代码,self.get_routes做了什么呢,如下:
-
取出这些路由规则中的绑定方法(每个route的mapping的value)放到一个List中,比如GET请求绑定list,POST绑定create,mapping = {‘get’: ‘list’, ‘post’: ‘create’},那么这个list=[‘list’, ‘create’]。
-
遍历你的ViewSet,取出你用@action装饰器额外指定的方法,放到一个List中,如:extra_list = [‘get_obj’, ‘create’]。
你可以指定你自定的方法绑定哪个method方法,默认@action(methods=[‘get’])绑定get请求
from rest_framework.viewsets import ModelViewSet
class YourViewSet(ModelViewSet):
@action
def get_obj(self, xxxxxx):
......
# 不行,会加入到extra_list中
@action
def create(self, xxxx):
......
# 可以,属于方法重写,不会加入到extra_list中
def create(self, xxxxxx):
......
- 如果这些被@action装饰的方法名在第1步取出的list中,会抛出异常。
msg = ('Cannot use the @action decorator on the following '
'methods, as they are existing routes: %s')
raise ImproperlyConfigured(msg % ', '.join(not_allowed))
- 定义一个新的List比如叫new_routes,遍历self.routes也就是SimpleRouter中routes定义的一些默认的规则。如果某个route是DynamicRoute类型的,根据一定规则转成普通Route,再放入new_routes,否则就直接放入new_routes。
- 最后,返回第4步的new_routes
要点三
# 记为要点二
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)
遍历要点二中返回的List集合(符合条件routes集合),看route中的mapping映射的方法在你的ViewSet有多少实现了。比如下面的例子,你只实现了list,那么经过验证,这个mapping最后等于mapping={‘get’: ‘list’}
mapping={'get': 'list', 'post': 'create'}
class YourViewSet(xxxx):
def list(self, xxxxx):
......
要点四
# 省略部分代码
......
for prefix, viewset, basename in self.registry:
# 省略部分代码
......
for route in routes:
# 记为要点三
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
)
#省略部分代码
......
经过要点二,routes的url就类似这样:
# 这是由普通Route转化来的,由DynamicRoute转化来,url会稍微不一样
routes = [
Route(
url=r'^{prefix}{trailing_slash}$',
......
),
Route(
url=r'^{prefix}/{lookup}{trailing_slash}$',
......
),
]
- prefix是初始化指定的
router = routers.DefaultRouter()
# def register(prefix, viewset, basename=None)
router.register(r'users', MyUserViewSet)
- lookup是要点一得到的,也就是根据你的ViewSet的一些属性得到的,源码如下:
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
)
- trailing_slash默认为’/’,我们可以在初始化时修改这个默认值
所以,这个regex最后类似下面的格式,是不是很熟悉了?
regex = r'^index/'
or
regex = r'^index/(?<pk>[^/.]+)/$'
要点五
def get_urls(self):
"""
Use the registered viewsets to generate a list of URL patterns.
"""
ret = []
for prefix, viewset, basename in self.registry:
# 省略部分代码
......
for route in routes:
# 省略部分代码
......
# 记为要点五
view = viewset.as_view(mapping, **initkwargs)
name = route.name.format(basename=basename)
# 记为要点六
ret.append(url(regex, view, name=name))
return ret
viewset.as_view,因为你的viewset最终继承自ViewSetMixin(之前文章提到过),它有一个as_view方法,实现了方法绑定,至此方法绑定到此结束。
这个mapping应该没有忘记吧,要点三中提到过,是实际你实现的方法和request method的映射。
要点六
这就更好理解了,之前说的绑定之后会生成一个url规则,ret就是这个规则,也就是下面urlpatterns这种
urlpatterns = [
url(r'^index/', views.index),
url(r'delete_obj/', views.delete),
url(r'get_obj/', views.get_obj),
url(r'myviewset/', MyViewSet.as_view(xxxxxx)),
]
至此,方法绑定,以及生成路由规则结束,这就是Route自动生成,源码也相当好理解,建议自己走一遍,加深印象。