django中的路由系统
django中路由的作用和路由器类似,当一个用户请求Django站点的一个页面时,是路由系统通过对url的路径部分进行匹配,一旦匹配成功就导入并执行对应的视图来返回响应。
django如何处理请求
- 当一个请求来到时,django首先到项目中查找根URLconf模块,在其中查找路由匹配规则。
- 根URLconf模块,就是项目文件目录下的urls.py文件。在这个文件中定义了一个变量urlpatterns。它是一个列表,其中每一个元素都是一个url模式,定义了url和视图函数的对应关系。
- django按顺序运行每个url模式,并在与请求的url匹配的第一个模式停止。
- 一旦去中一个url模式匹配,django将导入并调用该视图。
- 如果没有匹配的模式,或者在此过程中任何时候引发异常,django调用错误处理视图。
路由模块
在django中路由模块一般命名为url.py
。
每一个路由模块中都会包含一个urlpatterns
变量,它是一个django.urls.path()
或者django.urls.re_path()
实例的列表。
根路由模块
最外层的路由模块,路由解析的入口。
django通过设置ROOT_URLCONF
来确定主路由模块,通常是项目目录下的urls.py
模块。
子路由
主路由包含的其他路由都是子路由。
一般都是各自应用目录下的urls.py
模块。
path()
-
path(route,view, kwargs=None,name=None)
-
path
函数返回一个对象,表示一个路由规则 -
route
:一个字符串,表示url规则 view
: 一个视图kwargs
:一个字典,需要传入的额外参数name
:url的命名
案例:
上一节,我们在crm
应用中定义了如下路由:
path('index/', views.index)
将'index/'
和视图views.index
进行映射。
include()
-
include(module, namespace=None)
-
将一个子路由导入到一个URLconf模块中
- module: URLconf模块(或模块名称)
案例:
上一节,我们在根路由中,通过include
包含了crm
子路由:
path('crm/', include('crm.urls'))
将crm/
和子路由crm.urls
进行映射。
URLconf在什么上查找
请求的url会被看做是一个普通的Python字符串,URLconf在其上查找并匹配。
进行匹配时不包含GET
或POST
请求方式的参数以及域名。
例如,https://www.example.com/myapp/
请求中,URLconf 将查找 myapp/
在 https://www.example.com/myapp/?page=3
请求中,URLconf 仍将查找 myapp/
。
URLconf不检查使用哪种请求方法。
换句话说,所有请求方法,对同一个URL无论是POST请求
,GET请求
等等,都将路由到相同的视图函数。
案例:
所以在浏览器中访问地址http://127.0.0.1/crm/index/
时,URLconf将查找crm/index/
。
第一步在跟路由中进行匹配,可以匹配到:
path('crm/', include('crm.urls'))
第二步当遇到include()
时,它会将匹配到该点的URL
部分crm/
切掉,并将剩余字符串index/
发送到包含的crm.urls
模块中进一步匹配
第三/index
在crm.urls
路由中可以匹配到:
path('index/', views.index)
没有下一步匹配,直接执行对应的视图函数views.index
。
注意,这个过程会递归的进行,中间遇到任何一条匹配的路由就会返回。
在URL中捕获参数
django允许在url中捕获值,若要从URL中捕获值,请使用尖括号。
尖括号定义变量名,捕获的值传递给视图函数相同名称的参数。格式如下:
'<参数名>'
案例:
写一个学生详情接口,通过crm/students/n/
返回id为n的学生的信息。
视图代码:
# crm/views.py
def detail(request, pk):
return HttpResponse('学生id为{}的详情'.format(pk)) # 模拟返回对应学生的详情
路由代码:
# crm
urlpatterns = [
...
path('students/<pk>/', views.detail)
]
现在,在浏览器中访问:http://127.0.0.1:8000/crm/students/2/
会返回页面:
注意看路由部分的<pk>
,这里的pk
对应视图函数的pk
形参。django会自动匹配url中这部分的字符串2
然后传递给detail
函数的pk
形参。
路径转换器
上面的案例有个漏洞,如果我们在浏览器中输入http://127.0.0.1:8000/crm/students/aaa/
,我们发现它依然可以访问。
在实际开发中,这显然不对,id不可能是个字符串。当然可以在view中进行类型转换,但是不够通用。django中设计了路径转换器,能够在路由匹配时,自动进行转换。
以下路径转换器在默认情况下是可用的:
-
str
: 匹配除了路径分隔符/
之外的任何非空字符串。如果表达式中不包含转换器,默认为字符串转换器。 -
int
: 匹配0或任何整数。返回一个整数类型 slug
: 匹配任何由ASCII字符或数字组成的slug字符串,加上连字符和下划线。uuid
: 匹配格式化的UUID。为了防止多个url映射到同一个页面,必须包含破折号并且必须是小写字母。例如:075194d3-6885-417e-a8a8-6c931e272f00
path
: 匹配任何非空字符串,包括路径分隔符/
。这允许匹配完整的URL路径,而不是像str
那样仅匹配url路径部分。
路径转换器的使用方式非常简单,只需要在捕获符号<>
中,以以下语法即可:
'<转换器:参数名>'
我们可以将上面的案例修改为:
path('students/<int:pk>/', views.detail)
然后,我们再次访问`http://127.0.0.1:8000/crm/students/aaa/,结果是404。
当然,我们也可以捕获多个值,看如下案例:
视图代码:
# crm/views.py
def student_list(request, year, month):
return HttpResponse('{}年{}月创建的学生列表'.format(year, month))
路由代码:
# crm/urls.py
urlpatterns = [
...
path('students/<int:year>-<int:month>/', views.student_list),
path('students/<int:year>/<int:month>/', views.student_list)
]
那么通过urlhttp://127.0.0.1:8000/crm/students/2022/01
和http://127.0.0.1:8000/crm/students/2022-01/
会得到相同的结果。
但是如果访问http://127.0.0.1:8000/crm/students/9527-100/
也会得到结果:
这显然又是bug。
路径转换器只能进行简单的类型转换和匹配,还需要更强大的匹配功能,需要用到re_path()
函数。
re_path()
re_path(route,view, kwargs=None,name=None)
函数返回一个对象,表示一条路由规则。- route: 一个字符串,表示一个url规则
- view:一个视图
- kwargs: 一个字典,需要传入的额外参数
- name: url命名
与path()
不同的是,route
部分包含正则表达式。
当进行匹配时,从正则表达式中捕获的组会被传递到视图中。
如果组是命名的,则作为命名参数,否则作为位置参数。值以字符串的形式专递,不进行任何类型转换。
命名正则表达式分组的语法是:(?P<name>pattern)
,其中name
是组的名称,pattern是要匹配的某个模式。
下面是前面例子中的路由,使用正则表达式重写:
re_path(r'^students/(?P<year>\d{4})-(?P<month>[1-9]|1[0-2])/$', views.student_list),
re_path(r'^students/(?P<year>\d{4})/(?P<month>[1-9]|1[0-2])/$', views.student_list)
这样写和之前的路由匹配一致,只是:
- 匹配的url会受到限制,例如:100月将不再匹配,因为月份整数被限制为1-12。
- 捕获的每个参数都以字符串的形式发送到视图。
- 当从使用
path()
切换到re_path()
或相反时,特别重要的是注意视图参数的类型会发生变化,因此可能需要调整视图。
使用没有命名分组的正则表达式
除了命名组语法,例如(?P<year>\d{4})
,还可以使用较短的未命名组,例如(\d{4})
。这种写法并不特别推荐,因为它更容易在匹配的预期含义和视图参数之间意外的一如错误。
注意在实际使用中建议只使用一种,要么命名,要么不命名,因为当两种方式混合使用时,会忽略未命名的组,只将命名的组传递给视图函数。
url命名
path()
, re_path()
还有一个参数,那就是name,这个参数可以给我们的url命一个名。
那它有什么作用呢?
在实际开发中,经常需要获取最终形式的url,比如嵌入的页面链接和服务端导航(重定向)。
我们来模拟一个登录过程,创建一个登录的函数视图如下:
# crm/views.py
from django.shortcuts import redirect
...
def login(request):
return redirect('/crm/index/')
配置好url
# crm/urls.py
path('login/', views.login)
当我们访问/crm/login/
时会发现页面被重定向到了/crm/index/
,但是这里有一个问题:重定向这里的url是硬编码的,万一将来我们要修改这个url(这个几率很大),那么我们需要在代码中修改所有硬编码的url部分。这显然不利于维护。所以,强烈建议不要硬编码URL(这是一个费力,不能扩展,容易出错的注意)。
django提供一个django.shortcuts.reverse()
函数,它接受一个url的命名,能反向解析出url的绝对路径。
给crm应用的每条路由都添加一个name。
urlpatterns = [
path('index/', views.index, name='index'),
path('students/<int:pk>/', views.detail, name='student_detail'),
path('students/<int:year>/<int:month>/', views.student_list, name='student_list'),
path('login/', views.login, name='login')
]
然后修改登录视图如下:
from django.shortcuts import reverse
def login(request):
url = reverse('index')
return redirect(url)
这样不管怎么修改url,reverse
都可以动态的解析出url。
当有url参数时,可以通过args
,kwargs
进行传递,例如:
reverse('student_list', kwargs={'year': 2021, 'month': 12})
# 或
reverse('student_list', args=(2021,12))
# 都可以解析出 /crm/students/2021/12/
注意:在reverse
中args
,kwargs
两个参数不能同时使用。
app_name
将url命名为index
,login
非常常见,一个项目中,不同的app给url相同的命名,那怎么区分不同应用相同名称的url呢?
非常简单,在应用下的urls.py文件中定义一个app_name
变量,给它赋值为引用的名称,例如在crm/urls.py
中定义app_name
变量如下:
# crm/urls.py
...
app_name = 'crm' # 一般和引用同名
...
定义app_name
之后,再解析url时,需同时传入app_name
,格式如下:
'app_name:url_name'
那么要解析index
的url的代码如下:
reverse('crm:index')