对于高质量的Web 应用来说,使用简洁、优雅的URL 模式是一个非常值得重视的细节。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 还提供根据当前语言翻译URL 的一种方法。更多信息参见国际化文档。
当一个用户请求Django 站点的一个页面,下面是Django 系统决定执行哪个Python 代码遵循的算法:
下面是一个简单的 URLconf:
from django.conf.urls import url
from . import views
urlpatterns = [
url(r'^articles/2003/$', views.special_case_2003),
url(r'^articles/([0-9]{4})/$', views.year_archive),
url(r'^articles/([0-9]{4})/([0-9]{2})/$', views.month_archive),
url(r'^articles/([0-9]{4})/([0-9]{2})/([0-9]+)/$', views.article_detail),
]
注:
一些请求的例子:
上面的示例使用简单的、没有命名的正则表达式组(通过圆括号)来捕获URL 中的值并以位置 参数传递给视图。在更高级的用法中,可以使用命名的正则表达式组来捕获URL 中的值并以关键字 参数传递给视图。
在Python 正则表达式中,命名正则表达式组的语法是(?P<name>pattern),其中name 是组的名称,pattern 是要匹配的模式。
下面是以上URLconf 使用命名组的重写:
from django.conf.urls import url
from . import views
urlpatterns = [
url(r'^articles/2003/$', views.special_case_2003),
url(r'^articles/(?P<year>[0-9]{4})/$', views.year_archive),
url(r'^articles/(?P<year>[0-9]{4})/(?P<month>[0-9]{2})/$', views.month_archive),
url(r'^articles/(?P<year>[0-9]{4})/(?P<month>[0-9]{2})/(?P<day>[0-9]{2})/$', views.article_detail),
]
这个实现与前面的示例完全相同,只有一个细微的差别:捕获的值作为关键字参数而不是位置参数传递给视图函数。例如:
在实际应用中,这意味你的URLconf 会更加明晰且不容易产生参数顺序问题的错误 —— 你可以在你的视图函数定义中重新安排参数的顺序。当然,这些好处是以简洁为代价;conf
下面是URLconf 解析器使用的算法,针对正则表达式中的命名组和非命名组:
根据传递额外的选项给视图函数(下文),这两种情况下,多余的关键字参数也将传递给视图。
请求的URL被看做是一个普通的Python 字符串, URLconf在其上查找并匹配。进行匹配时将不包括GET或POST请求方式的参数以及域名。
例如,http://www.example.com/myapp/请求中,URLconf 将查找myapp/。
在http://www.example.com/myapp/?page=3 请求中,URLconf 仍将查找myapp/。
URLconf 不检查使用了哪种请求方法。换句话讲,所有的请求方法 —— 即,对同一个URL的无论是POST请求、GET请求、或HEAD请求方法等等 —— 都将路由到相同的函数。
每个捕获的参数都作为一个普通的Python 字符串传递给视图,无论正则表达式使用的是什么匹配方式。例如,下面这行URLconf 中:
url(r'^articles/(?P<year>[0-9]{4})/$', views.year_archive),
... views.year_archive() 的year 参数将是一个字符串,即使[0-9]{4} 值匹配整数字符串。
有一个方便的小技巧是指定视图参数的默认值。 下面是一个URLconf 和视图的示例:
# URLconf
from django.conf.urls import url
from . import views
urlpatterns = [
url(r'^blog/$', views.page),
url(r'^blog/page(?P<num>[0-9]+)/$', 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 将调用一个错误处理视图。
这些情况发生时使用的视图通过4个变量指定。它们的默认值应该满足大部分项目,但是通过赋值给它们以进一步的自定义也是可以的。
完整的细节请参见自定义错误视图。
这些值可以在你的根URLconf 中设置。在其它URLconf 中设置这些变量将不会产生效果。
它们的值必须是可调用的或者是表示视图的Python 完整导入路径的字符串,可以方便地调用它们来处理错误情况。
这些值是:
在任何时候,你的urlpatterns 都可以包含其它URLconf 模块。这实际上将一部分URL 放置于其它URL 下面。
例如,下面是 Django 网站自己的URLconf 中一个片段。它包含许多其它URLconf:
from django.conf.urls import include, url
urlpatterns = [
# ... snip ...
url(r'^community/', include('django_website.aggregator.urls')),
url(r'^contact/', include('django_website.contact.urls')),
# ... snip ...
]
注意,这个例子中的正则表达式没有包含$(字符串结束匹配符),但是包含一个末尾的斜杠。每当Django 遇到include()(django.conf.urls.include())时,它会去掉URL 中匹配的部分并将剩下的字符串发送给包含的URLconf 做进一步处理。
另外一种包含其它URL 模式的方式是使用一个url() 实例的列表。例如,请看下面的URLconf:
from django.conf.urls import include, url
from apps.main import views as main_views
from credit import views as credit_views
extra_patterns = [
url(r'^reports/(?P<id>[0-9]+)/$', credit_views.report),
url(r'^charge/$', credit_views.charge),
]
urlpatterns = [
url(r'^$', main_views.homepage),
url(r'^help/', include('apps.help.urls')),
url(r'^credit/', include(extra_patterns)),
]
在这个例子中,/credit/reports/ URL将被 credit.views.report() 这个Django 视图处理。
这可以用于移除URL配置中重复的部分例如,考虑这个URLconf:
from django.conf.urls import url
from . import views
urlpatterns = [
url(r'^(?P<page_slug>[\w-]+)-(?P<page_id>\w+)/history/$', views.history),
url(r'^(?P<page_slug>[\w-]+)-(?P<page_id>\w+)/edit/$', views.edit),
url(r'^(?P<page_slug>[\w-]+)-(?P<page_id>\w+)/discuss/$', views.discuss),
url(r'^(?P<page_slug>[\w-]+)-(?P<page_id>\w+)/permissions/$', views.permissions),
]
我们可以改进它,通过只声明共同的路径前缀一次并将后面的部分分组:
from django.conf.urls import include, url
from . import views
urlpatterns = [
url(r'^(?P<page_slug>[\w-]+)-(?P<page_id>\w+)/', include([
url(r'^history/$', views.history),
url(r'^edit/$', views.edit),
url(r'^discuss/$', views.discuss),
url(r'^permissions/$', views.permissions),
])),
]
被包含的URLconf 会收到来自父URLconf 捕获的任何参数,所以下面的例子是合法的:
# In settings/urls/main.py
from django.conf.urls import include, url
urlpatterns = [
url(r'^(?P<username>\w+)/blog/', include('foo.urls.blog')),
]
# In foo/urls/blog.py
from django.conf.urls import url
from . import views
urlpatterns = [
url(r'^$', views.blog.index),
url(r'^archive/$', views.blog.archive),
]
在上面的例子中,捕获的"username"变量将被如期传递给include()指向的URLconf。
正则表达式允许嵌套参数,Django 将解析它们并传递给视图。当反查时,Django 将尝试填满所有外围捕获的参数,并忽略嵌套捕获的参数。考虑下面的URL 模式,它带有一个可选的page 参数:
from django.conf.urls import url
urlpatterns = [
url(r'blog/(page-(\d+)/)?$', blog_articles), # bad
url(r'comments/(?:page-(?P<page_number>\d+)/)?$', comments), # good
]
两个模式都使用嵌套的参数,其解析方式是:例如blog/page-2/ 将匹配blog_articles并带有两个位置参数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 的值。
作为一个经验的法则,当正则表达式需要一个参数但视图忽略它的时候,只捕获视图需要的值并使用非捕获参数。
URLconfs 具有一个钩子,让你传递一个Python 字典作为额外的参数传递给视图函数。
django.conf.urls.url() 函数可以接收一个可选的第三个参数,它是一个字典,表示想要传递给视图函数的额外关键字参数。
例如:
from django.conf.urls import url
from . import views
urlpatterns = [
url(r'^blog/(?P<year>[0-9]{4})/$', views.year_archive, {'foo': 'bar'}),
]
在这个例子中,对于/blog/2005/请求,Django 将调用views.year_archive(request, year='2005', foo='bar')。
这个技术在Syndication 框架 中使用,来传递元数据和选项给视图。
处理冲突
URL 模式捕获的命名关键字参数和在字典中传递的额外参数有可能具有相同的名称。当这种情况发生时,将使用字典中的参数而不是URL 中捕获的参数。
类似地,你可以传递额外的选项给include()。当你传递额外的选项给include() 时,被包含的URLconf 的每一 行将被传递这些额外的选项。
例如,下面两个URLconf 设置功能上完全相同:
设置一:
# main.py
from django.conf.urls import include, url
urlpatterns = [
url(r'^blog/', include('inner'), {'blogid': 3}),
]
# inner.py
from django.conf.urls import url
from mysite import views
urlpatterns = [
url(r'^archive/$', views.archive),
url(r'^about/$', views.about),
]
设置二:
# main.py
from django.conf.urls import include, url
from mysite import views
urlpatterns = [
url(r'^blog/', include('inner')),
]
# inner.py
from django.conf.urls import url
urlpatterns = [
url(r'^archive/$', views.archive, {'blogid': 3}),
url(r'^about/$', views.about, {'blogid': 3}),
]
注意,额外的选项将永远传递给被包含的URLconf 中的每一行,无论该行的视图实际上是否认为这些选项是合法的。由于这个原因,该技术只有当你确定被包含的URLconf 中的每个视图都接收你传递给它们的额外的选项时才有价值。
在使用Django 项目时,一个常见的需求是获得URL 的最终形式,以用于嵌入到生成的内容中(视图中和显示给用户的URL等)或者用于处理服务器端的导航(重定向等)。
人们强烈希望不要硬编码这些URL(费力、不可扩展且容易产生错误)或者设计一种与URLconf 毫不相关的专门的URL 生成机制,因为这样容易导致一定程度上产生过期的URL。
换句话讲,需要的是一个DRY 机制。除了其它优点,它还允许设计的URL 可以自动更新而不用遍历项目的源代码来搜索并替换过期的URL。
要获取一个URL,最初拥有的信息是负责处理它的视图的标识(例如名字),与查找正确的URL 的其它必要的信息如视图参数的类型(位置参数、关键字参数)和值。
Django 提供了一个解决方案使得URL 映射是URL 设计唯一的储存库。你用你的URLconf填充它,然后可以双向使用它:
第一种方式是我们在前面的章节中一直讨论的用法。第二种方式叫做反向解析URL、反向URL匹配、反向URL查询或者简单的URL反查。
在需要URL 的地方,对于不同层级,Django 提供不同的工具用于URL 反查:
考虑下面的URLconf:
from django.conf.urls import url
from . import views
urlpatterns = [
#...
url(r'^articles/([0-9]{4})/$', views.year_archive, name='news-year-archive'),
#...
]
根据这里的设计,某一年nnnn对应的归档的URL是/articles/nnnn/。
你可以在模板的代码中使用下面的方法获得它们:
<a href="{% url 'news-year-archive' 2012 %}">2012 Archive</a>
<ul>
{% for yearvar in year_list %}
<li><a href="{% url 'news-year-archive' yearvar %}">{{ yearvar }} Archive</a></li>
{% endfor %}
</ul>
在Python 代码中,这样使用:
from django.core.urlresolvers import reverse
from django.http import HttpResponseRedirect
def redirect_to_year(request):
# ...
year = 2006
# ...
return HttpResponseRedirect(reverse('news-year-archive', args=(year,)))
如果出于某种原因决定按年归档文章发布的URL应该调整一下,那么你将只需要修改URLconf 中的内容。
在某些场景中,一个视图是通用的,所以在URL 和视图之间存在多对一的关系。对于这些情况,当反查URL 时,只有视图的名字还不够。请阅读下一节来了解Django 为这个问题提供的解决办法。
为了完成上面例子中的URL 反查,你将需要使用命名的URL 模式。URL 的名称使用的字符串可以包含任何你喜欢的字符。并不仅限于合法的Python 名称。
当命名你的URL 模式时,请确保使用的名称不会与其它应用中名称冲突。如果你的URL 模式叫做comment,而另外一个应用中也有一个同样的名称,当你在模板中使用这个名称的时候不能保证将插入哪个URL。
在URL 名称中加上一个前缀,比如应用的名称,将减少冲突的可能。我们建议使用myapp-comment 而不是comment。
URL 命名空间允许你反查到唯一的命名URL 模式,即使不同的应用使用相同的URL 名称。第三方应用始终使用带命名空间的URL 是一个很好的实践(我们在教程中也是这么做的)。类似地,它还允许你在一个应用有多个实例部署的情况下反查URL。换句话讲,因为一个应用的多个实例共享相同的命名URL,命名空间提供了一种区分这些命名URL 的方法。
在一个站点上,正确使用URL命名空间的Django 应用可以部署多次。例如,django.contrib.admin 具有一个AdminSite 类,它允许你很容易地部署多个管理站点的实例。在下面的例子中,我们将讨论在两个不同的地方部署教程中的polls应用,这样我们可以为两种不同的用户(作者和发布者)提供相同的功能。
一个URL命名空间有两个部分,它们都是字符串:
URL 的命名空间使用':' 操作符指定。例如,管理站点应用的主页使用'admin:index'。它表示'admin' 的一个命名空间和'index' 的一个命名URL。
命名空间也可以嵌套。命名URL'sports:polls:index' 将在命名空间'polls'中查找'index',而poll 定义在顶层的命名空间'sports' 中。
当解析一个带命名空间的URL(例如'polls:index')时,Django 将切分名称为多个部分,然后按下面的步骤查找:
首先,Django 查找匹配的应用命名空间(在这个例子中为'polls')。这将得到该应用实例的一个列表。
如果有一个当前应用被定义,Django 将查找并返回那个实例的URL 解析器。当前应用可以通过请求上的一个属性指定。预期会具有多个部署的应用应该设置正在处理的request 的current_app 属性。
在以前版本的Django 中,你必须在用于渲染模板的每个Context 或 RequestContext上设置current_app 属性。
当前应用还可以通过reverse() 函数的一个参数手工设定。
如果没有当前应用。Django 将查找一个默认的应用实例。默认的应用实例是实例命名空间 与应用命名空间 一致的那个实例(在这个例子中,polls 的一个叫做'polls' 的实例)。
如果没有默认的应用实例,Django 将挑选该应用最后部署的实例,不管实例的名称是什么。
如果有嵌套的命名空间,将为命名空间的每个部分重复调用这些步骤直至剩下视图的名称还未解析。然后该视图的名称将被解析到找到的这个命名空间中的一个URL。
为了演示解析的策略,考虑教程中polls 应用的两个实例:'author-polls' 和'publisher-polls'。假设我们已经增强了该应用,在创建和显示投票时考虑了实例命名空间。
from django.conf.urls import include, url
urlpatterns = [
url(r'^author-polls/', include('polls.urls', namespace='author-polls', app_name='polls')),
url(r'^publisher-polls/', include('polls.urls', namespace='publisher-polls', app_name='polls')),
]
from django.conf.urls import url
from . import views
urlpatterns = [
url(r'^$', views.IndexView.as_view(), name='index'),
url(r'^(?P<pk>\d+)/$', views.DetailView.as_view(), name='detail'),
...
]
根据以上设置,可以使用下面的查询:
如果其中一个实例是当前实例 —— 如果我们正在渲染'author-polls' 实例的detail 页面 —— 'polls:index' 将解析成'author-polls' 实例的主页面;例如下面两个都将解析成"/author-polls/"。
在基于类的视图的方法中:
reverse('polls:index', current_app=self.request.resolver_match.namespace)
和在模板中:
{% url 'polls:index' %}
注意,在模板中的反查需要添加request 的current_app 属性,像这样:
def render_to_response(self, context, **response_kwargs):
self.request.current_app = self.request.resolver_match.namespace
return super(DetailView, self).render_to_response(context, **response_kwargs)
如果没有当前实例 —— 如果我们在站点的其它地方渲染一个页面 —— 'polls:index' 将解析到最后注册的polls的一个实例。因为没有默认的实例(命名空间为'polls'的实例),将使用注册的polls 的最后一个实例。它将是'publisher-polls',因为它是在urlpatterns中最后一个声明的。
'author-polls:index' 将永远解析到 'author-polls' 实例的主页('publisher-polls' 类似)。
如果还有一个默认的实例 —— 例如,一个名为'polls' 的实例 —— 上面例子中唯一的变化是当没有当前实例的情况(上述第二种情况)。在这种情况下 'polls:index' 将解析到默认实例而不是urlpatterns 中最后声明的实例的主页。
被包含的URLconf 的命名空间可以通过两种方式指定。
首先,在你构造你的URL 模式时,你可以提供应用和实例的命名空间给include() 作为参数。例如:
url(r'^polls/', include('polls.urls', namespace='author-polls', app_name='polls')),
这将包含polls.urls中定义的URL 到应用命名空间 'polls'中,其实例命名空间为'author-polls'。
其次,你可以include 一个包含嵌套命名空间数据的对象。如果你include() 一个url() 实例的列表,那么该对象中包含的URL 将添加到全局命名空间。然而,你还可以include() 一个3个元素的元组:
(<list of url() instances>, <application namespace>, <instance namespace>)
例如:
from django.conf.urls import include, url
from . import views
polls_patterns = [
url(r'^$', views.IndexView.as_view(), name='index'),
url(r'^(?P<pk>\d+)/$', views.DetailView.as_view(), name='detail'),
]
url(r'^polls/', include((polls_patterns, 'polls', 'author-polls'))),
这样会包含命名的URL模式进入到给定的应用和实例命名空间中。
例如,Django 的管理站点部署的实例叫AdminSite。AdminSite 对象具有一个urls 属性:一个3元组,包含管理站点中的所有URL 模式和应用的命名空间'admin'以及管理站点实例的名称。你include()到你项目的urlpatterns 中的是这个urls 属性。
请确保传递一个元组给include()。如果你只是传递3个参数:include(polls_patterns, 'polls', 'author-polls'),Django 不会抛出一个错误,但是根据include() 的功能,'polls' 将是实例的命名空间而'author-polls' 将是应用的命名空间,而不是反过来的。
2015年5月13日