django源码分析之url路由(URLResolver)

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的层级解析。

推荐阅读更多精彩内容