Django 路由层 —— 基础用法,以及底层如何实现的

路由层

  • 负责网页 URL函数/类 的对应关系

用法

基本用法

urls.py:

urlpatterns = [
    # 基本用法
    path('login/', views.login),

    # 动态输入参数
    path('info/<int:v1>', views.info),

    # 可以同时传多个参数, path:v3 会记录 v1-v2/ 后面的所有路径
    path('other/<int:v1>-<str:v2>/<path:v3>', views.other),

    # 通过 re_path 应用正则表达式,通过 () 对参数进行分组。
    # r 的作用:告诉编译器需要进行转义
    # re_path 由 django2 引入,在此之前使用的是 url 进行正则
    # 输入 date/2014-02-11  ->  输出 ('2014', '02-11')
    re_path(r'date/(\d{4})-(\d{2}-\d{2})', views.date),
]

views.py:

def login(request):
    return HttpResponse("login")


def info(request, v1):  # 需要设置形参来接受
    return HttpResponse(f"{v1}")


def other(request, v1, v2, v3):
    return HttpResponse(f"{v1, v2, v3}")


def date(request, r1, r2):
    # 输出 ('2014', '02-11')
    return HttpResponse(f"{r1, r2}")

别名

  • 方便后续使用别名反向生产 URL ,以及使用 redirect 重定向
  • 有时还用于分配权限
path('login/', views.login, name="login"),
path('info/<int:v1>', view.info, name="info"),
print(reverse("login"))  # '/login/'
print(reverse("info", kwargs={"v1": 123}))  # '/info/123/'

路由分发

  • 项目较大 -> 一个 urls.py 内容太多 -> 拆分成多个 urls.py

urls.py:

urlpatterns = [
    # 根路由
    path('login/', views.login),  # /login/

    # 使用 include 进行路由分发
    path('another/', include("app.another_urls"))
]

app.another_urls:

urlpatterns = [
    path('login/', views.login)  # /another/login/
]
  • 另一种写法:

urls.py:

urlpatterns = [
    path('login/', views.login),

    # 元组里面放 urlpatterns 列表
    path('another/', ([
        path('login/', views.login)  # /another/login/
    ], None, None))
]

namespace

  • include 函数
def include(arg, namespace=None):
    app_name = None
    if isinstance(arg, tuple):
        try:
            urlconf_module, app_name = arg
        except ValueError:
            # ......
    else:
        # No namespace
        urlconf_module = arg
        
	if isinstance(urlconf_module, str):
        urlconf_module = import_module(urlconf_module)
        
    # ......
    
    # 只提供 namespace 不提供 app_name 会报错
    if namespace and not app_name:
        raise # ......
        
    # ......
    return (urlconf_module, app_name, namespace)

使用 namespace:

# urls.py:
path('another/', include(("app.another_urls", "app"), "ns"))

# another_urls.py:
path('ns/', views.ns, name="n")

# views:
def login(request):
    # print(reverse("n"))  # 在有 namespace 的情况下会报错
    print(reverse("ns:n"))  # namespace 的作用:防止多路由状态下的别名冲突
    return HttpResponse("login")

源码

路由的本质

path('login/', views.login),

#   ↓   path = partial(_path, Pattern=RoutePattern)
#   ↓   partial 是 python 自带的方法,称为偏函数。此处,用于将 Pattern=RoutePattern 参数传递到函数 _path 中
#   ↓   在函数 _path 中会创建一个 URLPattern 对象:URLPattern(pattern, view, kwargs, name)
#   ↓   pattern = Pattern(route, name=name, is_endpoint=True)

URLPattern(RoutePattern('login/', name=None, is_endpoint=True), views.login, None, None)


path('another/', include("app.another_urls"))
#   ↓   
URLResolver(RoutePattern('another/', name=None, is_endpoint=False), "app.another_urls", None, None, None)
  • _path 创建 URLPattern 或 URLResolver 对象部分如下:
def _path(route, view, kwargs=None, name=None, Pattern=None):
    # path('another/', include("app.another_urls"))
    if isinstance(view, (list, tuple)):
        pattern = Pattern(route, is_endpoint=False)
        urlconf_module, app_name, namespace = view
        return URLResolver(
            pattern,
            urlconf_module,
            kwargs,
            app_name=app_name,
            namespace=namespace,
        )
        
    # path('login/', views.login)
    elif callable(view):  # callable 用于判定函数是否可执行。简单来说就是 能不能在后面加括号。如:views.login()
        pattern = Pattern(route, name=name, is_endpoint=True)
        return URLPattern(pattern, view, kwargs, name)
  • 关于 URLPattern 和 RoutePattern
# 传入:
URLPattern(RoutePattern('login/', name=None, is_endpoint=True), views.login, None, None)


# URLPattern 
class URLPattern:
    def __init__(self, pattern, callback, default_args=None, name=None):
        self.pattern = pattern  # RoutePattern('login/', name=None, is_endpoint=True)
        self.callback = callback  # views.login
        self.default_args = default_args or {}  # None
        self.name = name  # None
        
# urls.py:
# print("callback: ", urlpatterns[0].callback)  ->  callback:  <function login at 0x0000023A5DBDBA30>


# RoutePattern
class RoutePattern(CheckURLMixin):
    regex = LocaleRegexRouteDescriptor()

    def __init__(self, route, name=None, is_endpoint=False):
        self._route = route  # 'login/', 
        
        # _route_to_regex 的作用:
        # 将路径模式转换为正则表达式。返回正则表达式和一个将捕获名称映射到转换器的字典。
        # 例如,“foo/<int:pk>”将返回“^foo\\/(?P<pk>[0-9]+)” 和 {'pk': <django.urls.converters.IntConverter>}
        self._regex, self.converters = _route_to_regex(str(route), is_endpoint)
        
        self._regex_dict = {}
        self._is_endpoint = is_endpoint  # is_endpoint=True
        self.name = name  # name=None
        
        
# URLPattern 中嵌入 RoutePattern 的一种用法:
class URLPattern:
    def resolve(self, path):
        # ......
        self.pattern.match  # -> 此处就可以调用 RoutePattern 中的 match 方法
  • 关于 is_endpoint的作用

is_endpoint=True 的作用是标识该路由是一个终点(endpoint)。具体来说:

  1. 路由终点:当 is_endpoint=True 时,Django 知道这个路由条目代表的是一个终点视图函数(比如 views.login),而不是一个子路由的中间层。这表示如果请求路径与该路由匹配,那么这个路径应该由 views.login 这个视图来处理。
  2. 区分子路由和终点:在 Django 路由中,有时会使用 include() 来包含其他的路由配置。对于这些包含的路由,is_endpoint 会被设置为 False,因为它们并不代表一个具体的视图,而是指向另一个路由配置文件。

例如:

urlpatterns = [
    path('login/', views.login),  # 这是一个终点,is_endpoint=True
    path('another/', include("app.another_urls")),  # 这不是一个终点,is_endpoint=False
]

路由匹配的流程

  1. 启动程序,用户访问 URL
  2. 程序执行到 wsgi.py
# Django 项目的 WSGI 配置。
# 它将 WSGI 可调用程序公开为一个名为 ``application`` 的模块级变量。

application = get_wsgi_application()
  1. get_wsgi_application 返回 WSGIHandler对象
def get_wsgi_application():
    """
    Django WSGI 支持的公共接口。返回一个 WSGI 可调用程序。

	避免将 django.core.handlers.WSGIHandler 作为公共 API,以防将来内部 WSGI 实现发生变化或移动。
    """
    django.setup(set_prefix=False)
    return WSGIHandler()
  1. WSGIHandler会调用 __call__
class WSGIHandler(base.BaseHandler):
    request_class = WSGIRequest

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.load_middleware()

    def __call__(self, environ, start_response):
        set_script_prefix(get_script_name(environ))
        signals.request_started.send(sender=self.__class__, environ=environ)
        
        # environ 是用户浏览器发送请求时,携带的所有请求相关的数据
        # request = self.request_class(environ)  =>  request = WSGIRequest(environ)
        # environ 内部的数据杂乱无章,使用 WSGIRequest 进行封装,方便使用
        # request.method
        # request.POST
        # request.path_info
        request = self.request_class(environ)
        
        # 路由的匹配 request.path_info  /login/  ->  读取项目中 urlpatterns 所有的路由
        # 找到相应的函数  views.login
        # 执行函数并返回值  HttpResponse("login")
        response = self.get_response(request)

        # ......
        # 此处完善了 response 的响应报文
        
        return response
  1. response = self.get_response(request)进行路由匹配
def get_response(self, request):
    """根据给出的 HttpRequest 返回一个 HttpResponse 对象."""
    set_urlconf(settings.ROOT_URLCONF)
    
    # self._middleware_chain
    # ->  self._middleware_chain = handler
    # ->  handler = self.adapt_method_mode(is_async, handler, handler_is_async)  # 判断是同步还是异步(目前是同步)
    # ->  handler = convert_exception_to_response(get_response)  # convert_exception_to_response 中间件,负责异常响应
    # ->  get_response = self._get_response_async if is_async else self._get_response  # 判断是同步还是异步(目前是同步)
    # ->  self._get_response:解析并调用视图,然后应用视图、异常和模板响应中间件。该方法就是请求/响应中间件内部发生的一切。
    # 综上:self._middleware_chain(request)  =>  self._get_response(request)
    response = self._middleware_chain(request)
    
    # ......
    return response
  1. self._get_response(request)进行路由匹配
def _get_response(self, request):
    """
    解析并调用视图,然后应用视图、异常和模板响应中间件。该方法就是请求/响应中间件内部发生的一切。
    """
    response = None
    
    # self.resolve_request: 返回已解析的视图及其参数 args 和 kwargs。
    # callback:视图函数
    # callback_args, callback_kwargs:参数,例如 info/<int:v1>/ 里面的 v1
    callback, callback_args, callback_kwargs = self.resolve_request(request)

    # Apply view middleware
    for middleware_method in self._view_middleware:
        # ......

    if response is None:
        # ......

    # Complain if the view returned None (a common error).
    self.check_response(response, callback)

    # If the response supports deferred rendering, apply template
    # 如果响应支持延迟渲染,则应用模板
    # response middleware and then render the response
    # 执行响应中间件并渲染响应
    if hasattr(response, "render") and callable(response.render):
        # ......

    return response
  1. self.resolve_request(request) 返回 ResolverMatch() 对象
def _get_cached_resolver(urlconf=None):
    return URLResolver(RegexPattern(r"^/"), urlconf)  # RegexPattern(r"^/"): 正则匹配,匹配以 '/' 开头的字符串


def get_resolver(urlconf=None):
    if urlconf is None:
        urlconf = settings.ROOT_URLCONF  # ROOT_URLCONF = 'Django.urls'  ->  此处的 Django 是项目名称,'Django.urls' 指向 urls.py
    return _get_cached_resolver(urlconf)


# 注意,此处的 resolve 属于 URLResolver 类,后续会有递归的情况
def resolve(self, path):
    # path = '/login/'
    # path('another/', include("app.another_urls"))
    path = str(path)
    tried = []
    
    # 判断目前访问的 URL 是否与创建的 URLPattern 记录的 URL 相同
    # match = ('login/', (), {})
    # match = ('another/ns/', (), {})
    match = self.pattern.match(path)
    
    if match:
        # new_path = 'login/'
        # new_path = 'another/ns/'
        new_path, args, kwargs = match
        
        
        # self.url_patterns
        # 使用 getattr 反射
        # ->  getattr(self.urlconf_module, "urlpatterns", self.urlconf_module)
        # =>  self.urlconf_module.urlpatterns(如果没有 urlpatterns 就返回 self.urlconf_module)
        
        # self.urlconf_name 在上面 URLResolver(RegexPattern(r"^/"), urlconf) 中获得,值为 urlconf 也就是 'Django.urls'
        # ->  import_module(self.urlconf_name)
        # =>  from Django import urls
        
        # 故上方的 self.urlconf_module.urlpatterns 即为 urls.urlpatterns, 也就是我们所有的定义的路由
        # 开始循环校验
        for pattern in self.url_patterns:
            try:
                
                # pattern.resolve(new_path)
                # 如果是 new_path = 'login/' 的情况:
                    # => URLPattern(RoutePattern('login/', ...), views.login).resolve('login/')
                    # -> match = self.pattern.match(path) => RoutePattern.match('login/')
                    # -> 进行匹配,判断目前访问的 URL 是否与创建的 URLPattern 记录的 URL 相同
                    # 匹配成功,就返回 path[match.end() :], (), kwargs
                    # match = ('login/', (), {})
                    # 再进一步进行封装
                    # 返回 ResolverMatch 对象
                    # ResolverMatch(func=app.views.login, args=(), kwargs={}, url_name=None, app_names=[],namespaces=[], route='login/')
                
                # 如果是 new_path = 'another/ns/' 的情况:
                	# => URLResolver(RoutePattern('another/', ...), "app.another_urls", ...).resolve('another/ns/')
                    # 此时就会递归当前的 resolve 函数
                    # match = self.pattern.match(path) 匹配时,会从 'another/ns/' 匹配出 new_path = 'ns/',此时为 URLPattern ,同上
                sub_match = pattern.resolve(new_path)
                
            except Resolver404 as e:
                # ......
            else:
                # 匹配成功
                if sub_match:
                    # ......
                    # 对一些参数进行处理
                    # 返回 ResolverMatch 对象
                   # ResolverMatch(func=app.views.login, args=(), kwargs={}, url_name=None, app_names=[], namespaces=[], route='login/')
                    return ResolverMatch(
                        # ......
                    )


def resolve_request(self, request):
    if hasattr(request, "urlconf"):  # 判断是否有 urlconf ,一般默认没有
        # ......
    else:
        resolver = get_resolver()  # resolver = URLResolver(RegexPattern(r"^/"), 'Django.urls')
        
    # 解析视图,并将匹配对象重新分配给请求。
    # resolver.resolve(request.path_info)  ->  resolver.resolve('/login/')
    # resolver_match = ResolverMatch(func=app.views.login, ......, route='login/')
    resolver_match = resolver.resolve(request.path_info)
    
    # 赋值给 request ,request.resolver_match = ResolverMatch(func=app.views.login, ......, route='login/')
    request.resolver_match = resolver_match
    return resolver_match
  • WSGI

WSGI(Python Web Server Gateway Interface,即Web服务器网关接口) 是 Python 定义的 Web 服服务器和 Web 应用程序或框架之间的一种简单而通用的接口。

它是 Python 为了解决 Web 服务器端与客户端之间的通信问题而产生的,它基于现存的 CGI 标准而设计。其定义了 Web 服务器如何与 Python 应用程序进行交互,让 Python 写的 Web 应用程序可以和 Web 服务器对接起来。

  • __call__:

使一个类可以像函数一样调用。

使用时,x() 等价于 x.__call__()

class A(object):
    def __init__(self, name, age, male):
        self.name = name
        self.age = age
        self.male = male
 
    def __call__(self, name, age):
        self.name, self.age = name, age
 
 
if __name__ == '__main__':
    a = A('dgw', 25, 'man')
    print(a.age, a.name)  # 25 dgw
    a('zhangsan', 52)
    print(a.name, a.age)  # zhangsan 52
  • 7 到 1:

self.resolve_request(request) 返回 resolver_match -> ResolverMatch() 对象ResolverMatch(func=app.views.login, args=(), kwargs={}, url_name=None, app_names=[], namespaces=[], route='login/')

self._get_response(request) 返回 response -> <HttpResponse status_code=200, "text/html; charset=utf-8">

然后一直往上,返回响应报文

源码的应用(用的不多)

自定义拓展:

Class MYURLPattern(URLPattern):
    def ... :
        pass

MYURLPattern(RoutePattern('login/', name=None, is_endpoint=True), views.login, None, None)

补充:关于网址后面的"/"

  • Django 的默认设置 地址
...\envs\Lib\site-packages\django\conf\global_settings.py
  • Django 的默认设置中,有一个 APPEND_SLASH = True,他的功能是:
# 在访问 "/login" 时,自动重定向到 "/login/"
path('login/', views.login)

注意: 重定向后,访问的方法是 GET 方法。如果使用 POST 访问 “/login”,重定向后会出问题。

  • 可以在 setting.py 中,添加 APPEND_SLASH = False
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值