路由层
- 负责网页 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)。具体来说:
- 路由终点:当
is_endpoint=True
时,Django 知道这个路由条目代表的是一个终点视图函数(比如views.login
),而不是一个子路由的中间层。这表示如果请求路径与该路由匹配,那么这个路径应该由views.login
这个视图来处理。 - 区分子路由和终点:在 Django 路由中,有时会使用
include()
来包含其他的路由配置。对于这些包含的路由,is_endpoint
会被设置为False
,因为它们并不代表一个具体的视图,而是指向另一个路由配置文件。
例如:
urlpatterns = [
path('login/', views.login), # 这是一个终点,is_endpoint=True
path('another/', include("app.another_urls")), # 这不是一个终点,is_endpoint=False
]
路由匹配的流程
- 启动程序,用户访问 URL
- 程序执行到 wsgi.py
# Django 项目的 WSGI 配置。
# 它将 WSGI 可调用程序公开为一个名为 ``application`` 的模块级变量。
application = get_wsgi_application()
get_wsgi_application
返回WSGIHandler
对象
def get_wsgi_application():
"""
Django WSGI 支持的公共接口。返回一个 WSGI 可调用程序。
避免将 django.core.handlers.WSGIHandler 作为公共 API,以防将来内部 WSGI 实现发生变化或移动。
"""
django.setup(set_prefix=False)
return WSGIHandler()
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
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
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
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