Django的路由系统

Django的路由系统指的是路径和函数的对应关系,之前只知道在urls.py内修改urlpattern的内容,知道正则匹配的路径开头与视图的匹配关系.

Django的路由系统在Django中叫做URLconf系统,就像Django维护的一个目录,本质是URL与要为该URL调用的视图之间的映射表.来看看Django 1.11的URLconf官方文档是怎么说的吧:

针对Django里的app,想让其正常工作,也要为其设计一套URL以便浏览器访问该app不同的功能.在设计一个app的URL时,会建立一个Python模块,我们非正式的将其称作URLconf. URLconf用纯Python语言编写,提供了URL路径样式(正则表达式)与视图函数的对应关系.URLconf可长可短,还可以引用其他的映射关系,也可以动态生成.

简明的说,URLconf就是一个py文件,里边写上了URL和视图的对应关系,放在项目下边,作为URLconf配置文件.当有请求进来的时候,就会按照其中的配置发挥作用.URLconf文件的名称也可以自定义.在项目的根目录下约定俗成采用urls.py当做初始的URLconf配置文件.

URLconf配置文件

# URLconf文件
from django.conf.urls import url
urlpatterns = [
     url(regex, view, kwargs=None, name=None),
]

在URLconf配置文件的开始部分,必须导入url.然后在文件中设置一个叫做urlpatterns的列表,其中的每一个元素,都是url的实例.
url可接受4个参数:

  1. regex 是正则表达式
  2. view 是视图函数(类)名
  3. kwargs是一个字典,是可选的,包含额外传递给视图的参数
  4. name是一个别名,稍后解释

制作了这样一个文件之后,这个文件就是当前站点的URLconf文件.

正则匹配详解

正则部分的匹配,是从域名加上表示根目录的/之后开始匹配的,对于常见的GET加参数的请求为例:

http://www.conyli.cc:8000/wp-admin/?article=1109

这行URL传到Django之后,Django用其中的wp-admin/部分与URLconf进行匹配,之前的域名,端口,第一个表示根目录的”/”以及表示参数的部分都会被忽略,只用wp-admin/部分.
在之前的项目中,一直使用类似:

url(r'^item_edit/', views.item_edit)

这样的模式.之前没有深究,现在可以看看具体的匹配了.

# 匹配item_edit/开头的URL,类似item_edit/new/也会匹配成功 
r'^item_edit/' 
# 精确匹配,仅能匹配"item_edit/",再有子路径无法匹配成功
r'^item_edit/$'
# 批量匹配一批由任意数字排序的URL,比较常用在内容管理系统内.
r'^book/[0-9]{2,4}/$'
# 分组,匹配类似book/333/ab/这样的URL,注意,如果采用分组,分组匹配的部分会把值传递给对应的视图函数.
r'^book/([0-9]{2,4})/([a-zA-Z]{2})/$'
# 分组命名匹配,与分组类似,但是给每个分组按照正则规则命名,会按照关键字传入视图函数
r'^articles/(?P[0-9]{4})/$'

上边的几种例子中,最后两种最为重要.仅分组不命名的情况下,匹配的url部分按照位置参数的方式传递给视图函数(视图函数必须采取各种方法接收:比如定义位置参数,采用默认值或者定义*args来接收参数).分组且命名的情况下,匹配成功的url部分按照关键字参数的方式传给视图函数(视图函数需要定义对应的关键字参数或者**kwargs用于接收关键字参数).

在今后编写URLconf和视图函数的时候,对于不需要传递字符串或者不需要返回给用户一个可重用的具体链接的时候,最好都尽量采用URLconf体系.
总结一下URLconf里的注意事项:

  1. URLconf内的匹配顺序是从上到下,取第一个匹配成功的视图执行响应,后边不再匹配
  2. 如果要捕获URL的一部分,在对应的正则匹配的地方用分组元字符,还可以命名.匹配的值会传递给视图函数
  3. 正则的开头不要写”/”,这个不属于路径的一部分
  4. 最好用r字符串传格式,否则会面临正则二次转义的问题
  5. 所有正则捕获传递给视图函数的值都是字符串类型,需要注意视图函数中的类型转换.
  6. 正则表达式不检查请求类型,GET POST等都是按照同样的路径解析方式

还有一个小要点是,Django 对于末尾没有/的路径,会自动加上”/”然后在用这个URL去匹配,如果想关闭该功能,需要在settings.py中加上 APPEND_SLASH=False 即可.默认是True.用的不多.

由于分组传递参数的功能,以及视图函数是纯python代码的灵活性,可以实现各种批量URL处理的功能.比如不指定具体路径的时候默认返回第一页的内容:

# urls.py中
from django.conf.urls import url

from . import views

urlpatterns = [
    url(r'^blog/$', views.page),
    url(r'^blog/page(?P[0-9]+)/$', views.page),
]
# views.py中,可以为num指定默认值
def page(request, num="1"):
    pass

这个url与函数的搭配就实现了如果直接输入/blogs/默认返回第一页,输入/blogs/9/会返回第9页的功能.

多重路径匹配

一开始的时候说过,只要符合URLconf配置标准的文件都可以用来进行路径解析.在路径解析的时候,对于某一个路径,还可以引入其他的路径解析文件继续再当前的基础上解析.这就类似一级路由转发给二级路由,让路径解析变得模块化和易于扩展.在url内部采用include方式进行导入.例子如下:

from django.conf.urls import include, url

urlpatterns = [
   url(r'^admin/', admin.site.urls),
   url(r'^blog/', include('blog.urls')),
]
# 其中的blog.urls就是另外一个URLconf文件.会继续拿着以"blog/"开头的路径进行解析.

传递额外参数给视图函数

url的第一个参数正则表达式已经学习完毕.第二个参数已经知道是视图函数或者类,类用as_view()来注册.
现在看第三个参数.
第三个参数Kwargs是一个可选的字典类型的参数,如果不传,就不会给视图函数传入参数.如果传了参数,这个字典会被拆解成关键字参数传给视图函数.举个例子:

from django.conf.urls import url
from . import views

urlpatterns = [
    url(r'^blog/(?P[0-9]{4})/$', views.year_archive, {'foo': 'bar'}),
]

这里正则用命名分组,url还带一个字典参数,假设如果year分组匹配到2019的话,views.year_archive函数接受到的参数有:默认的request, year=2019, foo = “bar”.这个在某些场景下有使用.

命名URL和URL反向解析

最后这一部分就是url的第四个参数即name=None.这个name实际上就是给匹配的那段url起一个别名.
先来看一个例子,用来理解什么叫做反向解析:
在模板里,经常会写各种超链接,尤其是在网站内部跳转的超链接,之前的项目经常写成这样:

<a href="/move_add/?id={{ item.id }}" class="btn btn-primary">录入单据

这样做的最大问题就是跳转的链接被硬编码了,一旦更改了url里的路径匹配,则所有用到该路径的模板全部需要重写路径.
这个时候就要用到name别名来通过别名来反向解析指定的url,不用硬编码.
修改urls.py里的内容改成如下:

url(r'^move_add/', views.move_add,name="io_ticket")

然后在模板内采用url tag 来写成如下:

<a href="{% url "io_ticket" %}?id={{ item.id }}" class="btn btn-primary">录入单据</a>

此时打开页面,可以看到比如修改urls.py里的move_add为move_plus,刷新页面之后,a标签的链接也会同步变动,依然由原来的函数接受,便不用再具体更改页面中的链接.

所谓反向解析,是URLconf文件提供的一种对应机制,不仅通过匹配URL从而找到处理该请求的视图函数,还能够通过名字找到当时匹配的URL.刚才解决了模板内硬编码的问题,但是在后端代码里,视图函数的函数体和重定向的过程中,还有可能硬编码了路径,这个时候就要看如何在普通代码和重定向的时候使用反向解析.

反向解析在视图函数的重定向方法里的使用,以返回上一个例子的URL为例:

# 需要先导入反向解析函数
from django.urls import reverse

reverse_url = reverse("io_ticket")
redirect(reverse_url)

这样就可以返回反向解析后的路径了.

解决了绝对路径硬编码的问题,还有一个问题需要解决,就是我的io_ticket别名对应的内部路径其实是一个死路径/move_add/,没有用到正则表达式里的匹配符号,如果是下边这种情况,如何取得确定的URL呢?

url(r'^blog/(?P<year>[0-9]{2,4})/(?P<title>[a-zA-Z]{2})$', views.year_archive,name="archive")

在Djaogo里如果直接对archive用reverse方法,Django会报一个NoReverseMatch错误,因为通过archive虽然找到了URLconf中的对应关系,但对应的是一个不确定的正则表达式,这个时候如果想要得到确定的URL,就必须给reverse函数传递参数.
上边的redirect代码部分就要修改成:

from django.urls import reverse

reverse_url = reverse("archive",kwargs={"year":2018,"title":"xb"})
redirect(reverse_url)

这段代码就是把需要解析的路径别名,给路径别名传递的关键字参数,都交给reverse函数处理,这样就动态的得到了URL.
这里的URL是分组且命名的,如果分组不命名,则要给reserve的第二个参数传递一个元组,其中按照位置顺序传值,即可得到实际的URL.
模板中的url tag,也可以用{% url “archive” 参数 %}来给模糊的URL传值.

看一个用反向解析实现按照年份显示文章的例子:

# URLconf配置文件,用来匹配articles/xxxx/这种路径,x为任意数字
urlpatterns = [
    url(r'^articles/([0-9]{4})/$', views.year_archive, name='news-year-archive'),
]

在模板中使用url tag 并且传递参数来设置路径:

# 模板内使用url tag 加参数来反向解析URL
<ul>
{% for yearvar in year_list %}
<li><a href="{% url 'news-year-archive' yearvar %}">{{ yearvar }} Archive</a></li>
{% endfor %}
</ul>

url后边跟着别名,由于别名的正则表达式有一个分组,所以将年份变量yearvar 传给 url tag,得到的实际URL,就是articles/xxxx/,其中xxxx为year_list其中的所有年份的一系列URL链接.这个功能实际上就是按年份建立了所有的路径,视图函数在接受这个链接的时候,就知道去取哪一年的文章展示出来.

在视图函数中解析URL:

def redirect_to_year(request):
    year = 2018
    return redirect(reverse('news-year-archive', args=(year,)))

给reverse的变量部分传递了一个元组,元组的唯一一个元素是year,就相当于给反向解析的URL传递了第一个位置参数year.所以redirect的URL是articles/2018/.

总结一下反向解析:

  • 没有模糊匹配的正则表达式起别名
    • 在模板中用{% url name %}来反向解析
    • 在代码中用reverse(name)来反向解析
  • 有模糊匹配,只分组,未命名的正则表达式起别名
    • 在模板中用{% url name arg… %}来反向解析
    • 在代码中用reverse(name,(arg1,…))来反向解析
  • 有模糊匹配,分组且命名的正则表达式起别名
    • 在模板中用{% url name arg… %}来反向解析,这里官方文档未提到,估计是采用名字相同的变量,然后通过视图给变量传值
    • 在代码中用reverse(name,kwargs={})来反向解析

URL命名空间

所谓URL命名空间,就是如果两个URLconf文件都有相同的别名出现,可以通过’命名空间名称:URL名称’的语法来区别两个别名,命名空间名称就是app的名称.
模板中使用:

{% url 'app01:detail' pk=12 pp=99 %}

代码中使用:

v = reverse('app01:detail', kwargs={'pk':11})

Django是如何处理URL的

作为总结,来看一下Django处理URL的顺序,修改自Github上一个Django文档的翻译:

当一个用户请求Django站点的一个页面时,Django按照下面的顺序来确定哪些代码被执行:

  1. Django首先需要确定根URLconf模块。通常它是settings.py里 ROOT_URLCONF 设置的值,但是如果传入的HttpRequest对象有一个名为URLconf的属性(通过中间件请求处理设置),那么它的值将被使用来代替 ROOT_URLCONF 设置。
  2. Django加载所有的Python模块并在其中查找urlpatterns变量。它应该是一个以函数 django.conf.urls.patterns() 返回的Python列表,其中的每一个元素是一个django.conf.urls.url实例.
  3. Django 拿着HTTP请求,从所有已经获得的URLconf配置文件中,按顺序匹配每个正则表达式,并停止于第一个成功匹配,然后调用对应的视图。
  4. 一旦匹配成功,Django给对应的视图传递如下参数:
    1. HttpRequest的一个实例
    2. 分组但未命名的正则表达式按照位置参数传入
    3. 分组且命名的正则表达式按照关键字参数传入
    4. url实例化时候附带的字典参数作为关键字传入,如果这个字典里有和分组且命名的正则表达式关键字参数重名的关键字,则会覆盖正则的参数.
  5. 没有正则表达式匹配,或者如果在这个过程中的任意点抛出一个异常,Django会调用一个适当的错误处理视图。

上边已经介绍了所有路由系统的详细.Django的路由系统非常强大,通过正则表达式模糊匹配且能够给视图函数传参数的方法,建立起类似目录结果的URL体系,并且有序的将这些URL分发到不同的视图函数进行处理.

从视图的角度来看,路由系统扩展了视图函数的处理范围.在了解路由系统之前,我们编写的视图函数仅对应一个具体的URL地址,通过路由系统,一个视图函数可以操作一批URL地址.

通过反向解析的应用,在站点内部互相跳转就可以不用写硬编码.同时可以携带参数传递给视图函数,可以略去编写带参数的GET请求,统一以URL进行标识,降低了编写模板的复杂性.
建立站点时,预先设计路由系统非常重要,相当于整个站点的目录.结构清晰的路由设计对于编写视图和模板起到事半功倍的效果.今后在写Django项目的时候,尽量用上路由系统的高级特性以减轻开发工作量.