A clean, elegant URL scheme is an important detail in a high-quality web application. Django lets you design URLs however you want, with no framework limitations.
参见万维网的发明者 Berners-Lee 的 Cool URIs don't change,里面有关于为什么URL 应该保持整洁和有意义的卓越论证。
为了给一个应用设计 URL,你需要创建一个 Python 模块,通常被称为 URLconf (URL configuration)。这个模块是纯粹的 Python 代码,包含 URL 模式(简单的正则表达式)到 Python 函数(你的视图)的简单映射。
映射可短可长,随便你。它可以引用其它的映射。而且,因为它是纯粹的 Python 代码,它可以动态构造。
Django 还提供根据当前语言翻译 URL 的一种方法。更多信息参见 国际化文档。
当一个用户请求 Django 站点的一个页面,下面是 Django 系统决定执行哪个 Python 代码使用的算法:
ROOT_URLCONF
设置的值,但如果传入 HttpRequest
对象拥有 urlconf
属性(通过中间件设置),它的值将被用来代替 ROOT_URLCONF
设置。urlpatterns
。它是 django.urls.path()
和(或) django.urls.re_path()
实例的序列(sequence)。path_info
匹配。HttpRequest
实例。django.urls.path()
或 django.urls.re_path()
的可选 kwargs
参数中指定的任何参数覆盖。下面是一个简单的 URLconf:
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),
]
注意:
<int:name>
来捕获整型参数。如果不包含转换器,则会匹配除了 /
外的任何字符。articles
而不是 /articles
。一些请求的例子:
/articles/2005/03/
会匹配 URL 列表中的第三项。Django 会调用函数 views.month_archive(request, year=2005, month=3)
。/articles/2003/
将匹配列表中的第一个模式不是第二个,因为模式按顺序匹配,第一个会首先测试是否匹配。请像这样自由插入一些特殊的情况来探测匹配的次序。在这里 Django 会调用函数 views.special_case_2003(request)
/articles/2003
不匹配任何一个模式,因为每个模式要求 URL 以一个斜线结尾。/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
。返回一个 UUID
实例。path
- 匹配非空字段,包括路径分隔符 '/'
。它允许你匹配完整的 URL 路径而不是像 str
那样匹配 URL 的一部分。对于更复杂的匹配需求,你能定义你自己的路径转换器。
转换器是一个类,包含如下内容:
regex
类属性。to_python(self, value)
方法,用来处理匹配的字符串转换为传递到函数的类型。如果没有转换为给定的值,它应该会引发 ValueError
。ValueError
说明没有匹配成功,因此除非另一个 URL 模式匹配成功,否则会向用户发送404响应。to_url(self, value)
方法,它将处理 Python 类型转换为字符串以用于 URL 中。如果不能转换给定的值,它应该引发 ValueError
。ValueError
被解释为无匹配项,因此 reverse()
将引发 NoReverseMatch
,除非有其他 URL 模式匹配。例如:
class FourDigitYearConverter:
regex = "[0-9]{4}"
def to_python(self, value):
return int(value)
def to_url(self, value):
return "%04d" % value
在 URLconf 中使用 register_converter()
来注册自定义的转换器类:
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),
...,
]
如果路径和转化器语法不能很好的定义你的 URL 模式,你可以可以使用正则表达式。如果要这样做,请使用 re_path()
而不是 path()
。
在 Python 正则表达式中,命名正则表达式组的语法是 (?P<name>pattern)
,其中 name
是组名,pattern
是要匹配的模式。
这里是先前 URLconf 的一些例子,现在用正则表达式重写一下:
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()
(反之亦然),要特别注意,视图参数类型可能发生变化,你可能需要调整你的视图。
还有命名组语法,例如 (?P<year>[0-9]{4})
,你也可以使用更短的未命名组,例如 ([0-9]{4})
。
不是特别推荐这个用法,因为它会更容易在匹配的预期含义和视图参数之间引发错误。
在任何情况下,推荐在给定的正则表达式里只使用一个样式。当混杂两种样式时,任何未命名的组都会被忽略,而且只有命名的组才会传递给视图函数。
正则表达式允许嵌套参数,Django 将处理它们并传递给视图。当转换时,Django 将试着填充给所有外部捕捉参数,忽略任何嵌套捕捉参数。考虑下面可选的带有页面参数的 URL 模式:
from django.urls import re_path
urlpatterns = [
re_path(r"^blog/(page-([0-9]+)/)?$", blog_articles), # bad
re_path(r"^comments/(?:page-(?P<page_number>[0-9]+)/)?$", comments), # good
]
两个模式使用嵌套参数,并处理:例如, blog/page-2/
将匹配给 blog_articles
并带有2个参数:page-2/
和 2
。第二个模式为 comments
匹配 comments/page-2/
并带有设置为2的关键参数 page_number
。这个案例里的外部参数是一个非捕捉参数 (?:...)
。
blog_articles
视图需要反转最外层捕捉的参数,page-2/
或在这里不需要参数,而 comments
可以在没有参数或 page_number
值的情况下反转。
嵌套捕捉参数在视图参数和 URL 直接创建一个强耦合,如 blog_articles
所示:视图接收部分 URL (page-2/
) 而不只是视图要的值。当反转时这种耦合更明显,因为反转视图我们需要传递一段 URL 而不是 page number。
根据经验,只有当正则表达式需要一个参数但视图忽略它时,才捕捉该视图需要的值并使用非捕捉参数。
请求的URL被看做是一个普通的Python 字符串, URLconf在其上查找并匹配。进行匹配时将不包括GET或POST请求方式的参数以及域名。
例如, https://www.example.com/myapp/
请求中,URLconf 将查找 myapp/
在 https://www.example.com/myapp/?page=3
请求中,URLconf 仍将查找 myapp/
。
URLconf 不检查使用了哪种请求方法。换句话讲,所有的请求方法 —— 即,对同一个URL的无论是 POST请求
、 GET请求
、或 HEAD
请求方法等等 —— 都将路由到相同的函数。
有一个方便的小技巧是指定视图参数的默认值。 下面是一个URLconf 和视图的示例:
# 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 processes regular expressions in the urlpatterns
list which is
compiled the first time it's accessed. Subsequent requests use the cached
configuration via the URL resolver.
urlpatterns
变量的语法¶urlpatterns 应该是 path()
或 re_path()
实例的序列。
当 Django 找不到所匹配的请求 URL 时,或引发了异常时,Django 会调用一个错误处理视图。
这些情况发生时使用的视图通过4个变量指定。它们的默认值应该满足大部分项目,但是通过赋值给它们以进一步的自定义也是可以的。
完整的细节请参见 自定义错误视图 。
这些值得在你的根URLconf 中设置。在其它URLconf 中设置这些变量将不会生效果。
它们的值必须是可调用的或者是表示视图的Python 完整导入路径的字符串,可以方便地调用它们来处理错误情况。
这些值是:
handler400
-- 查看 django.conf.urls.handler400
.handler403
-- 查看 django.conf.urls.handler403
.handler404
-- 查看 django.conf.urls.handler404
.handler500
-- 查看 django.conf.urls.handler500
.在任何时候,你的 urlpatterns
都可以 "include" 其它URLconf 模块。这实际上将一部分URL 放置于其它URL 下面。
例如,下面是URLconf Django website 自己的URLconf 中一个片段。它包含许多其它URLconf:
from django.urls import include, path
urlpatterns = [
# ... snip ...
path("community/", include("aggregator.urls")),
path("contact/", include("contact.urls")),
# ... snip ...
]
每当 Django 遇到 include()
,它会将匹配到该点的URLconf的任何部分切掉,并将剩余的字符串发送到包含的URLconf进行进一步处理。
另一种可能性是通过使用 path()
实例的列表来包含其他 URL 模式。比如,看这个 URLconf:
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)),
]
在这个例子中, /credit/reports/
URL将被 credit.views.report()
这个Django 视图处理。
这种方法可以用来去除URLconf 中的冗余,其中某个模式前缀被重复使用。例如,考虑这个URLconf:
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),
]
我们可以改进它,通过只声明共同的路径前缀一次并将后面的部分分组:
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 捕获的任何参数,所以下面的例子是合法的:
# 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()
函数可带有可选的第三参数(必须是字典),传递到视图函数里。
例如:
from django.urls import path
from . import views
urlpatterns = [
path("blog/<int:year>/", views.year_archive, {"foo": "bar"}),
]
在这个例子里,当请求到 /blog/2005/
时,Django 将调用 views.year_archive(request, year=2005, foo='bar')
。
在 syndication framework 中使用了这个办法,来向视图传递元数据和可选参数。
处理冲突
可能有一个 URL 模式来捕捉命名的关键字参数,并且同时也在附加参数字典里传递了同名的参数。当发生这种情况时,将使用字典里的参数来替代捕捉的参数。
include()
¶同样的,你可以额外其他选项给 include()
,并且已包含的 URLconf 里的每一行将被传递额外选项。
例如,下面两个 URLconf 配置在功能上是相同的:
配置一:
# main.py
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),
]
配置二:
# main.py
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}),
]
注意额外的选项会一直传递给所包含的 URLconf 的每一行,不管视图是否接受这些额外选项。因此,这个技巧仅在确定所包含的 URLconf 中的每一个视图接受你传递的额外选项时有用。
在 Django 项目中,一个常见需求是获取最终形式的 URL,比如用于嵌入生成的内容中(视图和资源网址,给用户展示网址等)或用户服务器端的导航处理(重定向等)。
强烈建议不要硬编码 URL(这是一个费力、不能扩展、容易出错的主意)。同样危险的是设计临时机制来生成的 URL 与URLconf描述的设计的URL一样,这会导致 URL 随着时间的推移变得过时。
换句话说,需要的是 DRY 机制。除其他优势外,它还允许 URL 设计自动更新,而不必遍历所有项目代码来搜索和替换过时的 URL 。
我们用来获取 URL 的首要信息是负责处理它的视图的标识(例如名称)。必须参与查找正确网址的其他信息是视图参数的类型(位置、关键字)和值。
Django 提供了一个解决方案,使得 URL 映射是 URL 设计唯一的仓库。你使用 URLconf 来填充它,然后可以双向使用它:
第一条我们在前面的章节以及讨论过。第二条就是所谓的 反向解析 URL *,*反向 URL 匹配,反向 URL 查找,或简称 URL 反向。
Django 提供执行反转 URL 的工具,这些工具与需要 URL 的不同层匹配:
url
模板标签。reverse()
函数。get_absolute_url()
方法。再次考虑这个 URLconf 条目:
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>/
。
你可以使用以下方式在模板代码中来获取它们:
<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 代码里:
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 中的条目即可。
在一些视图具有一般性质的场景下,URLs 和视图存在多对一关系。对于这些情况,当反转 URLs 时,视图名并不是一个足够好的标识符。阅读下一节来了解 Django 如何解决这一问题。
为了完成反向解析 URL ,你需要像上面那样使用 命名 URL 模式 。用于命名 URL 的字符串可以包含任意字符,并不仅限于 Python 里合法的命名。
当命名 URL 模式时,请选择名称与其他应用名称冲突的名字。如果你命名的 URL 模式 comment
和其他应用程序做了同样的事情,reverse()
查询出的 URL 会是在项目的 urlpatterns
列表里靠后的那个。
在 URL 名称前加入前缀,可以来自app名称(比如 myapp-comment
而不是 comment
),这样可以减少冲突。
如果你想覆盖某个视图,你可以有意选择一些和应用程序 相同的URL名。比如,一个常见的案例是覆盖 LoginView
。Django 的部分和绝大多数第三方 app 假设这个视图有名叫 login
的 URL 模式。如果你有名叫 login
的自定义登录视图,reverse()
将会在 django.contrib.auth.urls
之后寻找你的自定义视图。
如果多个 URL 模式的参数不同,也可以使用相同的名称。除了 URL 名称外, reverse()
会匹配参数的数量和关键字参数的名称。路径转换器也可以引发 ValueError
表示不匹配,详见 注册自定义的路径转换器。
URL 命名空间允许你使用唯一的反向命名URL模式( named URL patterns ),即便不同应用程序使用相同的 URL 名称。对于第三方应用程序来说,始终使用 URL 命名空间是个好习惯(像在教程里说的那样)。同样,如果已部署了应用程序的多个实例,它也允许你反向解析 URL。换句话说,因为单个应用的多个实例会分享已命名的 URL,命名空间提供了区分这些已命名 URL 的方法。
对于特定站点,正确使用URL名称空间的Django应用程序可以部署多次。比如 django.contrib.admin
有一个 AdminSite
,它允许admin实例部署多次(deploy more than one instance of the admin)。在稍后的例子里,我们将从教程里的两个不同位置讨论投票程序的部署,以便我们可以提供相同的函数给两个不同的群体(作者和出版社)。
URL 命名空间分为两部分,它们都是字符串:
'admin'
。'admin'
的实例命名空间。被指定的命名空间 URL 使用 ':'
操作符。比如,使用 'admin:index'
引用admin 应用的首页。这表明命名空间为 'admin'
,命名 URL 为 'index'
。
命名空间也可以嵌套。命名 URL 'sports:polls:index'
将在命名空间 'polls'
中寻找命名为 'index'
的模式,该模式是在顶层命名空间 'sports'
中定义的。
当给定一个命名空间 URL(例如 'polls:index'
)解析时,Django 会将完全限定的名称拆分成多个部分,然后尝试下面的查询:
首先,Django 查找匹配 application namespace (这个例子里是 'polls'
)。这将产生应用实例列表。
如果定义了当前应用程序,Django 会为这个实例查找并返回 URL 解析器。可以用 reverse()
函数的 current_app
参数来指定当前应用程序。
url
模板标签使用当前已解析的视图命名空间当作 RequestContext
中的应用程序。你可以通过设置在 request.current_app
属性上的当前应用来覆盖这个默认配置。
如果当前没有应用程序,Django 会寻找默认的应用实例。默认应用程序实例是具有与实例命名空间匹配的应用程序命名空间的实例(比如, polls
实例被称为 'polls'
)。
如果没有默认的应用程序实例,Django 将会引用最后一次部署的应用程序实例,无论其实例命名是什么。
如果提供的命名空间无法在第一步里匹配应用程序命名空间( application namespace ),Django 会尝试直接寻找命名空间来作为实例命名空间( instance namespace )。
如果有嵌套的命名空间,则会对命名空间的每个部分重复这些步骤,直到视图名不被解析为止,然后视图名称将被解析为已找到的命名空间中的一个 URL 。
为了展示这个解决策略的实际作用,请考虑教程里 polls
应用程序的两个实例案例:分别被称为 'author-polls'
和 'publisher-polls'
。假设我们已经增强了这个应用程序,以便会在创建和显示 polls 时考虑实例命名空间
urls.py
¶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
¶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/"
。
在基于类的视图里的方法:
reverse("polls:index", current_app=self.request.resolver_match.namespace)
以及在模板中:
{% url 'polls:index' %}
如果其中一个实例是最新的 - 例如,如果我们在站点某处渲染一个页面 - 'polls:index'
将被解析为 polls
的最后一个注册实例。因为这里没有默认实例( 'polls'
的实例命名空间),所以将使用 polls
的最后一个注册实例。这将是 'publisher-polls'
,因为它是在 urlpatterns
的最后面声明的。
'author-polls:index'
会一直被解析为实例 'author-polls'
的首页(对于 'publisher-polls'
同样如此)。
如果还有一个默认实例 - 例如,一个叫 'polls'
的实例 - 唯一的变化就是没有当前实例(上面列表中的第二项)。在这个例子 'polls:index'
将解析为默认实例的首页而不是在 urlpatterns
中最后声明的实例。
有两种办法指定包含的URLconfs应用程序空间。
首先,你可以在包含的 URLcon 模块中设置一个 app_name
属性,在相同层作为 urlpatterns
属性。你必须传递实际的模块或对该模块的一个字符串引用传递给 include()
,而不是 urlpatterns
本身的列表。
polls/urls.py
¶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
¶from django.urls import include, path
urlpatterns = [
path("polls/", include("polls.urls")),
]
polls.urls
里的 URLs 定义将具有应用程序命名空间 polls
。
Secondly, you can include an object that contains embedded namespace data. If
you include()
a list of path()
or
re_path()
instances, the URLs contained in that object
will be added to the global namespace. However, you can also include()
a
2-tuple containing:
(<list of path()/re_path() instances>, <application namespace>)
例如:
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 的应用程序命名空间。这意味着它也将成为那个命名空间的默认实例。
12月 05, 2023