Django 系列博客(六)

前言

本篇博客介绍 Django 中的路由控制部分,一个网络请求首先到达的就是路由这部分,经过路由与视图层的映射关系再执行相应的代码逻辑并将结果返回给客户端。

Django 中路由的作用

URL 配置(URLconf)可以比作是 Django 支撑网站的目录。它的本质是 URL 要为该 URL 滴啊用的视图函数之间的映射表。以这种方式告诉 Django,对于客户端发来的 URL 要具体调用视图层的哪段代码。

from django.urls import url
from app import views

urlpatterns = [
    url(r'^$', views.home),
]
# ^$这个路由对应视图函数中的 home 方法,只要浏览器往该网址发送请求,就会响应到这个函数执行。

简单的路由配置

from django.conf.urls import url

urlpatterns = [
    url(正则表达式, views视图函数, 默认参数, 路由别名),
]
  • 正则表达式:一个正则表达式字符串;
  • views 视图函数:一个可调用对象,通常为一个视图函数或一个指定视图路径的字符串;
  • 默认参数:可选的要传递给视图函数的默认参数(字典形式);
  • 路由别名:一个可选的name 参数。
from django.conf.urls import url
from app import views

urlpatterns = [
    url(r'^admin/', admin.site.urls),
    url(r'^article/(\d{4})/(\d{2})', views.article),
    url(r'^user/(?P<name>[a-z]{3})', views.user_havename),
    url(r'^user/$', views.user),
]

注意:

  • 若要从 URL 中捕获一个值,只需要在它周围放置一对圆括号;
  • 不需要添加一个前导的反斜杠,因为每个 url 都有。例如,应该是^articles而不是^/articles
  • 每个正则表达式前面的r是可选的,但是建议加上,表示这是个原生字符串,字符串中的任何字符都不应该转义。
  • urlpatterns 中的元素按照书写顺序从上往下逐一匹配正则表达式,一旦匹配成功则不再继续。
一些请求的例子:

/articles/2005/03/ 请求将匹配列表中的第三个模式。Django 将调用函数views.month_archive(request, '2005', '03')。
/articles/2005/3/ 不匹配任何URL 模式,因为列表中的第三个模式要求月份应该是两个数字。
/articles/2003/ 将匹配列表中的第一个模式不是第二个,因为模式按顺序匹配,第一个会首先测试是否匹配。请像这样自由插入一些特殊的情况来探测匹配的次序。
/articles/2003 不匹配任何一个模式,因为每个模式要求URL 以一个反斜线结尾。
/articles/2003/03/03/ 将匹配最后一个模式。Django 将调用函数views.article_detail(request, '2003', '03', '03')。

APPEND_SLASH

该参数的作用是会在访问连接的时候,如果连接没有加斜杠,会自动加上。可以在global_settings.py中修改。

项目的 settings 中没有改配置的选项,一个项目会有两个 settings。

效果:

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

urlpatterns = [
    url(r'^blog/$', views.blog),
]

当我们访问http://www.example.com/blog时,默认将网址自动替换成http://www.example.com/blog/。如果将该配置值设为false,此时在向该网址发送请求时就会提示找不到页面。

分组

当我们需要捕获 url 中的参数并要传递给视图函数时,有两种捕获方式:无名分组和有名分组。无名分组就是该值没有变量标识,传值的时候采用位置参数传递;有名分组就是给捕获的值赋值一个变量,这样就可以通过关键字参数传值了。

无名分组

# urls.py文件
from django.conf.urls import url
from app_01 import views

urlpatterns = [
    url(r'^article/(\d{4})/(\d{2})', views.article),
]
# views.py文件
from django.shortcuts import HttpResponse

def article(reques, year, month):
    return HttpResponse('您要查看%s 年%s 月的文章' %(year, month))

通过圆括号捕获年份和月份,然后通过位置参数传递给 year 和 month 变量,接着在 views 函数中使用。

有名分组

import re
ret=re.search('(?P<year>[0-9]{4})/([0-9]{2})','2012/12')
print(ret.group())
print(ret.group(1))
print(ret.group(2))
print(ret.group('year'))

这些示例使用简单的、没有命名的正则表达式组(通过圆括号)来捕获 URL 中的值并以位置参数传递给视图。在更高级的用法中,可以使用命名的正则表达式组来捕获 URL 中的值并以关键子参数传递给视图函数。

在python 正则表达式中,命名有名分组的语法时(P<变量名>pattern),其中变量名是分组的标识符,pattern 是要匹配的正则表达式。下面是上面 URLconf 使用有名分组的重写:

from django.urls import path, re_path
from app_01 import views

urlpatterns = [
    re_path(r'^articles/2003/$', views.special_case_2003),
    re_path(r'^articles/(?P<year>[0-9]{4})/$', views.year_archive),
    re_path(r'^articles/(?P<year[0-9]{4})/(?P<month>[0-9]{2})/$', views.month_archive),
    re_path(r'^articles/(?P<year[0-9]{4})/(?P<month>[0-9]{2})/(?P<day>[0-9]{2})/$', views.archive_detail),
]
# 捕获到的数据都是 str 类型
# 视图函数里可以指定默认值
url('blog/$', views.blog),
url('blog/?(?P<num>[0-9]{1})', views.blog),
def blog(request, num=1):
    print(num)
    return HttpResponse('ok')

这个实现与前面的示例完全相同,只有一个细微的差别:捕获的值作为关键字参数而不是位置参数传递给视图函数。例如:

/articles/2005/03/ 请求将调用views.month_archive(request, year='2005', month='03')函数,而不是views.month_archive(request, '2005', '03')。
    /articles/2003/03/03/ 请求将调用函数views.article_detail(request, year='2003', month='03', day='03')。

在实际应用中,这意味你的 URLconf 会更加清晰且不容易产生参数顺序问题的错误,可以在视图函数定义中重新安排参数的顺序。

路由分发

其实和项目名相同的文件夹下面的urls.py文件是整个项目的根路由:

Django 系列博客(六)-LMLPHP

这是整个项目的根路由,所有向该项目发送的连接请求,首先需要从该路由配置里面过滤,如果只有一个应用,或者路由配置不多一个根路由就足够了,但当 app 多起来之后还是使用一个根路由配置会造成路由混乱,所以有了路由分发,比如这是发往app_01应用的连接,那么在根路由中进行路由分发,把连接转向app_01中的路由中进行处理,这就是路由分发。

# 根路由配置
from django.conf.urls import url, include
from app_01 import views
from app_01 import urls

urlpatterns = [
    url(r'^app_01/', include('app_01.urls')), # 注意前面的正则表达式后面不能加$
    url(r'^app_01/', include(urls)),
]
# 应用路由配置
from django.conf.urls import url
from app_01 import views

urlpatterns = [
    url(r'^test/(?P<year>[0-9]{2})/$', views.url_test),
]

反向解析

在使用 Django 项目时,一个常见的需求是获得 URL 的最终形式,以用于嵌入到生成的内容中(视图中和显示给用户的 URL 等)或者用于处理服务器端的导航(重定向等)。人们强烈希望不要硬编码这些 URL(费时费力、不可扩展且易产生错误)或者设计一种与 URLconf 毫不相关的专门的 URL 生成机制,因为这样容易导致一定程度上产生过期的 URL。

在需要 URL 的地方,对于不同层级,Django 提供不同的工具用于 URL 反查:

  • 在模板中:使用 url 模板标签;
  • 在 python 代码中:使用from django.urls import reverse
# urls.py文件
from django.urls import path, re_path
from app_01 import views

urlpatterns = [
    re_path(r'^test/(?P<year>[0-9]{2})/(P?<month>[0-9]{2})/$', views.url_test, name='test'),
]
<!-- html文件 -->
<a href="{% url 'test' 10 23 %}">哈哈</a>
# views.py文件
from django.shortcuts import render, HttpResponse, redirect, reverse

def url_test(request, year, month):
    url = reverse('test', args=(10, 20))
    return HttpResponse('ok')

总结:

  • 在 html 代码里面使用{% url '别名' 参数 参数... %}
  • 在视图函数中:
    • url = reverse('test')
    • url = reverse('test', args=(10, 20))

当命名 URL 模式时,要确保使用的名称不会与其他应用中的名称产生冲突。如果你的 URL 模式叫做 comment,而另外一个应用中也有一个同样的名字,当你在模板中使用这个名称的时候不能保证将插入哪个 URL。在 URL 名称加上一个前缀,比如应用的名称,将会减少冲突的可能。建议使用myapp-comment

名称空间

命名空间(namespace)是表示标识符的可见范围。一个标识符可在多个命名空间中定义,它在不同命名空间中的含义是互不相干的。这样在一个新的命名空间中可以定义任何标识符,它们不会与任何已有的标识符产生冲突,因为已有的定义处于其他的命名空间中。

由于name没有作用域,Django 在返解 URL 时,会在项目的全局路径中按顺序搜索,当查找到第一个name指定的 URL 时,立即返回。

在开发项目时,会经常使用name属性反解出 URL,当不小心在不同的 app 的 urls 中定义相同的name时,可能会导致 URL 反解错误,为了避免发生引入了命名空间。

创建 app01和 app02

python manage.py startapp app01

python manage.py startapp app02

根路由

from django.urls import path,re_path,include

urlpatterns = [
    path('app01/', include('app01.urls')),
    path('app02/', include('app02.urls'))
]

app01的 urls.py

from django.urls import path,re_path
from app01 import views

urlpatterns = [
    re_path(r'index/',views.index,name='index'),
]

app02的 urls.py

from django.urls import path, re_path, include
from app02 import views

urlpatterns = [
    re_path(r'index/', views.index,name='index'),
]

app01的视图

def index(request):
    url=reverse('index')
    print(url)
    return HttpResponse('index app01')

app02的视图

def index(request):
    url=reverse('index')
    print(url)
    return HttpResponse('index app02')

这样都找index.html,app01和 app02找到的都是app02的 index。如何处理?在路由分发的时候指定名称空间。根路由在路由分发是,指定名称空间。

# 一
path('app01/', include(('app01.urls','app01'))),
path('app02/', include(('app02.urls','app02')))

# 二
url(r'app01/',include('app01.urls',namespace='app01')),
url(r'app02/',include('app02.urls',namespace='app02'))

# 三
url(r'app01/',include(('app01.urls','app01'))),
url(r'app02/',include(('app02.urls','app02')))

在视图函数反向解析的时候,指定名称空间:

url=reverse('app02:index')
print(url)
url2=reverse('app01:index')
print(url2)

在模板里面也是用相应的名称空间名:

<a href="{% url 'app02:index'%}">哈哈</a>

Django2.0版的 path

Django2.0的re_path和1.0的url一样。2.0多了个path

思考情况如下:

urlpatterns = [
    re_path('articles/(?P<year>[0-9]{4})/', year_archive),
    re_path('article/(?P<article_id>[a-zA-Z0-9]+)/detail/', detail_view),
    re_path('articles/(?P<article_id>[a-zA-Z0-9]+)/edit/', edit_view),
    re_path('articles/(?P<article_id>[a-zA-Z0-9]+)/delete/', delete_view),
]

考虑下这样的两个问题:

第一个问题,函数 year_archive 中year参数是字符串类型的,因此需要先转化为整数类型的变量值,当然year=int(year) 不会有诸如如TypeError或者ValueError的异常。那么有没有一种方法,在url中,使得这一转化步骤可以由Django自动完成?

第二个问题,三个路由中article_id都是同样的正则表达式,但是你需要写三遍,当之后article_id规则改变后,需要同时修改三处代码,那么有没有一种方法,只需修改一处即可?

在Django2.0中,可以使用 path 解决以上的两个问题。

基本示例

from django.urls import path
from . import views
urlpatterns = [
    path('articles/2003/', views.special_case_2003),
    path('articles/<int:year>/', views.year_archive),
    path('articles/<int:year>/<int:month>/', views.month_archive),
    path('articles/<int:year>/<int:month>/<slug>/', views.article_detail),
  # path才支持,re_path不支持
  path('order/<int:year>',views.order),
]

基本规则:

  • 使用尖括号(<>)从 url中捕获值;
  • 捕获值中可以包含一个转化器类型(converter type),比如使用<int:name>捕获一个整数变量。如果没有转化器,将匹配任何字符串,也包含/字符。
  • 无需添加前导斜杠

以上是示例,分别和上面的基本示例对应:

Django 系列博客(六)-LMLPHP

path 转换器

  • str:匹配除了路径分隔符(/)之外的非空字符串,这是默认的形式;
  • int:匹配正整数,包含0;
  • slug:匹配字母、数字、横杠以及下划线组成的字符串;
  • uuid:匹配格式化的uuid,如075194d3-6885-417e-a8a8-6c931e272f00。
  • path: 匹配任何非空字符,包含了路径分隔符(/)

注册自定义转化器

对于一些复杂或者复用的需要,可以定义自己的转化器。转化器是一个类或接口,它的要求有三点:

  • regex类属性,字符串类型;
  • to_python(self, value)方法,value 是由类属性 regex 所匹配到的字符串,返回具体的 python 变量值,以供 Django 传递到对应的视图函数中;
  • to_url(self, value)方法,和to_python相反,value 是一个具体的 python 变量值,返回其字符串,通常用于 url 反向引用。

示例

class FourDigitYearConverter:
    regex = '[0-9]{4}'

    def to_python(self, value):
        return int(value)

    def to_url(self, value):
        return '%04d' % value

使用register_converter将其注册到 URL 配置中:

from django.urls import register_converter, path
from . import converters, views

register_converter(converters.FourDigitYearConverter, 'yyyy')

urlpatterns = [
    path('articles/2003/', views.spwcial_case_2003),
    path('articles/<yyyy:year>/', views.year_archive),
]
01-10 00:43