一个干净、优雅的URL方案是高质量Web应用程序中的一个重要细节。 Django 让你随心所欲设计你的URL,不受框架束缚。
不要求有 .php
或 .cgi
,更不会要求类似0,2097,1-1-1928,00
这样无意义的东西。
参见万维网的发明者Berners-Lee 的Cool URIs don’t change,里面有关于为什么URL 应该保持整洁和有意义的卓越论证。
为了给一个应用设计URL,你需要创建一个Python模块,通常称为路由选择模块或路由解析模块URLconf(URL configuration)。 该模块是一个纯粹的Python模块,是 URL路径表达式 到 Python 函数(你的视图)之间的映射。
根据你的需要,这个映射可短可长。 它也可以引用其它的映射。 而且,由于它是纯粹的Python代码,因此可以动态构建它。
Django还提供了根据活动语言(active language)翻译URL的方法。 更多信息请参考国际化文档
当用户向你的Django站点请求一个页面时,系统会采用一个算法来确定要执行哪一段Python代码:
ROOT_URLCONF
设置的值,但是如果传入的HttpRequest
对象具有urlconf
属性(由中间件设置)那么ROOT_URLCONF
的设置将被其替换。urlpatterns
。 它应该是django.urls.path()
或者django.urls.re_path()
实例的Python列表。HttpRequest
的实例。kwargs
参数中指定的任何参数覆盖到django.urls.path()
或django.urls.re_path()
。下面是一个简单的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/
的请求将与列表中的第三个条目匹配。 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/
会匹配到最后一个模式。 Django会调用函数views.article_detail(request, year=2003, month=3, slug ="building-a-django-site")
。以下路径转换器默认可用:
str
- 匹配除了路径分隔符'/'
的任意非空字符串。
如果表达式中没有包含转换器,那么这将是默认行为。int
- 匹配0或任意正整数。 并作为 int 返回。slug
- 匹配任意的黏接字符串(slug string),这些黏接字符串是ASCII的字母或数字,词与词之间由连字符或下划线黏接组成。
例如, building-your-1st-django-site
。uuid
- 匹配一个格式化的 UUID. 为了防止多个URL映射到同一页面,必须包含多个破折号(dash),同时字母必须小写。 例如, 075194d3-6885-417e-a8a8-6c931e272f00
. 返回一个 UUID
实例。path
- 匹配包含路径分隔符 '/'
在内的任意非空字符串。 相对于str
,这允许你匹配一个完整的URL路径,而不仅仅是URL路径的一部分。为了处理更为复杂的匹配需求,你可以定义属于你自己的路径转换器。
一个转换器的class定义需要包含以下信息:
正则表达式
属性。to_python(self, value)
方法,负责将匹配的字符串转换成python类型的数据,处理的结果会传给视图的相关方法。 当类型转换失败时,这个方法应该抛出ValueError
异常。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
使用register_converter()
方法在你的URLconf配置文件中注册自定义的转换器:
from django.urls import register_converter, path
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 pattern,你还可以使用正则表达式。 为了使用正则表达式,请使用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('articles/(?P<year>[0-9]{4})/', views.year_archive),
re_path('articles/(?P<year>[0-9]{4})/(?P<month>[0-9]{2})/', views.month_archive),
re_path('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将尝试填充所有外部捕获的参数,而忽略任何嵌套的捕获参数。 Consider the following URL patterns which optionally take a page argument:
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
产生两个有位置顺序的参数的匹配:page-2/
和2
。 第二个模式comments
将会匹配comments/page-2/
并以page_number
为关键字,设置其为2。 这个例子中的外层参数(?:...)
是一个非捕获参数的用法。
The blog_articles
view needs the outermost captured argument to be reversed,
page-2/
or no arguments in this case, while comments
can be reversed
with either no arguments or a value for page_number
.
嵌套捕获的参数在视图参数和URL之间创建了一个强大的耦合,如blog_articles
所示:视图接收部分URL(page-2/
)作为参数,而不仅仅是视图感兴趣的值。 这种耦合在反转时更为明显,因为要反转视图,我们需要传递一段URL而不是页码。
作为一个经验法则,只去捕获视图所需要使用的值,并在正则表达式需要参数而视图需要忽略它时,使用非捕获参数语法。
URLconf把请求的URL作为普通的Python字符串来搜索。 所以在URL检索时,不包括GET或POST参数或域名。
例如,在https://www.example.com/myapp/
的请求中,URLconf将查找myapp/
。
在https://www.example.com/myapp/?page=3
的请求中,URLconf将查找myapp/
。
URLconf不关注请求的方法。 换句话说,所有的请求方法 - POST
, GET
, HEAD
等都将被路由到相同URL的同一个函数。
一个方便的技巧是为视图的参数指定默认参数。 下面是URLconf和view的例子:
# 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中捕获任何内容。 如果第一个模式匹配,则page()
函数将使用其默认参数,设置num
为1
。 如果第二个模式匹配, page()
将使用捕获的num
值。
urlpatterns
中的每个正则表达式会在第一次访问时进行编译。 这使得系统非常快速。
当Django无法为请求的URL找到匹配项或者引发异常时,Django会调用错误处理视图。
用于这些情况的视图由四个变量指定。 他们的默认值应该足以满足大多数项目,但是可以通过覆盖其默认值进一步定制。
有关详细信息,请参阅自定义错误视图中的文档。
这些值可以在您的根URLconf中设置。 在其他URLconf中设置这些变量将不起作用。
值必须是可调用的,或者是表示应该调用以处理当前错误情况的视图的完整Python导入路径的字符串。
这些变量是:
handler400
- 请参阅django.conf.urls.handler400
。handler403
- 请参阅django.conf.urls.handler403
。handler404
- 请参阅django.conf.urls.handler404
。handler500
- 请参阅django.conf.urls.handler500
。任何时候,你的urlpatterns
都可以“包含”其他URLconf模块。 这实质上是将一组其他的url“根植于”我们的URLconf之下。
例如,以下是Django网站本身的URLconf摘录。 它包含许多其他URLconf:
from django.urls import include, path
urlpatterns = [
# ... snip ...
path('community/', include('aggregator.urls')),
path('contact/', include('contact.urls')),
# ... snip ...
]
每当Django遇到include()
时,它会去掉URL中匹配的部分,并将剩余的字符串发送到包含的URLconf以供进一步处理。
另一种用法是通过使用添加path()
实例的列表来添加额外的其他URL模式到urlpatterns中。 例如,看这个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()
视图处理。
这可以用于移除URL配置中前缀重复的部分。 例如,看这个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"
变量传递给被包含的URLconf。
URLconfs有一个钩子,允许您将额外的参数作为Python字典传递给视图函数。
path()
函数可以采用可选的第三个参数,该参数应该是传递给视图函数的额外关键字参数的字典。
For example:
from django.urls import path
from . import views
urlpatterns = [
path('blog/<int:year>/', views.year_archive, {'foo': 'bar'}),
]
In this example, for a request to /blog/2005/
, Django will call
views.year_archive(request, year=2005, foo='bar')
.
此技术在syndication framework中用于将元数据和选项传递给视图。
Dealing with conflicts
可以使用URL模式捕获命名关键字参数,并在其额外参数字典中传递具有相同名称的参数。 发生这种情况时,将使用字典中的参数而不是URL中捕获的参数。
include()
¶同样,您可以将额外的选项传递给include()
,并且包含的URLconf中的每一行都将传递额外的选项。
例如,这两个URLconf集在功能上是相同的:
Set one:
# 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),
]
Set two:
# 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}),
]
Note that extra options will always be passed to every line in the included URLconf, regardless of whether the line’s view actually accepts those options as valid. For this reason, this technique is only useful if you’re certain that every view in the included URLconf accepts the extra options you’re passing.
A common need when working on a Django project is the possibility to obtain URLs in their final forms either for embedding in generated content (views and assets URLs, URLs shown to the user, etc.) or for handling of the navigation flow on the server side (redirections, etc.)
It is strongly desirable to avoid hard-coding these URLs (a laborious, non-scalable and error-prone strategy). Equally dangerous is devising ad-hoc mechanisms to generate URLs that are parallel to the design described by the URLconf, which can result in the production of URLs that become stale over time.
In other words, what’s needed is a DRY mechanism. Among other advantages it would allow evolution of the URL design without having to go over all the project source code to search and replace outdated URLs.
The primary piece of information we have available to get a URL is an identification (e.g. the name) of the view in charge of handling it. Other pieces of information that necessarily must participate in the lookup of the right URL are the types (positional, keyword) and values of the view arguments.
Django provides a solution such that the URL mapper is the only repository of the URL design. You feed it with your URLconf and then it can be used in both directions:
The first one is the usage we’ve been discussing in the previous sections. The second one is what is known as reverse resolution of URLs, reverse URL matching, reverse URL lookup, or simply URL reversing.
Django provides tools for performing URL reversing that match the different layers where URLs are needed:
url
template tag.reverse()
function.get_absolute_url()
method.Consider again this URLconf entry:
from django.urls import path
from . import views
urlpatterns = [
#...
path('articles/<int:year>/', views.year_archive, name='news-year-archive'),
#...
]
According to this design, the URL for the archive corresponding to year nnnn
is /articles/<nnnn>/
.
You can obtain these in template code by using:
<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>
Or in Python code:
from django.urls import reverse
from django.http import HttpResponseRedirect
def redirect_to_year(request):
# ...
year = 2006
# ...
return HttpResponseRedirect(reverse('news-year-archive', args=(year,)))
If, for some reason, it was decided that the URLs where content for yearly article archives are published at should be changed then you would only need to change the entry in the 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.
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 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:
'admin'
.'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'
.
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 the reverse()
function.
The url
template tag uses the namespace of the currently resolved
view as the current application in a
RequestContext
. You can override this default by
setting the current application on the request.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 of polls
. Since there is no default instance
(instance namespace of 'polls'
), the last instance of polls
that is
registered will be used. This would be 'publisher-polls'
since it’s
declared last in the urlpatterns
.
'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
.
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>)
For example:
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.
Jan 17, 2018