url路由
该过程核心的功能是解析用户请求中指定的url,然后匹配找到对应的view函数来处理生成响应。一个简单的Django应用其实可以由一个或多个view函数,以及一个将这些view函数映射到url的配置文件就可以组成。下面我们通过分析Django源码来看下是如何实现url的解析的。
代码分析
创建的项目默认会带有setting文件,它会有ROOT_URLCONF
,而ROOT_URLCONF
会指向一个默认的url配置文件。
from django.contrib import admin
from django.urls import path
from . import views
urlpatterns = [
path('admin/', admin.site.urls),
path('blog/', views.page),
]
上面的urlconf文件设置了两条urlpattern,分别是把blog/
和views.page
绑定,以及把admin/
和admin.site.urls
绑定。这里的views.page
其实就是一个正常的view函数;但是admin.site.urls
并不是一个view函数,通过分析django/contrib/admin/sites.py
代码可以发现,它对应的是一个urlpatterns
。所有可以看到,urlpattern可以把一个url和view函数绑定,也可以把url和一个urlpattern进行二级或者多级的绑定。如何做到的?我们继续看path
对应的源码,应该就能找到原因了。
# django/urls/conf.py
def _path(route, view, kwargs=None, name=None, Pattern=None):
# 这里做了分支处理,如果是一个list或者tuple,就用URLResolver处理
if isinstance(view, (list, tuple)):
# For include(...) processing.
pattern = Pattern(route, is_endpoint=False)
urlconf_module, app_name, namespace = view
return URLResolver(
pattern,
urlconf_module,
kwargs,
app_name=app_name,
namespace=namespace,
)
# 如果是一个正常的可调用的view函数,则用URLPattern处理
elif callable(view):
pattern = Pattern(route, name=name, is_endpoint=True)
return URLPattern(pattern, view, kwargs, name)
else:
raise TypeError('view must be a callable or a list/tuple in the case of include().')
path = partial(_path, Pattern=RoutePattern)
果然通过分析path
就能发现个一二了。当是一个正常的可调用的view函数的时候,就会当成URLPattern处理;但是如果是一个list或者tuple,则会当成URLResolver继续处理。那么继续看下URLPattern和URLResolver,基本就能明白整个原理。
# django/urls/resolvers.py
class URLPattern:
def __init__(self, pattern, callback, default_args=None, name=None):
# 需要匹配的urlpattern
self.pattern = pattern
# 对应的回调函数
self.callback = callback # the view
# 参数等
self.default_args = default_args or {}
self.name = name
def __repr__(self):
return '<%s %s>' % (self.__class__.__name__, self.pattern.describe())
...
def resolve(self, path):
match = self.pattern.match(path)
if match:
# 如果匹配成功,则返回一个ResolverMatch
new_path, args, kwargs = match
# Pass any extra_kwargs as **kwargs.
kwargs.update(self.default_args)
return ResolverMatch(self.callback, args, kwargs, self.pattern.name)
URLPattern包括根据正则表达式字符串生成的pattern对象,一个可调用的对象,以及一些参数。所以很明显,对应的就是url配置文件里面配置了可调用view函数或者对象的那些记录。这里面当匹配成功后,会返回ResolverMatch
,定义如下:
# django/urls/resolvers.py
class ResolverMatch:
def __init__(self, func, args, kwargs, url_name=None, app_names=None, namespaces=None):
self.func = func
self.args = args
self.kwargs = kwargs
self.url_name = url_name
# If a URLRegexResolver doesn't have a namespace or app_name, it passes
# in an empty value.
self.app_names = [x for x in app_names if x] if app_names else []
self.app_name = ':'.join(self.app_names)
self.namespaces = [x for x in namespaces if x] if namespaces else []
self.namespace = ':'.join(self.namespaces)
if not hasattr(func, '__name__'):
# A class-based view
self._func_path = func.__class__.__module__ + '.' + func.__class__.__name__
else:
# A function-based view
self._func_path = func.__module__ + '.' + func.__name__
view_path = url_name or self._func_path
self.view_name = ':'.join(self.namespaces + [view_path])
# 可以获取回调函数,或者参数
def __getitem__(self, index):
return (self.func, self.args, self.kwargs)[index]
def __repr__(self):
return "ResolverMatch(func=%s, args=%s, kwargs=%s, url_name=%s, app_names=%s, namespaces=%s)" % (
self._func_path, self.args, self.kwargs, self.url_name,
self.app_names, self.namespaces,
)
ResolverMatch就是匹配成功后返回的结果。它包含了可调用对象。通常是视图函数;参数等信息。以及因为实现了__getitem__
方法,所以可以很方便的回调函数参数等信息。 最后还剩下URLResolver
。
# django/urls/resolvers.py
class URLResolver:
...
def resolve(self, path):
path = str(path) # path may be a reverse_lazy object
tried = []
# 匹配path
match = self.pattern.match(path)
if match:
new_path, args, kwargs = match
# 如果匹配成功,则继续匹配它的url_patterns
for pattern in self.url_patterns:
try:
# 这里比较关键,这个pattern可能是urlpattern,也可能是URLResolver;如果是urlpattern,匹配成功则返回ResolverMatch;如果是URLResolver,则会递归调用下去。
sub_match = pattern.resolve(new_path)
except Resolver404 as e:
sub_tried = e.args[0].get('tried')
if sub_tried is not None:
tried.extend([pattern] + t for t in sub_tried)
else:
tried.append([pattern])
else:
if sub_match:
# 匹配成功,生成ResolverMatch返回。
# Merge captured arguments in match with submatch
sub_match_dict = {**kwargs, **self.default_kwargs}
# Update the sub_match_dict with the kwargs from the sub_match.
sub_match_dict.update(sub_match.kwargs)
# If there are *any* named groups, ignore all non-named groups.
# Otherwise, pass all non-named arguments as positional arguments.
sub_match_args = sub_match.args
if not sub_match_dict:
sub_match_args = args + sub_match.args
return ResolverMatch(
sub_match.func,
sub_match_args,
sub_match_dict,
sub_match.url_name,
[self.app_name] + sub_match.app_names,
[self.namespace] + sub_match.namespaces,
)
tried.append([pattern])
raise Resolver404({'tried': tried, 'path': new_path})
raise Resolver404({'path': path})
...
摘选了关键部分来分析,最核心的就是看resolve方法。这里面在遍历url_patterns的时候,这里面的pattern可能是urlpattern,也可能是URLResolver;如果是urlpattern,匹配成功则返回ResolverMatch;如果是URLResolver,则会递归调用下去。所以这就可以解释为什么可以进行多级的url配置。
总结
可以发现,比较关键的就是ResolverMatch,URLPattern和URLResolver。
ResolverMatch就是匹配结果,包含匹配成功后需要的信息;
URLPattern是一条url映射信息的对象,包含了url映射对应的可调用对象等信息;
URLResolver是实现url路由,解析url的关键的地方,它的url_patterns既可以是URLPattern也可以是URLResolver。正是因为这种设计, 实现了对URL的层级解析。