django的使用3-URL调度器

[TOC]

Django 允许你自由地设计你的URL,不受框架束缚。

概况

对于高质量的Web 应用来说,使用简洁、优雅的URL 模式是一个非常值得重视的细节。

为了给一个应用设计URL,你需要创建一个Python 模块,通常被称为URLconf(URL configuration)。这个模块是纯粹的Python 代码,包含URL 模式(简单的正则表达式)到Python 函数(你的视图)的简单映射。

映射可短可长,随便你。它可以引用其它的映射。而且,因为它是纯粹的Python 代码,它可以动态构造。

Django 如何处理一个请求

当一个用户请求Django 站点的一个页面,下面是Django 系统决定执行哪个Python 代码使用的算法:

  1. Django 确定使用根 URLconf 模块。通常,这是 ROOT_URLCONF 设置的值,但如果传入 HttpRequest 对象拥有 urlconf 属性(通过中间件设置),它的值将被用来代替 ROOT_URLCONF 设置。
  2. Django 加载该 Python 模块并寻找可用的 urlpatterns 。它是 django.urls.path() 和(或) django.urls.re_path() 实例的序列(sequence)。
  3. Django 会按顺序遍历每个 URL 模式,然后会在所请求的URL匹配到第一个模式后停止,并与 path_info 匹配。
  4. 一旦有 URL 匹配成功,Djagno 导入并调用相关的视图,这个视图是一个Python 函数(或基于类的视图 class-based view )。视图会获得如下参数:
    • 一个 HttpRequest 实例。
    • 如果匹配的 URL 包含未命名组,那么来自正则表达式中的匹配项将作为位置参数提供。
    • 关键字参数由路径表达式匹配的任何命名部分组成,并由 django.urls.path() 或 django.urls.re_path() 的可选 kwargs 参数中指定的任何参数覆盖。
  5. 如果没有 URL 被匹配,或者匹配过程中出现了异常,Django 会调用一个适当的错误处理视图。

实例

下面是一个简单的 URLconf:

1
2
3
4
5
6
7
8
9
10
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:slug>/', views.article_detail),
]

注意:

  • 要从 URL 中取值,使用尖括号。
  • 捕获的值可以选择性地包含转换器类型。比如,使用 <int:name> 来捕获整型参数。如果不包含转换器,则会匹配除了 / 外的任何字符。
  • 这里不需要添加反斜杠,因为每个 URL 都有。比如,应该是 articles 而不是 /articles

一些请求的例子:

  • /articles/2005/03/ 会匹配 URL 列表中的第三项。Django 会调用函数 views.month_archive(request, year=2005, month=3)
  • /articles/2003/ 会匹配 URL 列表中的第一项。Django 会调用函数 views.special_case_2003(request)
  • /articles/2003 没有匹配项,因为没有/结尾。
  • /articles/2003/03/building-a-django-site/ 会匹配 URL 列表中的最后一项。Django 会调用函数 views.article_detail(request, year=2003, month=3, slug="building-a-django-site")

路径转换器

下面的路径转换器在默认情况下是有效的:

  • str - 匹配除了 '/' 之外的非空字符串。如果表达式内不包含转换器,则会默认匹配字符串。
  • int - 匹配 0 或任何正整数。返回一个 int
  • slug - 匹配任意由 ASCII 字母或数字以及连字符和下划线组成的短标签。比如,building-your-1st-django-site
  • uuid - 匹配一个格式化的 UUID 。为了防止多个 URL 映射到同一个页面,必须包含破折号并且字符都为小写。比如,075194d3-6885-417e-a8a8-6c931e272f00
  • path - 匹配非空字段,包括路径分隔符 '/' 。它允许你匹配完整的 URL 路径而不是像 str 那样匹配 URL 的一部分。

使用正则表达式

如果路径和转化器语法不能很好的定义你的 URL 模式,你可以可以使用正则表达式。如果要这样做,请使用 re_path() 而不是 path() 。

在 Python 正则表达式中,命名正则表达式组的语法是 (?P<name>pattern) ,其中 name 是组名,pattern 是要匹配的模式。

这里是先前 URLconf 的一些例子,现在用正则表达式重写一下:

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

from . import views

urlpatterns = [
path('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<slug>[\w-]+)/$', views.article_detail),
]

当从使用 path() 切换到 re_path() (反之亦然),视图参数类型可能发生变化,可能需要调整你的视图。

URLconf查找

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

指定视图参数的默认值

有一个方便的小技巧是指定视图参数的默认值。 下面是一个URLconf 和视图的示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# URLconf
from django.urls import path

from . import views

urlpatterns = [
path('blog/', views.page),
path('blog/page<int:num>/', views.page),
]

# View (in blog/views.py)
def page(request, num=1):
# Output the appropriate page of blog entries, according to num.
...

在上面的例子中,两个URL模式都指向了相同的视图—— views.page 但是第一个样式不能在URL中捕获到任意东西。如果第一个URL模式去匹配URL,page() 函数会使用它默认参数 num=1。如果第二个URL模式去匹配URL,page() 函数都会使用捕获到的任意 num参数。

错误处理

当 Django 找不到所匹配的请求 URL 时,或引发了异常时,Django 会调用一个错误处理视图。

这些情况发生时使用的视图通过4个变量指定。它们的默认值应该满足大部分项目,但是通过赋值给它们以进一步的自定义也是可以的。

这些值得在你的根URLconf 中设置。在其它URLconf 中设置这些变量将不会生效果。

它们的值必须是可调用的或者是表示视图的Python 完整导入路径的字符串,可以方便地调用它们来处理错误情况。

这些值是:

  • handler400
  • handler403
  • handler404
  • handler500

包含其它的URLconfs

在任何时候,你的 urlpatterns 都可以 “include” 其它URLconf 模块。这实际上将一部分URL 放置于其它URL 下面。

例如:

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

from apps.main import views as main_views
from credit import views as credit_views

extra_patterns = [
path('reports/', credit_views.report),
path('reports/<int:id>/', credit_views.report),
path('charge/', credit_views.charge),
]

urlpatterns = [
path('', main_views.homepage),
path('help/', include('apps.help.urls')),
path('credit/', include(extra_patterns)),
]

每当 Django 遇到 include() ,它会将匹配到该点的URLconf的任何部分切掉,并将剩余的字符串发送到包含的URLconf进行进一步处理。

在这个例子中, /credit/reports/ URL将被 credit.views.report() 这个Django 视图处理。

这种方法可以用来去除URLconf 中的冗余,其中某个模式前缀被重复使用。例如,考虑这个URLconf:

1
2
3
4
5
6
7
8
9
from django.urls import path
from . import views

urlpatterns = [
path('<page_slug>-<page_id>/history/', views.history),
path('<page_slug>-<page_id>/edit/', views.edit),
path('<page_slug>-<page_id>/discuss/', views.discuss),
path('<page_slug>-<page_id>/permissions/', views.permissions),
]

我们可以改进它,通过只声明共同的路径前缀一次并将后面的部分分组:

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

urlpatterns = [
path('<page_slug>-<page_id>/', include([
path('history/', views.history),
path('edit/', views.edit),
path('discuss/', views.discuss),
path('permissions/', views.permissions),
])),
]

捕获的参数

被包含的URLconf 会收到来自父URLconf 捕获的任何参数,所以下面的例子是合法的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# In settings/urls/main.py
from django.urls import include, path

urlpatterns = [
path('<username>/blog/', include('foo.urls.blog')),
]

# In foo/urls/blog.py
from django.urls import path
from . import views

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

在上面的例子中,捕获的 "username" 变量将被如期传递给include()指向的URLconf。

传递额外选项给视图函数

URLconfs 有钩子来允许你把其他参数作为 Python 字典来传递给视图函数。path() 函数可带有可选的第三参数(必须是字典),传递到视图函数里。

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

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

URL 的反向解析

在 Django 项目中,一个常见需求是获取最终形式的 URL,比如用于嵌入生成的内容中(视图和资源网址,给用户展示网址等)或用户服务器端的导航处理(重定向等)。

强烈建议不要硬编码 URL。

Django 提供执行反转 URL 的工具,这些工具与需要 URL 的不同层匹配:

  • 在模板里:使用 url模板标签。
  • 在 Python 编码:使用 reverse() 函数。
  • 在与 Django 模型实例的 URL 处理相关的高级代码中: get_absolute_url() 方法。

示例

再次考虑这个 URLconf 条目:

1
2
3
4
5
6
7
8
9
from django.urls import path

from . import views

urlpatterns = [
#...
path('articles/<int:year>/', views.year_archive, name='news-year-archive'),
#...
]

根据这个设计,与 year nnnn 相对应的 URL 是 /articles/<nnnn>/

你可以使用以下方式在模板代码中来获取它们:

1
2
3
4
5
6
7
<a href="{% url 'news-year-archive' 2012 %}">2012 Archive</a>
{# Or with the year in a template context variable: #}
<ul>
{% for yearvar in year_list %}
<li><a href="{% url 'news-year-archive' yearvar %}">{{ yearvar }} Archive</a></li>
{% endfor %}
</ul>

或在 Python 代码里:

1
2
3
4
5
6
7
8
from django.http import HttpResponseRedirect
from django.urls import reverse

def redirect_to_year(request):
# ...
year = 2006
# ...
return HttpResponseRedirect(reverse('news-year-archive', args=(year,)))

因为某些原因,如果决定改变每年已发布的文章存档内容的 URL ,你只需要改变 URLconf 中的条目即可。

命名 URL 模式

为了完成反向解析 URL ,你需要像上面那样使用 ** 命名 URL 模式 ** 。用于命名 URL 的字符串可以包含任意字符,并不仅限于 Python 里合法的命名。

当命名 URL 模式时,请选择名称与其他应用名称冲突的名字。如果你命名的 URL 模式 comment 和其他应用程序做了同样的事情,reverse()查询出的 URL 会是在项目的 urlpatterns 列表里靠后的那个。

在 URL 名称前加入前缀,可以来自app名称(比如 myapp-comment 而不是 comment ),这样可以减少冲突。

URL 命名空间

介绍

URL 命名空间允许你使用唯一的反向命名URL模式,即便不同应用程序使用相同的 URL 名称。同样,如果已部署了应用程序的多个实例,它也允许你反向解析 URL。换句话说,因为单个应用的多个实例会分享已命名的 URL,命名空间提供了区分这些已命名 URL 的方法。

URL 命名空间分为两部分,它们都是字符串:

  • 应用程序命名空间

    这描述了正在部署的程序名。单个应用的每个实例拥有相同的命名空间。比如,Django admin 应用有可预测的应用命名空间 'admin'

  • 实例命名空间

    这标识了应用程序的特定实例。实例命名空间应该是完整项目唯一的。但是实例命名空间可以和应用命名空间相同。这常用来指定应用的默认实例。比如,默认Django admin 实例拥有名为 'admin' 的实例命名空间。

被指定的命名空间 URL 使用 ':' 操作符。比如,使用 'admin:index' 引用admin 应用的首页。这表明命名空间为 'admin' ,命名 URL 为'index'

命名空间也可以嵌套。命名 URL 'sports:polls:index' 将在命名空间 'polls' 中寻找命名为 'index' 的模式,该模式是在顶层命名空间 'sports' 中定义的。

反向命名空间

当给定一个命名空间 URL(例如 'polls:index' )解析时,Django 会将完全限定的名称拆分成多个部分,然后尝试下面的查询:

  1. 首先,Django 查找匹配 application namespace(这个例子里是 'polls' )。这将产生应用实例列表。

  2. 如果定义了当前应用程序,Django 会为这个实例查找并返回 URL 解析器。可以用 reverse() 函数的 current_app 参数来指定当前应用程序。

    url 模板标签使用当前已解析的视图命名空间当作 RequestContext 中的应用程序。你可以通过设置在 request.current_app 属性上的当前应用来覆盖这个默认配置。

  3. 如果当前没有应用程序,Django 会寻找默认的应用实例。默认应用程序实例是具有与实例命名空间匹配的应用程序命名空间的实例(比如, polls 实例被称为 'polls' )。

  4. 如果没有默认的应用程序实例,Django 将会引用最后一次部署的应用程序实例,无论其实例命名是什么。

  5. 如果提供的命名空间无法在第一步里匹配应用程序命名空间( application namespace ),Django 会尝试直接寻找命名空间来作为实例命名空间( instance namespace )。

如果有嵌套的命名空间,则会对命名空间的每个部分重复这些步骤,直到视图名不被解析为止,然后视图名称将被解析为已找到的命名空间中的一个 URL 。

例如

为了展示这个解决策略的实际作用,请考虑教程里 polls 应用程序的两个实例案例:分别被称为 'author-polls''publisher-polls' 。假设我们已经增强了这个应用程序,以便会在创建和显示 polls 时考虑实例命名空间

urls.py

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

urlpatterns = [
path('author-polls/', include('polls.urls', namespace='author-polls')),
path('publisher-polls/', include('polls.urls', namespace='publisher-polls')),
]

polls/urls.py

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

from . import views

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

使用这步后,可以进行以下查找:

  • 如果其中一个实例是最新的- 例如,如果我们在实例 'author-polls' 中渲染详情页 - 'polls:index' 将解析为 'author-polls' 的首页;比如下面两种都将触发 "/author-polls/"

    在基于类的视图里的方法:

    1
    reverse('polls:index', current_app=self.request.resolver_match.namespace)

    以及在模板中:

    1
    {% url 'polls:index' %}
  • 如果其中一个实例是最新的 - 例如,如果我们在站点某处渲染一个页面 - 'polls:index' 将被解析为 polls 的最后一个注册实例。因为这里没有默认实例( 'polls' 的实例命名空间),所以将使用 polls 的最后一个注册实例。这将是 'publisher-polls' ,因为它是在 urlpatterns 的最后面声明的。

  • 'author-polls:index' 会一直被解析为实例 'author-polls' 的首页(对于 'publisher-polls' 同样如此)。

如果还有一个默认实例 - 例如,一个叫 'polls' 的实例 - 唯一的变化就是没有当前实例(上面列表中的第二项)。在这个例子 'polls:index' 将解析为默认实例的首页而不是在 urlpatterns 中最后声明的实例。

URL 命名空间和包含的 URLconfs

有两种办法指定包含的URLconfs应用程序空间。

首先,你可以在包含的 URLcon 模块中设置一个 app_name 属性,在相同层作为 urlpatterns 属性。你必须传递实际的模块或对该模块的一个字符串引用传递给 include() ,而不是 urlpatterns 本身的列表。

polls/urls.py

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

from . import views

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

urls.py

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

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

polls.urls 里的 URLs 定义将具有应用程序命名空间 polls

其次,你可以包括一个包含嵌入式命名空间数据的对象。如果你 include() 了一个 path()re_path() 实例的列表,那个对象里包含的 URLs 将被添加到全局命名空间内。但是,你也可以 include() 一个包含以下内容的2元组:

1
(<list of path()/re_path() instances>, <application namespace>)

例如:

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)),
]

这会将指定的 URL 模式包含到给定的应用程序命名空间里。

使用 include()namespace 参数来指定实例命名空间。如果实例命名空间没有被指定,会默认已被导入的 URLconf 的应用程序命名空间。这意味着它也将成为那个命名空间的默认实例。

参考: