问题
- 当我们拿到一个路径的时候,是怎么找到对应的view函数的
- 为什么可以include其它的urls
- 为什么urls.py里面需要一个叫urlpatterns的列表,并且里面是一个个的path()或re_path()函数调用
- 路径pattern开头要不要写反斜杠/,结尾要不要写反斜杠/
- 路径pattern里要不要写^和$
- 为什么包含admin.site.urls不需要include
看了下面的剖析并结合Django源码,就可以解答上面这些问题了。
剖析
urls是怎么进行匹配的呢?
其实就是一颗树,DFS(深度优先查找)匹配。
主要函数和类:
- RegexPattern # 对re的封装,返回新的path、 args、 kwargs
- URLResolver # 可以理解为非叶子节点,遇到则递归下去
- URLPattern # 可以理解为叶子节点,匹配则结束
- ResolverMatch # 匹配对象,可通过
func, args, kwargs = ResolverMatch对象
来获取对应view函数和参数 - path/re_path # 根据情况返回URLPattern或URLResolver
- include
URLResolver和URLPattern都有resolve()
方法, 这是递归的关键。
下面是对源码的抽离和精简,方便演示匹配原理。
有2个文件: urls/resolvers.py和urls/conf.py
urls/resolvers.py文件
import re
from importlib import import_module
class Resolver404(Exception):
pass
class URLResolver:
def __init__(self, pattern, urlconf_name):
self.pattern = pattern
self.urlconf_name = urlconf_name
def resolve(self, path):
match = self.pattern.match(path)
if match:
new_path, args, kwargs = match
for pattern in self.urlconf_module.urlpatterns:
try:
sub_match = pattern.resolve(new_path)
except Resolver404 as e:
pass
else:
if sub_match:
return ResolverMatch(
sub_match.func,
sub_match.args,
sub_match.kwargs,
)
raise Resolver404({'path': new_path})
raise Resolver404({'path': path})
@property
def urlconf_module(self):
if isinstance(self.urlconf_name, str):
return import_module(self.urlconf_name)
else:
return self.urlconf_name
class URLPattern:
def __init__(self, pattern, callback):
self.pattern = pattern
self.callback = callback # the view
def resolve(self, path):
match = self.pattern.match(path)
if match:
_, args, kwargs = match
return ResolverMatch(self.callback, args, kwargs)
class ResolverMatch:
def __init__(self, func, args, kwargs):
self.func = func
self.args = args
self.kwargs = kwargs
def __getitem__(self, index):
return (self.func, self.args, self.kwargs)[index]
def __repr__(self):
return "ResolverMatch(func=%s, args=%s, kwargs=%s)" % (self.func, self.args, self.kwargs)
class RegexPattern:
def __init__(self, regex):
self.regex = re.compile(regex)
def match(self, path):
match = self.regex.search(path)
if match:
kwargs = match.groupdict()
args = () if kwargs else match.groups()
return path[match.end():], args, kwargs # 这里path[match.end():]返回后面未匹配的部分。比如'^myapp/'匹配'myapp/bar/', 就变成了'bar/'
return None
urls/conf.py文件
from importlib import import_module
from types import ModuleType
from .resolvers import RegexPattern, URLResolver, URLPattern
def re_path(route, view):
if isinstance(view, ModuleType):
# For include(...) processing.
pattern = RegexPattern(route)
urlconf_module = view
return URLResolver(
pattern,
urlconf_module,
)
elif callable(view):
pattern = RegexPattern(route)
return URLPattern(pattern, view)
else:
raise TypeError('blah blah blah')
def include(urlconf_module):
return import_module(urlconf_module)
ok, 我们的urls模块写好了。下面使用并验证。
有3个文件: urls1.py和urls2.py和test_urls.py
urls1.py
from urls.conf import re_path, include
def foo_list_view(request):
pass
def foo_view(request, user_id):
pass
urlpatterns = [
re_path('^foo/$', foo_list_view),
re_path('^foo/(?P<user_id>\d+)/$', foo_view),
re_path('^myapp/', include('urls2')), # 这里用了include
]
urls2.py
from urls.conf import re_path
def bar_view(request):
pass
urlpatterns = [
re_path('^bar/$', bar_view),
]
test_urls.py
from urls.resolvers import URLResolver, RegexPattern
urlconf = 'urls1'
r = URLResolver(RegexPattern(r'^/'), urlconf) # 树根
print(r.resolve('/foo/'))
print(r.resolve('/foo/1234/'))
print(r.resolve('/myapp/bar/'))
输出
ResolverMatch(func=<function foo_list_view at 0x10b2a0f28>, args=(), kwargs={})
ResolverMatch(func=<function foo_view at 0x10b299158>, args=(), kwargs={'user_id': '1234'})
ResolverMatch(func=<function bar_view at 0x10b299268>, args=(), kwargs={})