django[四] 路由层urls-迁
URL路由
路由基础
路由的编写方式是Django 2.0 和 1.11 最大的区别。将url改成path这种更简单的表达方式,但是依然可以通过re_path()方法保持对1.X版本的兼容。
一个基本的url:
from django.contrib import admin from django.urls import path import views urlpatterns = [ 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), path('admin/', admin.site.urls), ]
注意:
- 要捕获一段url中的值,需要使用尖括号,而不是之前的圆括号;
- 可以转换捕获到的值为指定类型,比如例子中的int。默认情况下,捕获到的结果保存为字符串类型,不包含
/
这个特殊字符; - 匹配模式的最开头不需要添加
/
,因为默认情况下,每个url都带一个最前面的/
,既然大家都有的部分,就不用浪费时间特别写一个了。
- /articles/2005/03/ 将匹配第二条,并调用views.month_archive(request, year=2005, month=3);
- /articles/2003/匹配第一条,并调用views.special_case_2003(request);
- /articles/2003将一条都匹配不上,因为它最后少了一个斜杠,而列表中的所有模式中都以斜杠结尾;
- /articles/2003/03/building-a-django-site/ 将匹配最后一个,并调用views.article_detail(request, year=2003, month=3, slug="building-a-django-site")
1.X 和 2.x在路由方面的区别
# Django 1.11 # from django.conf.urls import url # from django.contrib import admin # # urlpatternes = [ # url(r'admin/', admin.site.urls), # url('^login$', view.login), # url('^logout$', view.logout) # ] + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) # Django 2.x # from django.urls import path, re_path # urlpatterns = [ # path('admin/', admin.site.urls) # ]
django如何处理请求
- 决定要使用的根URLconf模块。通常,这是
ROOT_URLCONF
设置的值,但是如果传入的HttpRequest对象具有urlconf属性(由中间件设置),则其值将被用于代替ROOT_URLCONF
设置。通俗的讲,就是你可以自定义项目入口url是哪个文件! - 加载该模块并寻找可用的urlpatterns。 它是
django.urls.path()
或者django.urls.re_path()
实例的一个列表。 - 依次匹配每个URL模式,在与请求的URL相匹配的第一个模式停下来。也就是说,url匹配是从上往下的短路操作,所以url在列表中的位置非常关键。
- 导入并调用匹配行中给定的视图,该视图是一个简单的Python函数(被称为视图函数),或基于类的视图。 视图将获得如下参数:
- 一个HttpRequest 实例。
- 如果匹配的表达式返回了未命名的组,那么匹配的内容将作为位置参数提供给视图。
- 关键字参数由表达式匹配的命名组组成,但是可以被
django.urls.path()
的可选参数kwargs覆盖。
- 如果没有匹配到任何表达式,或者过程中抛出异常,将调用一个适当的错误处理视图。
path转换器
默认情况下,Django内置下面的路径转换器:
str
:匹配任何非空字符串,但不含斜杠/
,如果你没有专门指定转换器,那么这个是默认使用的;int
:匹配0和正整数,返回一个int类型slug
:可理解为注释、后缀、附属等概念,是url拖在最后的一部分解释性字符。该转换器匹配任何ASCII字符以及连接符和下划线,比如building-your-1st-django-site
;uuid
:匹配一个uuid格式的对象。为了防止冲突,规定必须使用破折号,所有字母必须小写,例如075194d3-6885-417e-a8a8-6c931e272f00
。返回一个UUID对象;path
:匹配任何非空字符串,重点是可以包含路径分隔符’/‘。这个转换器可以帮助你匹配整个url而不是一段一段的url字符串。要区分path转换器和path()方法。
使用正则表达式
Django2.0的url虽然改‘配置’了,但它依然向老版本兼容。而这个兼容的办法,就是用re_path()
方法代替path()
方法。re_path()
方法在骨子里,根本就是以前的url()
方法,只不过导入的位置变了
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()
方法不同的在于两点:
- year中匹配不到10000等非四位数字,这是正则表达式决定的
- 传递给视图的所有参数都是字符串类型。而不像
path()
方法中可以指定转换成某种类型。在视图中接收参数时一定要小心。
自定义错误页面
当Django找不到与请求匹配的URL时,或者当抛出一个异常时,将调用一个错误处理视图。Django默认的自带的错误视图包括400、403、404和500,分别表示请求错误、拒绝服务、页面不存在和服务器错误。它们分别位于:
- handler400 —— django.conf.urls.handler400。
- handler403 —— django.conf.urls.handler403。
- handler404 —— django.conf.urls.handler404。
- handler500 —— django.conf.urls.handler500。
在views中添加
from select_related.views import * urlpatterns = [ url("city/", city), url("test/", contentype_test), url("filter/", filter_test), url('admin/', admin.site.urls), ] handler400 = bad_request handler404 = page_not_found
然后在 views里面添加这两个处理视图
def bad_request(requests, exception): return render(requests, '400.html') def page_not_found(requests, exception): print("page_not_found") return render(requests, '404.html')
这样还不够,需要在settings里面把debug设置为False
路由转发
路由转发
通常,我们会在每个app里,各自创建一个urls.py路由模块,然后从根路由出发,将app所属的url请求,全部转发到相应的urls.py模块中
from django.urls import include, path urlpatterns = [ path('community/', include('aggregator.urls')), path('contact/', include('contact.urls')), ]
只需要声明共同的路径前缀一次,并将后面的部分分组转发:
from django.urls import include, 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), #] urlpatterns = [ path('<page_slug>-<page_id>/', include([ path('history/', views.history), path('edit/', views.edit), path('discuss/', views.discuss), path('permissions/', views.permissions), ])), ]
捕获参数
# 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具有一个钩子(hook),允许你传递一个Python字典作为额外的关键字参数给视图函数,像下面这样:
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')
。理论上,你可以在这个字典里传递任何你想要的传递的东西。但是要注意,URL模式捕获的命名关键字参数和在字典中传递的额外参数有可能具有相同的名称,这会发生冲突,要避免。
pk_url_kwarg默认取得pk组
url(r'^groupdetail/(?P<pk>[0-9]+)?/$', role.GroupDetailView.as_view(), name='role_detail'),
如果有多个
url(r'^groupdetail/(?P<pk>[0-9]+)?/(?P<poke_pk>\d+)$', role.GroupDetailView.as_view(), name='role_detail'),
传递到类视图
class UserGroupPowerView(LoginRequiredMixin, DetailView): '''修改用户权限''' model = UserProfile next_url = '/dashboard/userlist/' template_name = "dashboard/user_group_power.html" context_object_name = 'user' def get_context_data(self, **kwargs): context = super(UserGroupPowerView, self).get_context_data(**kwargs) context['user_has_groups'], context['user_has_permissions'] = self.get_user_group() context['user_not_groups'], context['user_not_permissions'] = self.get_user_not_group() return context def get_user_group(self): pk = self.kwargs.get(self.pk_url_kwarg) // pk_url_kwarg的取得 try: user = self.model.objects.get(pk=pk) // 本model return user.groups.all(), user.user_permissions.all() except self.model.DoesNotExist: raise Http404
URL反向解析和命名空间
反向解析URL
在实际的Django项目中,经常需要获取某条URL,为生成的内容配置URL链接。
比如,我要在页面上展示一列文章列表,每个条目都是个超级链接,点击就进入该文章的详细页面。
现在我们的urlconf是这么配置的:
path('post/<int:pk>/',views.some_view),
在前端中,这就需要为HTML的<a>
标签的href属性提供一个诸如http://www.xxx.com/post/3/
的值。其中的域名部分,Django会帮你自动添加无须关心,我们关注的是post/3/
。
此时,一定不能硬编码URL为post/3/
,那样费时、不可伸缩,而且容易出错。试想,如果哪天,因为某种原因,需要将urlconf中的表达式改成entry/<int:pk>/
,为了让链接正常工作,必须修改对应的herf属性值,于是你去项目里将所有的post/3/
都改成entry/3/
吗?显然这是不现实的!
我们需要一种安全、可靠、自适应的机制,当修改URLconf中的代码后,无需在项目源码中大范围搜索、替换失效的硬编码URL。
为了解决这个问题,Django提供了一种解决方案,只需在URL中提供一个name参数,并赋值一个你自定义的、好记的、直观的字符串。
通过这个name参数,可以反向解析URL、反向URL匹配、反向URL查询或者简单的URL反查。
在需要解析URL的地方,对于不同层级,Django提供了不同的工具用于URL反查:
-
在模板语言中:使用
url
模板标签。(也就是写前端网页时) -
在Python代码中:使用
reverse()
函数。(也就是写视图函数等情况时) -
在更高层的与处理Django模型实例相关的代码中:使用
get_absolute_url()
方法。(也就是在模型model中)
范例:
考虑下面的URLconf:
from django.urls import path from . import views urlpatterns = [ #... path('articles/<int:year>/', views.year_archive, name='news-year-archive'), #... ]
2019年对应的归档URL是/articles/2019/
。
可以在模板的代码中使用下面的方法获得它们:
<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 = 2019 # ... return HttpResponseRedirect(reverse('news-year-archive', args=(year,)))
其中,起到核心作用的是我们通过name='news-year-archive'
为那条url起了一个可以被引用的名称。
URL名称name使用的字符串可以包含任何你喜欢的字符,但是过度的放纵有可能带来重名的冲突,比如两个不同的app,在各自的urlconf中为某一条url取了相同的name,这就会带来麻烦。为了解决这个问题,又引出了下面的命名空间。
URL命名空间
URL命名空间可以保证反查到唯一的URL,即使不同的app使用相同的URL名称。
第三方应用始终使用带命名空间的URL是一个很好的做法。
类似地,它还允许你在一个应用有多个实例部署的情况下反查URL。 换句话讲,因为一个应用的多个实例共享相同的命名URL,命名空间提供了一种区分这些命名URL 的方法。
实现命名空间的做法很简单,在urlconf文件中添加app_name = 'polls'
和namespace='author-polls'
这种类似的定义。
范例:
以前面的polls应用的两个实例为例子:'publisher-polls' 和'author-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'), ... ]
如果当前的app实例是其中的一个,例如我们正在渲染实例'author-polls'中的detail视图,'polls:index'将解析到'author-polls'实例的index视图。
根据以上设置,可以使用下面的查询:
在基于类的视图的方法中:
reverse('polls:index', current_app=self.request.resolver_match.namespace)
和在模板中:
{% url 'polls:index' %}
如果没有当前app实例,例如如果我们在站点的其它地方渲染一个页面,'polls:index'将解析到polls注册的最后一个app实例空间。 因为没有默认的实例(命名空间为'polls'的实例),将使用注册的polls 的最后一个实例。 这将是'publisher-polls',因为它是在urlpatterns中最后一个声明的。
URL命名空间和include的URLconf
可以通过两种方式指定include的URLconf的应用名称空间。
第一种
在include的URLconf模块中设置与urlpatterns属性相同级别的app_name
属性。必须将实际模块或模块的字符串引用传递到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中定义的URL将具有应用名称空间polls
第二种
include一个包含嵌套命名空间数据的对象,格式如下:
(<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)), ]