反向解析和命名空间

阅读: 62126     评论:14

一、反向解析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/.../都改成entry/.../吗?显然这是不现实的!

我们需要一种安全、可靠、自适应的机制,当修改URLconf中的代码后,无需在项目源码中大范围搜索、替换失效的硬编码URL。

为了解决这个问题,Django提供了一种解决方案,只需在URL中提供一个name参数,并赋值一个你自定义的、好记的、直观的字符串。

通过这个name参数,可以反向解析URL、反向URL匹配、反向URL查询或者简单的URL反查。

在需要解析URL的地方,对于不同层级,Django提供了不同的工具用于URL反查:

  • 在模板语言中:使用url模板标签。(也就是写前端网页时)

  • 在Python代码中:使用reverse()函数。(也就是写视图函数等情况时)

  • 在更高层的与处理Django模型实例相关的代码中:使用get_absolute_url()方法。(也就是在模型model中,参考前面的章节)

所有上面三种方式,都依赖于首先在path种为url添加name属性!

范例:

考虑下面的URLconf:

from django.urls import path

from . import views

urlpatterns = [
    #...
    path('articles/<int:year>/', views.year_archive, name='news-year-archive'),
    #...
]

2020年对应的归档URL是/articles/2020/

可以在模板的代码中使用下面的方法获得它们(可以结合模板语法章节来理解下面的内容):

<a href="{% url 'news-year-archive' 2020 %}">2020 Archive</a>

{# 或者使用for循环变量 #}
<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 = 2020
    # ...
    return HttpResponseRedirect(reverse('news-year-archive', args=(year,)))

其中,起到核心作用的是我们通过name='news-year-archive'为那条path起了一个可以被引用的名称。

URL名称name使用的字符串可以包含任何你喜欢的字符,但是过度的放纵有可能带来重名的冲突,比如两个不同的app,在各自的urlconf中为某一条path取了相同的name,这就会带来麻烦。为了解决这个问题,又引出了下面的命名空间。

二、应用命名空间app_name

前面我们为介绍了path的name参数,为路由添加别名,实现反向url解析和软编码解耦。

但是,我们思考这么一个问题,假设下面的情况:

  • appA,有一条路由A,它的name叫做'index'
  • appB,有一条路由B,它的name也叫做'index'

这种情况完全是有可能的,甚至还常见!

请问,你在某个视图中使用reverse('index',args=(...))或者在模板中使用{% url 'index' ... %},它最终生成的URL到底是路由A还是路由B呢?

不管是哪个,肯定都不好,因为我们需要确定状态的路由,而不是混乱的。

之所以造成这种情况,根本上还是各个app之间没有统一的路由管理,实际上也不可能有。

最佳解决问题的办法就是隔离分治,命名空间解耦,这也是常用的设计模式。

Django提供了一个叫做app_name的属性,帮我们实现应用级别的命名空间,这样,虽然大家都叫‘大伟’,但一个是‘张大伟’,一个是‘王大伟’,自然就分清楚了甲乙丙丁。

from django.urls import path

from . import views

app_name = 'your_app_name'   # 重点是这行!

urlpatterns = [
    ...
]

我么只需要在app自己本身的urls.py文件内,添加一行app_name = 'your_app_name'即可。注意不是在根路由文件中。一般就和自己的app同命即可,因为项目里面不会有2个同样名字的app。

使用的方式很简单:

# 视图中
reverse('your_app_name:index',args=(...))

# 模板中
{% url 'your_app_name:index' ... %}

注意your_app_name和index之间是冒号分隔。

三、实例命名空间namespace

首先我们要知道,Django是可以对app进行实例化的。也就是说:

  • 一个app在运行的时候,可以同时生成多个实例
  • 每个实例运行同样的代码
  • 但是不同的实例,可能会有不同的状态

以上不好理解,甚至很多人都不知道这个知识点。

假设我们有个app,实现一个index页面展示功能:

  • 假设访问Django服务器的人分两类,author和publisher,作者和出版社
  • 他们都需要访问app
  • 业务需求:为两类人实现不同的权限或者页面内容
  • 尽可能重用代码

为此,我们可以这么实现:

  • 根据不同的url来区分两类人,author访问author/...,publisher访问publisher/...
  • 两个url都指向同一个app的url:include('app.urls')
  • 在视图中,根据来访人员的不同,if/else判断,实现不同的业务逻辑。
  • 这样,我们就相当于共用了urls和views实现了两套app

而这,就是所谓的app的多个实例!

但这种做法有个明显的问题,就是对于每条URL,如何区分两种人员?

使用应用命名?像reverse('your_app_name:index',args=(...))这样?

显然是不行的,因为多个应用的实例共享应用空间名,通过app_name是区分不了的!

针对这种情况,Django提供了一个namespace属性,用于标记不同的应用实例,如下所示:

根urls.py

from django.urls import include, path

urlpatterns = [
    path('author/', include('app.urls', namespace='author')),
    path('publisher/', include('app.urls', namespace='publisher')),
]

app/urls.py

from django.urls import path

from . import views

app_name = 'app'          # 这行不能少

urlpatterns = [
    path('index/', views.index, name='index'),
    path('detail/', views.detail, name='detail'),
]

app/views.py

from django.shortcuts import render,HttpResponse


def index(request):

    return HttpResponse('当前的命名空间是%s'% request.resolver_match.namespace)

def detail(request):
    if request.resolver_match.namespace == 'author':
        return HttpResponse('这里是作者的页面')
    elif request.resolver_match.namespace == 'publisher':
        return HttpResponse('这里是出版商的页面')
    else:
        return HttpResponse('去liujiangblog.com学习Django吧')

启动服务器,分别访问author/index/publisher/index,你应该能看到不同的结果。

要注意:

  • namespace定义在include中
  • 整个项目的所有app中的所有namespace不能重名,也就是全局唯一!所以我们一般设计成appname_spacename。(文中为了简洁,偷了个懒)
  • 使用namespace功能的前提是设置app_name,如果不设置,会弹出异常
  • 要在视图中获取namespace属性值,通过request.resolver_match.namespace

这样,我们实现了URL的正向解析。那么反向解析怎么做呢?

为根urls.py添加一条路由:

from app import views

path('goto/', views.goto)

app/views.py添加一个视图:

def goto(request):
    return HttpResponseRedirect(reverse('app:index', current_app='author' ))

注意reverse方法提供的current_app参数!

然后我们访问goto/,可以看到页面跳转到了author的index。

是直接指定字符串形式的namespace名称,还是使用request.resolver_match.namespace,取决于你当前视图是从哪个URL路由过来的,该URL是否携带了namespace属性。

在基于类的视图的方法中,我们也可以这样使用:

reverse('app:index', current_app=self.request.resolver_match.namespace)

再拓展一下,实际上我们再reverse的时候完全可以直接指定使用哪个命名空间:

reverse('author:index')

# 或者

reverse('publisher:index')

那么在HTML模板中怎么使用呢?

可以这么用:

{% url 'app:index' %}

但是要注意,这会有两种情况:

  • 视图携带了namespace,那么访问对应的namespace
  • 视图未携带namespace,app应用上一次使用的是author还是pushliser中的哪个,就访问哪个

所以为了彻底区分每个实例,你也可以这么用:

{% url 'author:index' %}

或者

{% url 'publisher:index' %}

其实,从这里也能看出,Django为什么要求namespace全局唯一。


 路由转发 视图函数及快捷方式 

评论总数: 14


点击登录后方可评论

reverse那里就没看明白了,多找点案例实操吧!



我有一个疑问? 应用命名空间在用的时候是这样表示:app:index 实例命名空间在用的时候怎么也是这样表示:author:index、publisher:index 有没有吊大解释下?



app_name跟namespace的例子还可以改改,没有突出这两个的差别。



这章给我看蒙了



确实,这节教程漏洞较多,比如namespace如何使用,没有说清除,没有较好的例子说明。不过瑕不掩瑜吧。



这章翻译的遗漏好多,revserse()函数整个都没,勉强能看懂吧



一脸懵逼,感觉很乱,没说清楚。其实建议博主可以在文章中间加一些图解,帮助理解,django感觉很多东西,容易混淆,有图的话就清晰很多。



说实在的,Django2.x中,namespace真没啥用,有用的是app_name。



适合有django开发基础的,查漏补缺



我也是,看的好懵,看细节真的好难懂啊



Django有一点基础,前面内容都没问题,这个真心没理解



这一节我也看得蒙蒙的





同上