Py学习  »  Django

【译】在Django中设置URL方法大全

老齐Py • 4 年前 • 270 次点击  
阅读 16

【译】在Django中设置URL方法大全

作者:Matt Layman

翻译:老齐


当用户访问网站的时候,其实是通过浏览器向网站发起访问请求,那么,网站是如何处理请求的?本文以Django框架为例进行说明,关于Django更详细的使用方法,请阅读《跟老齐学Python:Django实战(第二版)》。

来自浏览器的HTTP请求包含一个URL,URL描述Django应该生成哪些资源。由于URL可以有多种形式,我们必须把web应用可以处理的URL类型告诉Django,这就是URL配置的目的。在Django文档中,URL配置简称为URLconf。

URLconf在哪里?URLconf位于项目设置文件中的ROOT_URLCONF所设置的模块路径上。如果运行startproject命令,则该以project.url形式命名路由,其中“project”是项目名称。换句话说,URLconf就放在project/urls.py中,它与settings.py文件在同一个目录。

知道了文件所在的位置,但并没有讲它是如何工作的,下面来深入探索。

执行URLconf

可以将URL配置看作URL路径列表,Django会把这些路径从上到下进行匹配。当Django找到匹配的路径时,HTTP请求将转到与该路径相关联的Python代码块。这个“Python代码块”被称为视图,我们将在稍后对其进行更多的探讨。目前,请相信视图知道如何处理HTTP请求。

我们可以使用一个URLconf示例来实践上述观点。

# project/urls.py
from django.urls import path

from application import views

urlpatterns = [
    path("", views.home),
    path("about/", views.about),
    path("contact/", views.contact),
    path("terms/", views.terms),
]
复制代码

这里的内容与我上面的描述相吻合,Django对列表中的URL路径从上到下进行匹配。此列表的关键是urlpatterns。Django将把urlpatterns变量中的列表视为URLconf。

列表中的顺序也很重要,上述示例没有显示路径之间的任何冲突,但可以创建两个不同的path,它们可以匹配同一个URL,在后续内容中,会探究这个问题。

我们可以通过这个例子来看看它在www.itdiffer.com上是如何运行的。对于URLconf中的URL,Django不使用https://协议、或者主域名(www.itdiffer.com)及斜杠匹配路由,其他的都是URLconf所要匹配的。

  • 对https://www.itdiffer.com/about/匹配“about/”,并匹配第二个路径。该请求将转到views.about视图。
  • 对https://www.itdiffer.com/的请求匹配“”,并匹配第一个路径。该请求将路由到views.home视图。

补充说明:你可能注意到Django URL以斜杠结尾。事实上,如果你尝试访问像https://www.itdiffer.com/about这样的URL,Django会自动附加斜杠,再将请求重定向到相同的URL,这是因为使用了默认设置APPEND_SLASH,这种是Django在设计理念上的选择。

path的作用

如果我给出的只是上面的例子,你可能会说:“哇,Django太笨了,为什么urlpatterns不是下面这样的字典?”

urlpatterns = {
    "": views.home,
    "about/": views.about,
    "contact/": views.contact,
    "terms/": views.terms,
}
复制代码

我不会因为你以上言论而苛责。

原因是path的能力更强大,大部分功能都在传给函数的第一个字符串参数中。path的字符串部分(例如,“about/”)称为路由。

正如你所看到的,路由可以是一个简单的字符串,它也可以包含一些转换特征,如此,就可以从URL中提取信息,并传给视图,例如这样的一个路径:

path("blog/<int:year>/<slug:slug>/", views.blog_post),
复制代码

此路径中的两个转换器是:

使用尖括号和一些保留关键词,让Django对URL进行解析,每个转换器都有一些预期的规则要遵循。

  • int必须与整数匹配。
  • slug必须与标称匹配。Slug是一种出现在Django中的报刊行话,因为Django是从堪萨斯州的一家报纸开始的。slug是一个字符串,它可以包含字符、数字、破折号和下划线。

考虑到这些转换器定义,让我们与一些URL进行比较!

现在我们可以重新审视一下之前的排序问题,以不同的顺序考虑这两条路径:

path("blog/<int:year>/", views.blog_by_year),
path("blog/2020/", views.blog_for_twenty_twenty),

# vs.

path("blog/2020/", views.blog_for_twenty_twenty),
path("blog/<int:year>/", views.blog_by_year),
复制代码

在第一次排序中,转换器将匹配blog/之后的任何整数,包括https://www.itdiffer.com/blog/2020/。这意味着第一次排序将永远不会调用views.blog_for_twenty_twenty,因为Django按顺序匹配路径。

相反,在第二次排序中,blog/2020/将正确地路由到views.blog_for_twenty_twenty,因为它首先匹配。这意味着这里有一个教训要谨记:

当包含与转换器范围匹配的路径项时,请确保将它们放在更具体的项之后。

视图简介

转换器如何处理这些从路径中捕获的参数?这很难在不触及视图的情况下解释,所以要对视图给予入门介绍,关于视图更详细的内容,还是要阅读《跟老齐学Python:Django实战(第二版)》的深入浅出讲解。

视图的作用是接受请求并返回响应,使用Python的可选类型检查,下面是一个发送Hello World响应的示例。

from django.http import HttpRequest, HttpResponse

def some_view(request: HttpRequest) -> HttpResponse:
    return HttpResponse('Hello World')
复制代码

Django将用户的HTTP请求封装在一个容器类中,这就是HttpRequest。同样,我们可以使用HttpResponse,以便让Django将我们的响应数据转换为格式正确的HTTP响应,并将其发送回用户的浏览器。

现在我们可以再看一个转换器。

    path("blog/<int:year>/", views.blog_by_year),
复制代码

路由中有了这个转换器,blog_by_year会是什么样子?

# application/views.py
from django.http import HttpResponse

def blog_by_year(request, year):
    # ... some code to handle the year
    data = 'Some data that would be set by code above'
    return HttpResponse(data)
复制代码

Django开始在这里展示出一些不错的品质!转换器为我们做了一堆乏味的工作。Django设置的year参数已经是一个整数,因为Django进行了字符串解析和转换。

如果有人提交/blog/not_a_number/,Django将返回Not Found响应,因为not_a_number不是整数。这样做的好处是,我们不必在blog_by_year 中添加额外的检查逻辑来处理年份看起来不像数字的奇怪情况。这种函数可以节省时间!它使代码更干净,处理更精确。

怎样理解我们之前看到的另一个奇怪的例子/blog/0/life-in-rome/呢?这将与前面部分中的模式相匹配,但假设我们希望匹配一个四位数的年份,我们该怎么做到呢?可以使用正则表达式。

路由中的正则表达式

在编程中,正则表达式应用很普遍,常常被比作电锯:功能强大得令人难以置信,但如果不小心的话,也可以砍掉“你的脚”。

正则表达式可以以非常简洁的方式表示复杂的关系和匹配模式,这种简洁性常常给正则表达式带来难以理解的坏名声。但如果小心使用,它们可以是很好的工具。

正则表达式(通常缩写为“regex”)适合于匹配字符串中的复杂模式。这听起来像是要解决前年提到的年份问题!在我们的问题中,只想匹配一个四位数的整数。让我们看看Django的解决方案,然后分解它的含义。

提醒一下,此解决方案将匹配某些URL路径,如blog/2020/urls-lead-way/。

    re_path("^blog/(?P<year>[0-9]{4})/(?P<slug>[\w-]+)/$", views.blog_post),
复制代码

这个疯狂的字符串的行为与前面的例子完全一样,只是它把年份精确到四位数,这个疯狂的字符串被称为regex模式。当Django代码运行时,它将根据此模式中定义的规则测试URL路径。

要了解它的工作原理,我们必须知道模式的各个部分意味着什么,依次解释:

  • ^:意思是“模式必须从这里开始”,因为有了这个符号,像myblog/... 这样开头的路径是行不通的。
  • blog/:是字面上的解释。这些字符必须完全匹配。
  • (?P<year>[0-9]{4})中,圆括号内的部分是一个捕获组。?P<year>是与捕获组关联的名称,类似于<int:year>这样的转换器中的冒号右侧部分。该名称允许Django将名为year的参数中的内容传给视图。捕获组的另一部分[0-9]{4}是模式实际匹配的内容。[0-9]是一个字符类,意思是“匹配从0到9的任意数字,{4}表示它必须精确匹配四次。re_path提供的这种特殊性是int转换器所不能做到的!
  • 捕获组之间的斜杠/,是另一个要匹配的文本字符。
  • 第二个捕捉组(?P<slug>[\w-]+)将匹配的内容放入名为slug的参数中。[\w-]包含两种类型,\w是指自然语言中的任何单词字符,另一种是破折号(注意:英文的,即-)字符。最后,加号(+)字符表示字符类必须匹配一次或多次。
  • 最后一个斜杠也是原始字符匹配,即要匹配/
  • 为了完成这个模式,符号$的作用与^ 相反,表示“模式必须在这里结束”。因此blog/2020/some-slug/another-slug/是不匹配的。

祝贺你!这绝对是本文最难的部分。如果你明白我们对re_path做了什么,接下来的内容应该会让你很舒服。如果没有,请不要担心!如果你想了解有关正则表达式的更多信息,请知晓我在模式中描述的所有内容都不是Django所特有的。相反,这是Python的内置行为,你可以在网上搜到很多信息。

知道了re_path的强大作用,即使你今天不需要它,它也可以对你以后的Django开发有帮助。

URL分组配置

到目前为止,我们已经研究了可以在URLconf中映射的各个路由。如果多个视图函数使用同一个URL时,我们可以怎么做?为什么?

想象一下你正在创建一个教育项目。在你的项目中,有学校、学生和其他与教育相关的概念。你可以这样做:

# project/urls.py
from django.urls import path

from schools import views as schools_views
from students import views as students_views

urlpatterns = [
    path("schools/", schools_views.index),
    path("schools/<int:school_id>/", schools_views.school_detail),
    path("students/", students_views.index),
    path("students/<int:student_id>/", students_views.student_detail),
]
复制代码

这种方法可以奏效,但是,如果这样,根的路由就要遍历schools_viewsstudents_views每个应用中的视图。实际上,我们可以使用include来更好地处理这个问题。

# project/urls.py
from django.urls import include

urlpatterns = [
    path("schools/", include("schools.urls"),
    path("students/", include("students.urls"),
]
复制代码

然后,在每个应用中创建urls.py,并按照如下方式配置该应用的路由:

# schools/urls.py
from django.urls import path

from schools import views

urlpatterns = [
    path("", views.index),
    path("<int:school_id>/", views.school_detail),
]
复制代码

include使每个Django应用在需要定义的视图中拥有自主权,项目可以幸运地“忽视”应用要做的事情。

此外,还删除了重复的schools/students/。当Django处理一个路由时,它将匹配路由的第一部分,并将其余部分传递到在单个应用中定义的URLconf。通过这种方式,URL配置可以形成一个树,其中的root URLconf 是所有请求开始的地方,但是当一个请求被转到相应的应用时,该应用可以处理细节。

命名URL

我们已经研究了用pathre-pathinclude定义URL的主要方法。还有另外一个方面需要考虑,如何在代码中使用URL?想想这个(相当愚蠢的)做法:

# application/views.py
from django.http import HttpResponseRedirect

def old_blog_categories(request):
    return HttpRequestRedirect("/blog/categories/")
复制代码

重定向是指用户试图访问一个页面时,被浏览器发送到其他地方。有比这个例子更好的方法来处理重定向,但是这个例子说明了一个不同的观点。如果你希望重新构造该项目,以便将博客类别从/blog/categories/改为/marketing/blog/categories/,会发生什么情况?在当前代码中,我们必须重写这个视图,并且通过路由转到其他视图。

真是浪费时间!Django为我们提供解决此问题的方法。可以在路由中设置每个URL的名称,使用pathname参数来实现。

# project/urls.py
from django.urls import path

from blog import views as blog_views

urlpatterns = [
    ...
    path("/marketing/blog/categories/",
        blog_views.categories, name="blog_categories"),
    ...
]
复制代码

这使得blog_categories成为/marketing/blog/categories/的名称。要使用这个名称,我们需要返回到它的对应项。修改后的视图如下:

# application/views.py
from django.http import HttpResponseRedirect
from django.urls import reverse

def old_blog_categories(request):
    return HttpRequestRedirect(reverse("blog_categories"))
复制代码

reverse的任务是查找任何url的名称并返回其对应路径。这意味着:

reverse("blog_categories") == "/marketing/blog/categories/"
复制代码

至少持续到你想再次改变它之前。😁

名称冲突

如果你对多个URL取相同的名称,会发生什么情况?例如,index或detail是你可能要应用的常见名称。我们可以向《Python之禅》寻求建议。

如果编程时间不长,命名空间对你来说可能是新知识,它们是共享的名称空间。也许这一点很清楚,但我记得当我第一次开始写软件的时候,我还在为这个概念而挣扎。

我们用现实中的一类场景来类别,假设你有两个红色球、两个蓝色球和分别标有“A”和“B”的两个桶,把球分别放在两个桶里面。如果我想要蓝色球,我不能说“请把蓝色球给我”,因为那样会模棱两可。相反,为了得到一个特定的球,我需要说“请给我B桶中的蓝色球。”在这个场景中,bucket(桶)是命名空间。

我们所举的关于学校和学生的示例,也可以作为例子说明这个想法。两个应用都有一个index视图函数来表示该应用部分的根(即schools/students/)。如果想指向某个视图中的函数,不得不使用views.index,然后,这样Django就无法判断应该转向哪个视图的index函数。

一种解决方案是通过在名称前面加上schools_之类的常见名称来创建自己的命名空间。这种方法的问题在于URLconf会自我重复。

# schools/urls.py
from django.urls import path

from schools import views

urlpatterns = [
    path("", views.index, name="schools_index"),
    path("<int:school_id>/", views.school_detail, name="schools_detail"),
]
复制代码

Django提供了一个替代方法,可以让你保留一个较短的名称。

# schools/urls.py
from django.urls import path

from schools import views

app_name = "schools"
urlpatterns = [
    path("", views.index, name="index"),
    path("<int:school_id>/", views.school_detail, name="detail"),
]
复制代码

通过添加app_name,我们向Django发出信号,这些视图位于一个命名空间中。现在,当我们想要获得一个URL时,我们使用命名空间的名称和URL名称,并用冒号将它们连接起来。

reverse("schools:index") == "/schools/"
复制代码

这是Django提供的另一个便利,它使我们更容易专注于应用的开发。

我们的URL话题进入尾声。最后给大家推荐的是一本学习Django的好书《跟老齐学Python:Django实战(第二版)》。

原文链接:www.mattlayman.com/understand-…

关注微信公众号:老齐教室。读深度文章,得精湛技艺,享绚丽人生。

Python社区是高质量的Python/Django开发社区
本文地址:http://www.python88.com/topic/54228
 
270 次点击