URL调度器¶
对于高质量的Web 应用来说,使用简洁、优雅的URL 模式是一个非常值得重视的细节。Django 允许你自由地设计你的URL,不受框架束缚。
参见万维网的发明者Berners-Lee 的 Cool URIs don't change,里面有关于为什么URL 应该保持整洁和有意义的卓越论证。
概况¶
为了给一个应用设计URL,你需要创建一个Python 模块,通常被称为**URLconf**(URL configuration)。这个模块是纯粹的Python 代码,包含URL 模式(简单的正则表达式)到Python 函数(你的视图)的简单映射。
映射可短可长,随便你。它可以引用其它的映射。而且,因为它是纯粹的Python 代码,它可以动态构造。
Django 还提供根据当前语言翻译URL 的一种方法。更多信息参见 国际化文档。
Django 如何处理一个请求¶
当一个用户请求Django 站点的一个页面,下面是Django 系统决定执行哪个Python 代码使用的算法:
- Django 确定使用根 URLconf 模块。通常,这是
ROOT_URLCONF
设置的值,但如果传入HttpRequest
对象拥有urlconf
属性(通过中间件设置),它的值将被用来代替ROOT_URLCONF
设置。 - Django 加载该 Python 模块并寻找可用的
urlpatterns
。它是django.urls.path()
和(或)django.urls.re_path()
实例的序列(sequence)。 - Django 依次匹配每个URL 模式,在与请求的URL 匹配的第一个模式停下来。
- 一旦有 URL 匹配成功,Djagno 导入并调用相关的视图,这个视图是一个简单的 Python 函数(或基于类的视图 class-based view )。视图会获得如下参数:
- 一个
HttpRequest
实例。 - 如果匹配的 URL返回没有命名组,那么来自正则表达式中的匹配项将作为位置参数提供。
- 关键字参数由路径表达式匹配的任何命名部分组成,并由
django.urls.path()
或django.urls.re_path()
的可选kwargs
参数中指定的任何参数覆盖。
- 一个
- 如果没有 URL 被匹配,或者匹配过程中出现了异常,Django 会调用一个适当的错误处理视图。参加下面的错误处理( Error handling )。
例如¶
下面是一个简单的 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),
]
注意:
- 要从 URL 中取值,使用尖括号。
- 捕获的值可以选择性地包含转换器类型。比如,使用
<int:name>
来捕获整型参数。如果不包含转换器,则会匹配除了/
外的任何字符。 - 这里不需要添加反斜杠,因为每个 URL 都有。比如,应该是
articles
而不是/articles
。
一些请求的例子:
/articles/2005/03/
会匹配 URL 列表中的第三项。Django 会调用函数views.month_archive(request, year=2005, month=3)
。/articles/2003/
would match the first pattern in the list, not the second one, because the patterns are tested in order, and the first one is the first test to pass. Feel free to exploit the ordering to insert special cases like this. Here, Django would call the functionviews.special_case_2003(request)
/articles/2003
would not match any of these patterns, because each pattern requires that the URL end with a slash./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
被解释为不匹配,因此向用户发送404响应。to_url(self, 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
在 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),
]
这实现了与前面示例大致相同的功能,除了:
- 将要匹配的 URLs 将稍受限制。比如,10000 年将不在匹配,因为 year 被限制长度为4。
- 无论正则表达式进行哪种匹配,每个捕获的参数都作为字符串发送到视图。
当从使用 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-(\d+)/)?$', blog_articles), # bad
re_path(r'^comments/(?:page-(?P<page_number>\d+)/)?$', 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。
根据经验,只有当正则表达式需要一个参数但视图忽略它时,才捕捉该视图需要的值并使用非捕捉参数。
URLconf 在什么上查找¶
请求的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``参数。
性能¶
Each regular expression in a urlpatterns
is compiled the first time it's
accessed. This makes the system blazingly fast.
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
.
包含其它的URLconfs¶
在任何时候,你的 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 中的每一个视图接受你传递的额外选项时有用。
URL 的反向解析¶
在 Django 项目中,一个常见需求是获取最终形式的 URL,比如用于嵌入生成的内容中(视图和资源网址,给用户展示网址等)或用户服务器端的导航处理(重定向等)。
强烈建议不要硬编码 URL(这是一个费力、不能扩展、容易出错的主意)。同样危险的是设计临时机制来生成的 URL 与URLconf描述的设计的URL一样,这会导致 URL 随着时间的推移变得过时。
换句话说,需要的是 DRY 机制。除其他优势外,它还允许 URL 设计自动更新,而不必遍历所有项目代码来搜索和替换过时的 URL 。
我们用来获取 URL 的首要信息是负责处理它的视图的标识(例如名称)。必须参与查找正确网址的其他信息是视图参数的类型(位置、关键字)和值。
Django 提供了一个解决方案,使得 URL 映射是 URL 设计唯一的仓库。你使用 URLconf 来填充它,然后可以双向使用它:
- 从用户/浏览器请求的 URL 开始,它调用正确的Django视图,并从 URL 中提取它的参数需要的值。
- 从相应的 Django 视图标识以及要传递给它的参数来获取相关联的 URL 。
第一条我们在前面的章节以及讨论过。第二条就是所谓的 反向解析 URL *,*反向 URL 匹配,反向 URL 查找,或简称 URL 反向。
Django 提供执行反转 URL 的工具,这些工具与需要 URL 的不同层匹配:
- 在模板里:使用
url
模板标签。 - 在 Python 编码:使用
reverse()
函数。 - 在与 Django 模型实例的 URL 处理相关的高级代码中:
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 中的条目即可。
In some scenarios where views are of a generic nature, a many-to-one relationship might exist between URLs and views. For these cases the view name isn't a good enough identifier for it when comes the time of reversing URLs. Read the next section to know about the solution Django provides for this.
Naming URL patterns¶
In order to perform URL reversing, you'll need to use named URL patterns as done in the examples above. The string used for the URL name can contain any characters you like. You are not restricted to valid Python names.
When naming URL patterns, choose names that are unlikely to clash with other
applications' choice of names. If you call your URL pattern comment
and another application does the same thing, the URL that
reverse()
finds depends on whichever pattern is last in
your project's urlpatterns
list.
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.
You can deliberately choose the same URL name as another application if you
want to override a view. For example, a common use case is to override the
LoginView
. Parts of Django and most
third-party apps assume that this view has a URL pattern with the name
login
. If you have a custom login view and give its URL the name login
,
reverse()
will find your custom view as long as it's in
urlpatterns
after django.contrib.auth.urls
is included (if that's
included at all).
You may also use the same name for multiple URL patterns if they differ in
their arguments. In addition to the URL name, reverse()
matches the number of arguments and the names of the keyword arguments.
URL namespaces¶
介绍¶
URL namespaces allow you to uniquely reverse named URL patterns even if different applications use the same URL names. It's a good practice for third-party apps to always use namespaced URLs (as we did in the tutorial). Similarly, it also allows you to reverse URLs if multiple instances of an application are deployed. In other words, since multiple instances of a single application will share named URLs, namespaces provide a way to tell these named URLs apart.
Django applications that make proper use of URL namespacing can be deployed more
than once for a particular site. For example django.contrib.admin
has an
AdminSite
class which allows you to easily
deploy more than one instance of the admin.
In a later example, we'll discuss the idea of deploying the polls application
from the tutorial in two different locations so we can serve the same
functionality to two different audiences (authors and publishers).
A URL namespace comes in two parts, both of which are strings:
- application namespace
- This describes the name of the application that is being deployed. Every
instance of a single application will have the same application namespace.
For example, Django's admin application has the somewhat predictable
application namespace of
'admin'
. - instance namespace
- This identifies a specific instance of an application. Instance namespaces
should be unique across your entire project. However, an instance namespace
can be the same as the application namespace. This is used to specify a
default instance of an application. For example, the default Django admin
instance has an instance namespace of
'admin'
.
Namespaced URLs are specified using the ':'
operator. For example, the main
index page of the admin application is referenced using 'admin:index'
. This
indicates a namespace of 'admin'
, and a named URL of 'index'
.
Namespaces can also be nested. 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'
.
Reversing namespaced URLs¶
When given a namespaced URL (e.g. 'polls:index'
) to resolve, Django splits
the fully qualified name into parts and then tries the following lookup:
First, Django looks for a matching application namespace (in this example,
'polls'
). This will yield a list of instances of that application.If there is a current application defined, Django finds and returns the URL resolver for that instance. The current application can be specified with the
current_app
argument to thereverse()
function.The
url
template tag uses the namespace of the currently resolved view as the current application in aRequestContext
. You can override this default by setting the current application on therequest.current_app
attribute.If there is no current application, Django looks for a default application instance. The default application instance is the instance that has an instance namespace matching the application namespace (in this example, an instance of
polls
called'polls'
).If there is no default application instance, Django will pick the last deployed instance of the application, whatever its instance name may be.
If the provided namespace doesn't match an application namespace in step 1, Django will attempt a direct lookup of the namespace as an instance namespace.
If there are nested namespaces, these steps are repeated for each part of the namespace until only the view name is unresolved. The view name will then be resolved into a URL in the namespace that has been found.
例如¶
To show this resolution strategy in action, consider an example of two instances
of the polls
application from the tutorial: one called 'author-polls'
and one called 'publisher-polls'
. Assume we have enhanced that application
so that it takes the instance namespace into consideration when creating and
displaying polls.
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')),
]
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'),
...
]
Using this setup, the following lookups are possible:
If one of the instances is current - say, if we were rendering the detail page in the instance
'author-polls'
-'polls:index'
will resolve to the index page of the'author-polls'
instance; i.e. both of the following will result in"/author-polls/"
.In the method of a class-based view:
reverse('polls:index', current_app=self.request.resolver_match.namespace)
and in the template:
{% url 'polls:index' %}
If there is no current instance - say, if we were rendering a page somewhere else on the site -
'polls:index'
will resolve to the last registered instance ofpolls
. Since there is no default instance (instance namespace of'polls'
), the last instance ofpolls
that is registered will be used. This would be'publisher-polls'
since it's declared last in theurlpatterns
.'author-polls:index'
will always resolve to the index page of the instance'author-polls'
(and likewise for'publisher-polls'
) .
If there were also a default instance - i.e., an instance named 'polls'
-
the only change from above would be in the case where there is no current
instance (the second item in the list above). In this case 'polls:index'
would resolve to the index page of the default instance instead of the instance
declared last in urlpatterns
.
URL namespaces and included URLconfs¶
Application namespaces of included URLconfs can be specified in two ways.
Firstly, you can set an app_name
attribute in the included URLconf module,
at the same level as the urlpatterns
attribute. You have to pass the actual
module, or a string reference to the module, to include()
,
not the list of urlpatterns
itself.
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'),
...
]
from django.urls import include, path
urlpatterns = [
path('polls/', include('polls.urls')),
]
The URLs defined in polls.urls
will have an application namespace 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)),
]
This will include the nominated URL patterns into the given application namespace.
The instance namespace can be specified using the namespace
argument to
include()
. If the instance namespace is not specified,
it will default to the included URLconf's application namespace. This means
it will also be the default instance for that namespace.