Django入门——URL调度器

Django入门——URL调度器

前言

开新坑:Django框架
大一的时候在社团中使用来开发后端,现在整理当时的学习内容并完善,便于后续的查阅。
预计这个模块会有对Django源码的分析,根据我自己的实际情况来(有空就更),目前预计假期会抽出大量时间来阅读分析Django的源代码

什么?你问《程序员的自我修养》那个坑?I’m writing(咕咕咕

URL调度器(URL dispatcher)

本篇博客仅仅介绍在Django中url的书写和配置

博客的撰写基于我自己的使用和官方文档,有些内容还是需要阅读源码才能明白,源码分析的博客暂时处于计划中。

Django如何处理请求

doc中对处理url的描述

简单的翻译一下就是:

1.首先Django决定使用哪个urls.py文件来作为根设置,这个在settings.py中的ROOT_URLCONF中设置,往往为项目名文件夹下的urls.py,但是如果来的Http请求HTTPRequest对象中有urlconf这个属性(通常由中间件设置),他的值会被用来替代ROOT_URLCONF

2.Django加载对应文件的urlpatterns变量,从中寻找第一个url路径匹配的path或者re_path对象(Django2.2中用它们替代了url对象),装载对象里的函数,或者是基于类的函数,并且向它们传递参数:

  • HttpRequest实例
  • 如果有正则匹配到的组,若没有名字,匹配到的组作为一个位置参数返回;若有名字,则任何匹配到的参数都将传递给要装载的函数。

3.没有任何url被匹配,则调用错误处理的view。

Django中注册url

2.2中path封装了一些常用的正则组,不需要自己书写:

1
2
3
4
5
6
7
# Django2.2

path('articles/<int:year>/<int:month>/<slug:slug>', views.article_detail),

# Django 1.x

url(r'^articles/(?P<year>\[0-9]+)/(?P<month>\[0-9]+)/(?P<slug>[\w-]+)$', views.article_detail')

上述写法基本等效,我们的article_detail函数应该长这个样子:
def article_detail(request, year, month)
当前端访问了url:/articles/2005/03/时,year参数被传入2005,month参数被传入3

在2.2中,doc给出的例子里/articles/2005/03/中月份的0会被去掉,神奇,应该是做了判断

参数 匹配类型
str 匹配字符,除了”/“符
int 匹配0-9
slug 匹配特殊字符串,他们由-来连接,如:building-your-1st-django-site
path 匹配一个路径,即匹配内容包含”/“符
uuid 匹配一个UUID格式的字符串,返回一个UUID的实例

自定义正则模式

doc给出的例子就很好

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class FourDigitYearConverter:
regex = '[0-9]{4}'

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

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

from django.urls import path, register_converter

from . import converters, views

register_converter(converters.FourDigitYearConverter, 'yyyy')

urlpatterns = [
path('articles/2003/', views.special_case_2003),
path('articles/<yyyy:year>/', views.year_archive),
...
]

上面的%04d用到了格式化字符串:

可以用如下的方式,对格式进行进一步的控制:
%[(name)][flags][width].[precision]typecode
(name)为命名
flags可以有+,-,’ ‘或0。+表示右对齐。-表示左对齐。’ ‘为一个空格,表示在正数的左侧填充一个空格,从而与负数对齐。0表示使用0填充。
width表示显示宽度
precision表示小数点后精度

简单来说,我们自定义了一个yyyy的匹配类型(就想int,str那样),它匹配4位数的年。

注意:自定义的类必须包含如下内容:

  • regex:一个属性,正则匹配的字符串
  • to_python(self, value):一个方法,将匹配的内容转成你想要的类型,当无法转换时,你需要抛出一个ValueError的异常来让别的函数捕获(只允许抛出ValueError异常,别的需要自己捕获处理)
  • to_url(self, value):一个方法,把Python类型转为一个字符串用于正则匹配

re_path

re_path和老版的url基本类似,使用Python正则的语法,如(?Pxxx)表示匹配组然后可以用key获取到这个组而不是用group(x)来获取

这里只需要看一点:doc中的一个推荐规范要求

1
2
3
4
5
6
from django.urls import re_path

urlpatterns = [
re_path(r'^blog/(page-(\d+)/)?$', blog_articles), # bad
re_path(r'^comments/(?:page-(?P<page_number>\d+)/)?$', comments), # good
]

在正则中,我们用圆括号将所有选择项括起来,相邻的选择项之间用|分隔。但用圆括号会有一个副作用,使相关的匹配会被缓存,此时可用?:放在第一个选项前来消除这种副作用。

该例子中第二个就消除了page-的缓存,这个url捕获的参数仅仅只有属于整数的page_number,而第一个就捕获(缓存)了“page-”和我们需要的整数

url查找

请求的URL被看做是一个普通的Python 字符串, URLconf在其上查找并匹配。进行匹配时将不包括GET或POST请求方式的参数以及域名。

name参数

你可以把它理解为对url的注释(comment)
有了这个注释,你的所有需要输入该url的地方都可以换成这个comment
这通常使用在

  • view里的重定向中:
    1
    2
    3
    def login_page(request):
    # do something
    return redirect("hello")
  • html里涉及到的url跳转
1
2
3
4
<a href="{% url 'hello' %}">hello</a>
<form method='post' action='{% url 'hello' %}'>
......
</form>

有什么好处呢?当然有了,当你需要修改url的时候(比如网站上线等),如果所有的url都是以硬编码的方式写在你的文件里,那么修改起来十分麻烦,如果使用了name,这样就可以只修改对应path的前面的匹配即可

留意,这个name必须全局唯一,官方文档推荐的命名方式是:

Putting a prefix on your URL names, perhaps derived from the application name (such as myapp-comment instead of comment), decreases the chance of collision.

即name的命名中最好包括你的app的名字

include参数

上面使用name的方法也确实可以,但看起来不太美观,并且当你的项目需要很多的url时,你的root urlconf会十分庞大,难以维护。

最好的方式当然是分开写。

URL namespaces

就如同C++的命名空间一样,Django的app也是单独的命名空间。demo app下的一个url命名为“home”,foo app下一个url命名为“home”,他们处于不同的命名空间下,这时如果我们还想像前面一样访问,就不能简单用“home”来区分他们,需要加入命名空间

demo app 的 home url:demo:home
foo app 的 home url: foo:home

namespace也可以嵌套(nested),不过用的不多,doc里给了个例子:

The named URL ‘sports:polls:index’ would look for a pattern named ‘index’ in the namespace ‘polls’ that is itself defined within the top-level namespace ‘sports’.

这样我们每个app维护一个urls.py文件,项目的路由就被合理的分开了

include + namespace

但外部访问的url只匹配root urlconf对应的文件的urlpattern,这时候我们就需要include来把app的urls.py包括进来。

官方文档的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# urls.py
from django.urls import include, path

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

# polls/urls.py

from django.urls import path

from . import views

app_name = 'polls'
urlpatterns = [
path('home/', views.home, name='home'),
...
]

这时,我们要想访问home,就需要访问”/poll/home/“这个url,即所有被include的url都需要在最前面多一段 include 的调用者url路径才可以访问的到

使用include同时指定namespace

urls.py
1
2
3
4
5
6
7
from django.urls import include, path

from demo.views import home_page

urlpatterns = [
path('demo/', include(("demo.urls", "demo"), namespace="demo")),
]

namespace的名字并非必须与app的名字保持一致,但我比较喜欢这么做,一目了然(老版本好像必须一致)。

需要注意2.2和老版本不同的是,include的第一个参数为元组,不仅需要指定include的url而且要指出app的名字,不然会报错。

include

当然include也不一定要跨文件来引用别的url,同一个urlpattern下也可以使用include来将包含同一段url的合在一起

1
2
3
4
5
6
7
8
9
10
11
12
from django.urls import include, path

from demo.views import home_page

urlpatterns = [
path('demo/', include(("demo.urls", "demo"), namespace="demo")),
path('test/', include([
path('test1', home_page),
path('test2', home_page),
]))
]

doc中给了另一种用法,也很有参考意义,比上面的更加清晰规范

1
2
3
4
5
6
7
8
9
10
11
12
from django.urls import include, path

from . import views

polls_patterns = ([
path('', views.IndexView.as_view(), name='index'),
path('<int:pk>/', views.DetailView.as_view(), name='detail'),
], 'polls')

urlpatterns = [
path('polls/', include(polls_patterns)),
]

views.IndexView.as_view()是基于类的视图的写法

kwargs—hock

你可以在path里面加入字典形式的参数,这些被称为“钩子”(hock)(老版doc里这么叫,新版好像不这么说了)

目前我还没发现有啥用…… -_-||

path无include的情况

urls.py
1
2
3
4
5
6
from django.urls import path
from . import views

urlpatterns = [
path('blog/<int:year>/', views.year_archive, {'foo': 'bar'}),
]

这样相当于我们给了year_archive()函数一个我们指定的参数:foo,它的值为’bar’,当url为“/blog/2005/”,Django会调用views.year_archive(request, year=2005, foo='bar')

path里有include的情况

基本能猜到,就是被包含的所有url里都有这个额外参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
from django.urls import include, path

urlpatterns = [
path('blog/', include('inner'), {'blog_id': 3}),
]

# inner.py
from django.urls import path
from mysite import views

urlpatterns = [
path('archive/', views.archive),
path('about/', views.about),
]

# 下面的写法和上面等效

from django.urls import include, path
from mysite import views

urlpatterns = [
path('blog/', include('inner')),
]

# inner.py
from django.urls import path

urlpatterns = [
path('archive/', views.archive, {'blog_id': 3}),
path('about/', views.about, {'blog_id': 3}),
]

后记

  • 看doc基本看得一知半解,更清晰的理解需要认真阅读代码,我挺期待自己空下时间来阅读阅读大神的代码,但我太忙了
  • 我太难了,我也不知道自己在忙啥,就是贼忙
  • 现在的中心大部分还在学校课时上,我尽力保障每周1-2更
  • 《程序员的自我修养》看得比较慢,有点难……
Author

Ctwo

Posted on

2019-10-15

Updated on

2020-10-25

Licensed under

Comments