django rest_framework中的Router自动生成Request Method方法绑定

前言

之前的一篇博客说到了rest_framework对request method的绑定有两种,一种是通过APIView,另外一种是ViewSetViewSet生成绑定有两种方式,自动(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继承自SimpleRouterSimpleRouter继承自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这个方法的,而是交给它的子类DefaultRouterSimpleRouter去实现。

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构成:RouteDynamicRoute。都是自定义类型,如下:

from collections import namedtuple

Route = namedtuple('Route', ['url', 'mapping', 'name', 'detail', 'initkwargs'])
DynamicRoute = namedtuple('DynamicRoute', ['url', 'name', 'detail', 'initkwargs'])

回到要点二那行代码,self.get_routes做了什么呢,如下:

  1. 取出这些路由规则中的绑定方法(每个route的mapping的value)放到一个List中,比如GET请求绑定list,POST绑定create,mapping = {‘get’: ‘list’, ‘post’: ‘create’},那么这个list=[‘list’, ‘create’]

  2. 遍历你的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):
		......
  1. 如果这些被@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))
  1. 定义一个新的List比如叫new_routes,遍历self.routes也就是SimpleRouterroutes定义的一些默认的规则。如果某个route是DynamicRoute类型的,根据一定规则转成普通Route,再放入new_routes,否则就直接放入new_routes
  2. 最后,返回第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
		)
	#省略部分代码
	......

经过要点二routesurl就类似这样:

# 这是由普通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自动生成,源码也相当好理解,建议自己走一遍,加深印象。

  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值