HTTP服务端框架对不同功能模块的请求通常是按“路由”规划和拆分的,浏览器或HTTP客户端发起请求时在请求信息中设置不同的URL,服务端则根据URL找到对应的代码进行处理。Django中也是类似的,路由负责将URL请求映射到相应的视图函数(FBV)或类视图组件(CBV),这篇笔记我们介绍Django的路由系统。
Django的路由是通过配置文件集中配置的。前面我们提到过,Django项目的代码目录结构是Project下划分多个APP,路由划分也是类似的,Project下有一个根路由配置,每个APP下有一个子路由配置,子路由配置由根路由配置集中引用。那么根路由配置又是如何被加载的呢?其实在settings.py
中有配置项ROOT_URLCONF
,这个属性指定根路由配置文件的位置。例如配置ROOT_URLCONF = 'mysite.urls'
表示根路由配置文件在mysite
模块下,文件名是urls.py
。
下面例子演示了Django中路由的配置结构,工程目录如下,其中mysite
是根模块,其中包含项目配置文件、根路由配置文件等;demo01
是一个APP,其中包含具体业务视图代码和子路由配置文件。
|_ mysite # 根模块
|_ urls.py # 根路由配置文件
|_ settings.py # 项目配置文件
|_ demo01
|_ urls.py # 子路由配置文件
|_ views.py # 视图函数
settings.py
ROOT_URLCONF = 'mysite.urls'
项目配置文件中指定了根路由配置文件路径。
mysite/urls.py
from django.urls import path, include
urlpatterns = [
path('demo01/', include('demo01.urls')),
]
上面代码中,根模块中引入了子模块的路由配置。具体来说,path()
函数用于配置Django路由,参数指定demo01
子模块的路由具有demo01/
前缀;此外Django的路由系统提供了include()
函数实现了引入子路由配置这一目的,参数和写法和ROOT_URLCONF
相同。
demo01/urls.py
from django.urls import path
from demo01.views import demo
urlpatterns = [
path('demo/', demo),
]
子模块中,路由同样使用path()
函数配置,其中指定demo
函数作为视图函数(FBV),该路由的匹配路径为demo/
。
demo01/views.py
from django.shortcuts import render
def demo(request):
return render(request, "success.html")
demo01/views.py
中,我们定义了一个视图函数demo()
,它简单的返回了一个HTML模板。
如上配置完成后,我们就可以启动项目了,测试服务器默认监听8000端口,我们使用浏览器访问http://127.0.0.1:8000/demo01/demo/
并查看返回的页面。
Django的路由系统中,path()
函数能够创建精确匹配路由,下面例子代码创建了精确匹配/demo/
的路由配置。
from django.urls import path
from demo01.views import demo
urlpatterns = [
path('demo/', demo, name='demo'),
]
path()
函数的参数中,第1个参数是匹配路径,第2个参数是视图函数,第3个参数是路由名称(该参数是可选的)。路由名称通常用于反向解析,即根据name
获取对应的URL,它应该设置的尽量具有描述性且简短和唯一。下面代码演示了这一用法,其中demo_url
会输出/demo01/demo/
。
from django.shortcuts import render
from django.urls import reverse
def demo(request):
demo_url = reverse('demo')
print(demo_url)
return render(request, "success.html")
下面例子中演示了Django中使用正则匹配方式配置路由,正则匹配需要使用re_path()
函数。
from django.urls import re_path
from demo01.views import blog
urlpatterns = [
re_path(r'^blogs/(?P<blog_id>[0-9]+)/$', blog, name="blog"),
]
上面代码中,re_path()
函数的第1个参数是匹配路由的正则表达式,第2个参数是视图函数,第3个参数是路由名称(该参数是可选的)。正则表达式中的(?P<blog_id>[0-9]+)
表示匹配一个或多个数字,并将其命名为blog_id
,其中(?P<...>)
是捕获组。在这种匹配模式下,我们需要访问形如/blogs/1/
的URL,如果访问/blogs/
、/blogs/a/
是无法成功匹配的。
from django.shortcuts import render
def blog(request, blog_id):
print('Blog ID ', type(blog_id), blog_id)
return render(request, "success.html")
视图函数中,我们直接以blog_id
为参数名接收匹配到的值即可,注意此处正则表达式匹配的blog_id
是字符串类型。
正则匹配实际上写起来比较复杂,Django还支持一种转换器匹配,它能替代大部分正则匹配路由配置,省去写正则表达式的麻烦。下面例子我们依然是匹配形如/blogs/1/
的URL,但使用转换器匹配方式。
from django.urls import path
from demo01.views import blog
urlpatterns = [
path('blogs/<int:blog_id>/', blog, name="blog"),
]
上述配置和前面正则匹配的路由完全相同,而且blog_id
传入视图函数时还会自动转换为整数类型。除了<int: ... >
转换器,Django还支持其他几种转换器,如下表所示。
转换器 | 描述 |
---|---|
str | 匹配除了路径分隔符/ 之外的非空字符串 |
int | 匹配整数 |
slug | 匹配包含字母、数字、下划线和破折号的字符串 |
uuid | 匹配UUID字符串 |
path | 匹配包含斜杠的任意字符串 |
实际开发中,推荐优先考虑精确匹配和转换器匹配,它们更简洁高效,这两种匹配模式无法实现再考虑使用正则匹配。
嵌套路由主要用于根路由配置包含子路由配置的情况,前面我们已经使用过了。在大型项目中,子模块也可能进一步细分,这对于组织大型项目非常有用。
from django.urls import path, include
urlpatterns = [
path('demo01/', include('demo01.urls')),
]
上面代码中,根模块中引入了demo01
子模块的路由配置。
前面例子中,我们最终访问demo()
视图函数的URL是http://127.0.0.1:8000/demo01/demo/
,可以看到它是以斜线/
结尾的,这其实是Django的一种约定而非强制性的,不过使用Django框架时我们应尽量遵守约定。Django约定URL以斜线结尾有以下几个理由:
URL规范化:URL规范化是Django的核心设计哲学之一。技术上来说,foo.com/bar
和foo.com/bar/
看似相同,但其实是两个不同的URL。为了避免搜索引擎和某些Web流量分析工具将它们视为不同的页面,Django约定通过统一添加斜线来规范化URL。
APPEND_SLASH:基于前一条原因,Django默认启用APPEND_SLASH
设置。如果请求的URL不匹配任何路由且不以斜线结尾,Django会自动重定向到结尾带斜线的URL。也就是说,前面代码中如果访问/demo01/demo
,Django也会自动重定向到/demo01/demo/
。但如果路由配置是path('demo', demo)
,则不会发生任何自动的重定向行为,你必须且只能通过/demo01/demo
访问这一路由。