URL调度器

一个干净、优雅的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如何处理一个请求

当用户向你的Django站点请求一个页面时,系统会采用一个算法来确定要执行哪一段Python代码:

  1. 首先,Django会使用根路由解析模块(root URLconf)来解析路由。 通常,这是ROOT_URLCONF设置的值,但是如果传入的HttpRequest对象具有urlconf属性(由中间件设置)那么ROOT_URLCONF的设置将被其替换。
  2. Django加载该Python模块并查找变量urlpatterns 它应该是django.urls.path()或者django.urls.re_path()实例的Python列表。
  3. Django按顺序遍历每个URL pattern,并在第一个匹配的请求URL被匹配时停下。
  4. 一旦某个URL pattern成功匹配,Django会导入并调用给定的视图,该视图是一个简单的Python函数(或基于类的视图)。 这个视图会被传以以下参数:
    • 一个 HttpRequest的实例。
    • 如果所匹配的正则表达式返回的是若干个无名组,那么该正则表达式所匹配的内容将被作为位置参数提供给该视图。
    • 关键字参数是由路径表达式匹配的任何指定部件组成的,在可选的kwargs参数中指定的任何参数覆盖到django.urls.path()django.urls.re_path()
  5. 如果请求的URL没有匹配到任何一个表达式,或者在匹配过程的任何时刻抛出了一个异常,那么Django 将调用适当的错误处理视图进行处理。 参考下方的 错误处理

示例¶ T0>

下面是一个简单的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都有前导斜杠,所以使用时,每个URL没有必要添加一个/。 例如, path函数中URL部分应该使用 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),
]

这完成了与前面的例子大致相同的事情,除了:

  • 匹配的确切URL会受到一些限制。 例如,年份10000将不再匹配,因为年份整数限制为四位长。
  • 无论正则表达式匹配什么类型,每个捕获的参数都会以字符串形式传给视图。

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搜索什么内容

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()函数将使用其默认参数,设置num1 如果第二个模式匹配, page()将使用捕获的num值。

性能

urlpatterns中的每个正则表达式会在第一次访问时进行编译。 这使得系统非常快速。

urlpatterns变量​​的语法

urlpatterns应该是path()和/或re_path()实例的Python列表。

错误处理

当Django无法为请求的URL找到匹配项或者引发异常时,Django会调用错误处理视图。

用于这些情况的视图由四个变量指定。 他们的默认值应该足以满足大多数项目,但是可以通过覆盖其默认值进一步定制。

有关详细信息,请参阅自定义错误视图中的文档。

这些值可以在您的根URLconf中设置。 在其他URLconf中设置这些变量将不起作用。

值必须是可调用的,或者是表示应该调用以处理当前错误情况的视图的完整Python导入路径的字符串。

这些变量是:

包含其他URLconf

任何时候,你的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。

传递额外参数到view functions

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中捕获的参数。

Passing extra options to 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.

URL的反向解析

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:

  • Starting with a URL requested by the user/browser, it calls the right Django view providing any arguments it might need with their values as extracted from the URL.
  • Starting with the identification of the corresponding Django view plus the values of arguments that would be passed to it, obtain the associated URL.

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:

  • In templates: Using the url template tag.
  • In Python code: Using the reverse() function.
  • In higher level code related to handling of URLs of Django model instances: The get_absolute_url() method.

Examples

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.

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

Introduction

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:

  1. First, Django looks for a matching application namespace (in this example, 'polls'). This will yield a list of instances of that application.

  2. 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.

  3. 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').

  4. If there is no default application instance, Django will pick the last deployed instance of the application, whatever its instance name may be.

  5. 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.

Example

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.

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

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.

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.

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

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.