04. 路由

发布时间 2023-11-16 19:54:36作者: 星河樱梦

一、简单配置

  URL 配置的本质是 URL 与要为该 URL 调用的视图函数之间的 映射表。通过这种方式告诉 Django,对于客户端发过来的某个 URL 调用哪一段逻辑代码执行。

  用户通过在浏览器中输入 URL 和单击链接来请求网页,因此需要确定项目需要哪些 URL。我们新建一个 Django 项目,并在项目的主文件夹 Django 中的文件 urls.py 中添加要请求的路径:

from django.contrib import admin
from django.urls import path, re_path

from app import views

urlpatterns = [
    # 路由配置:路径 ---> 视图函数
    path('admin/', admin.site.urls),

    # 这里URL使用正则表达式,^表示开头匹配,$表示结尾匹配
    re_path(r'^articles/2003/$', views.special_case_2003),
    # ()表示分组,分组的内容也会发送给视图函数作为形参
    re_path(r'^articles/([0-9]{4})/$', views.year_archive),
    re_path(r'^articles/(\d{4})/(\d{2})/$', views.month_archive),
]

  视图函数接受请求的信息,准备好生成网页所需要的数据,再将这些数据发给浏览器。我们再新建一个应用程序,并在 app 文件夹中的文件 views.py 中添加视图函数:

from django.shortcuts import HttpResponse

# Create your views here.
def special_case_2003(request):
    # HttpResponse 响应对象,将数据返回给浏览器
    return HttpResponse("special_case_2003")

def year_archive(request, year):
    return HttpResponse(f"{year}")

def month_archive(request, year, month):
    return HttpResponse(f"{year}-{month}")

  我们在浏览器中输入 URL http://localhost:8000/articles/2003/ 访问:

简单配置1

  我们在浏览器中输入 URL http://localhost:8000/articles/2004/ 访问:

简单配置2

当 URL 与多个请求路径匹配成功时,会匹配上面的那一个请求路径;

若从 URL 中捕获一个值,只需要在它周围放置一对圆括号,分组即可;

二、有名分组

  上面的示例使用简单的、没有命名的正则表达式组(通过圆括号)来捕获 URL 中的值并以 位置参数 传递给视图函数。在 Python 正则表达式中,可以使用 命名正则表达式组 的语法(?P<name>pattern)将捕获的值作为 关键字参数 而不是位置参数传递给视图函数。其中,name 是组的名称,pattern 是要匹配的模式。

 修改项目的主文件夹 Django 中的文件 urls.py 中的请求路径:

from django.contrib import admin
from django.urls import path, re_path

from app import views

urlpatterns = [
    # 路由配置:路径 ---> 视图函数
    path('admin/', admin.site.urls),

    # 这里URL使用正则表达式,^表示开头匹配,$表示结尾匹配
    re_path(r'^articles/2003/$', views.special_case_2003),
    # ()表示分组,分组的内容也会发送给视图函数作为形参
    re_path(r'^articles/([0-9]{4})/$', views.year_archive),
    re_path(r'^articles/(?P<y>\d{4})/(?P<m>\d{2})/$', views.month_archive),
]

  修改 app 文件夹中的文件 views.py 中的视图函数:

from django.shortcuts import HttpResponse

# Create your views here.
def special_case_2003(request):
    # HttpResponse 响应对象,将数据返回给浏览器
    return HttpResponse("special_case_2003")

def year_archive(request, year):
    return HttpResponse(f"{year}")

def month_archive(request, m, y):
    return HttpResponse(f"{y}-{m}")

  我们在浏览器中输入 URL http://localhost:8000/articles/2004/12/ 访问:

有名分组

三、路由分发

  在实际开发过程中,一个 Django 项目会包含多个应用 ,这时候如果我们只在主路由里进行配置就会显得很乱,所以通常会在每个应用里,创建各自的 urls.py(文件名随意)路由模块,然后从根路由出发,将 应用所属的 URL 请求,全部转发到相应的 urls.py 模块中。而这个从主路由转发到各个应用路由的过程叫做路由的 分发,而它的实现是使用 include() 函数来完成的

  修改项目的主文件夹 Django 中的文件 urls.py 中的请求路径:

from django.contrib import admin
from django.urls import path, re_path, include

urlpatterns = [
    # 路由配置:路径 ---> 视图函数
    path('admin/', admin.site.urls),

    # 路由分发
    re_path(r"^app/", include("app.urls")),
]

  在 app 应用中新建一个 urls.py 文件:

from django.urls import re_path

from app import views

urlpatterns = [
    # 路由配置:路径 ---> 视图函数
    # 这里URL使用正则表达式,^表示开头匹配,$表示结尾匹配
    re_path(r'^articles/2003/$', views.special_case_2003),
    # ()表示分组,分组的内容也会发送给视图函数作为形参
    re_path(r'^articles/([0-9]{4})/$', views.year_archive),
    re_path(r'^articles/(?P<y>\d{4})/(?P<m>\d{2})/$', views.month_archive),
]

  我们在浏览器中输入 URL http://localhost:8000/app/articles/2003/ 访问:

路由分发1

  我们在浏览器中输入 URL http://localhost:8000/app/articles/2004/ 访问:

路由分发2

  我们还可以将路由手动划分,把 url 的公共部分提取出来,修改 app 应用的 urls.py 文件,内容如下:

urlpatterns = [
    # 路由配置:路径 ---> 视图函数
    # 这里URL使用正则表达式,^表示开头匹配,$表示结尾匹配
    re_path(r'^articles/', ([
        re_path(r'^2003/$', views.special_case_2003),
        re_path(r'^([0-9]{4})/$', views.year_archive),
        re_path(r'^(?P<y>\d{4})/(?P<m>\d{2})/$', views.month_archive),
    ], None, None)),
]

如果我们还想向之前一样访问,URL 中没有 app,re_path() 方法的第一个参数可以传入空字符;

四、反向解析

 每个视图函数都有一个和其相对应的路由,但是如果它们之间的匹配关系发生了变化,那么与之对应的访问地址也需要跟着发生改变,这是极其不方便的。因此我们可以用一种动态解析 URL 的方法来避免。我们可以在 path() 中使用 name 关键字参数给对应路由起别名,从而让与之对应的链接或者跳转,会根据这个别名来动态解析 URL,这个动态解析 URL 路径的过程就是 反向解析

   path() 函数接受三个实参。第一个是一个字符串,帮助 Django 正确的 路由(route)请求。收到请求的 URL 后,Django 力图将请求路由给一个 视图,并为此搜索所有的 URL 模式,以找到与当前请求匹配的。如果请求的 URL 与任何既有的 URL 模式都不匹配,Django 将返回一个错误页面。

  path() 的第二个实参指定要调用 views.py 中的哪个函数。

  path() 的第三个实参将为这个 URL 模式起个别名,让我们能在其它项目文件中轻松地引用它。每当需要提供这个主页地链接时,都将使用这个名称,而不编写 URL。

【1】、使用 reverse() 函数实现反向解析

  修改 app 应用中 urls.py 文件:

from django.urls import re_path

from app import views

urlpatterns = [
    # 路由配置:路径 ---> 视图函数
    # 这里URL使用正则表达式,^表示开头匹配,$表示结尾匹配
    re_path(r'^articles/2003/$', views.special_case_2003, name="special_case_2003"),
    # ()表示分组,分组的内容也会发送给视图函数作为形参
    re_path(r'^articles/([0-9]{4})/$', views.year_archive, name="year_archive"),
    re_path(r'^articles/(?P<y>\d{4})/(?P<m>\d{2})/$', views.month_archive, name="month_archive"),
]

  修改 app 应用中 views.py 文件:

from django.shortcuts import HttpResponse
from django.urls import reverse

# Create your views here.
def special_case_2003(request):
    url = reverse("special_case_2003")
    # HttpResponse 响应对象,将数据返回给浏览器
    return HttpResponse(url)

def year_archive(request, year):
    # 反向解析与具体的视图函数无关
    val = reverse("special_case_2003")
    # 如果反向解析出的内容有正则表达式,则需要替换正则表达式
    url = reverse("year_archive", args=(year,))
    return HttpResponse(f"{url}<br>{val}")

def month_archive(request, m, y):
    val = reverse("special_case_2003")
    res = reverse("year_archive", args=(y,))
    url = reverse("month_archive", kwargs={"m":m, "y":y})
    return HttpResponse(f"{url}<br>{val}<br>{res}")

  我们在浏览器中输入 URL http://localhost:8000/app/articles/2003/ 访问:

reverse反向解析1

  我们在浏览器中输入 URL http://localhost:8000/app/articles/2004/ 访问:

reverse反向解析2

  我们在浏览器中输入 URL http://localhost:8000/app/articles/2004/12 访问:

reverse反向解析3

【2】、使用 URL 模板标签实现反向解析

  修改 app 应用中 urls.py 文件:

from django.urls import path

from app import views

urlpatterns = [
    # 路由配置:路径 ---> 视图函数
    # 这里URL使用正则表达式,^表示开头匹配,$表示结尾匹配
    re_path(r'^articles/$', views.articles, name="articles"),
    re_path(r'^articles/2003/$', views.special_case_2003, name="special_case_2003"),
    # ()表示分组,分组的内容也会发送给视图函数作为形参
    re_path(r'^articles/([0-9]{4})/$', views.year_archive, name="year_archive"),
    re_path(r'^articles/(?P<y>\d{4})/(?P<m>\d{2})/$', views.month_archive, name="month_archive"),
]

  修改 app 应用中 views.py 文件:

from django.shortcuts import HttpResponse, render


# Create your views here.
def articles(request):
    # render 根据视图提供的数据渲染响应
    return render(request, "app/articles.html")

def special_case_2003(request):
    url = reverse("special_case_2003")
    # HttpResponse 响应对象,将数据返回给浏览器
    return HttpResponse(url)

def year_archive(request, year):
    # 反向解析与具体的视图函数无关
    val = reverse("special_case_2003")
    # 如果反向解析出的内容有正则表达式,则需要替换正则表达式
    url = reverse("year_archive", args=(year,))
    return HttpResponse(f"{url}<br>{val}")

def month_archive(request, m, y):
    val = reverse("special_case_2003")
    res = reverse("year_archive", args=(y,))
    url = reverse("month_archive", kwargs={"m":m, "y":y})
    return HttpResponse(f"{url}<br>{val}<br>{res}")

  在 template 文件夹下新建一个 app 文件夹,专门用来存放 app 应用中的模板。然后新建一个 articles.html 文件,内容如下:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
    <a href="{% url 'special_case_2003' %}">special_case_2003</a><br>
    <a href="{% url 'year_archive' '2004' %}">special_case_2004</a><br>
    <a href="{% url 'month_archive' y='2004' m='12' %}">special_case_2004_12</a><br>
</body>
</html>

  我们在浏览器地址栏中输入 http://localhost:8000/app/articles/ 访问页面。

URL标签反向解析

  这里我们使用了模板标签,模板标签是用花括号和百分号({% %})表示的,实际上是一小段代码,生成要在网页中显示的信息。这里的的模板标签 {% url 'login' %} 与 app 中的 urls.py 文件中定义的名称 login 的 URL 模式匹配。

  通过模板标签生成 URL,能很容易的确保链接是最新的:只需要修改 urls.py 中的 URL 模式,Django 就会在网页被请求时自动插入修改后的 URL。

五、名称空间

  名称空间(namespace)是标识符的可见范围。一个标识符可在多个名称空间中定义,它在不同名称空间的含义是互不干扰的。这样,在一个新的名称空间中可以定义任何标识符,它们不会与任何已有的标识符发生冲突,因为已有的标识符的定义在其它名称空间中。

  在命令行中,我们使用如下命令在创建一个应用 app01 和 app02:

python manage.py startapp app01
python manage.py startapp app02

  修改项目的主文件夹 Django 中的文件 urls.py 中的请求路径,添加 app01 和 app02 的路由:

from django.contrib import admin
from django.urls import path, re_path, include

urlpatterns = [
    # 路由配置:路径 ---> 视图函数
    path('admin/', admin.site.urls),

    # 路由分发
    re_path(r"^app/", include("app.urls")),
    re_path(r"^app01/", include("app01.urls")),
    re_path(r"^app02/", include("app02.urls")),
]

  由于 name 没有作用域,Django 在反解 URL 时,会在项目全局顺序搜索,当查找第一个 name 指定 URL 时,立即返回。我们在开发项目中,会经常使用 name 属性反解 URL,当不小心在不同的应用的 URL 中定义相同的 name 时,可能会导致 URL 反解错误。

【1】、app01 应用的修改

  app01 应用中新建一个 urls.py 文件:

from django.urls import re_path

from app01 import views

urlpatterns = [
    re_path("index/", views.index, name="index"),
]

  修改 app01 应用中 views.py 文件:

from django.shortcuts import HttpResponse
from django.urls import reverse

# Create your views here.
def index(request):
    # 反向解析
    url = reverse("index")
    return HttpResponse(f"app01 index<br>{url}")

【2】、app02 应用的修改

  app02 应用中新建一个 urls.py 文件:

from django.urls import re_path

from app02 import views

urlpatterns = [
    re_path("index/", views.index, name="index"),
]

  修改 app02 应用中 views.py 文件:

from django.shortcuts import HttpResponse
from django.urls import reverse

# Create your views here.
def index(request):
    url = reverse("index")
    return HttpResponse(f"app02 index<br>{url}")

  我们在浏览器中输入 URL http://localhost:8000/app01/index/ 访问:

反向解析混乱1

  我们在浏览器中输入 URL http://localhost:8000/app02/index/ 访问:

反向解析混乱2

  我们可以用名称空间解决 URL 解析混乱的问题,在项目的主文件夹 Django 中的文件 urls.py 中的为 app01 和 app02 指定 namespace 参数。

from django.contrib import admin
from django.urls import path, re_path, include

urlpatterns = [
    # 路由配置:路径 ---> 视图函数
    path('admin/', admin.site.urls),

    # 路由分发
    re_path(r"^app/", include("app.urls")),
    re_path(r"^app01/", include(("app01.urls", "app01"))),
    re_path(r"^app02/", include(("app02.urls", "app02"))),
]

  修改 app01 应用中 views.py 文件:

from django.shortcuts import HttpResponse
from django.urls import reverse

# Create your views here.
def index(request):
    # 反向解析
    url = reverse("app01:index")
    return HttpResponse(f"app01 index<br>{url}")

  修改 app02 应用中 views.py 文件:

from django.shortcuts import HttpResponse
from django.urls import reverse

# Create your views here.
def index(request):
    url = reverse("app02:index")
    return HttpResponse(f"app02 index<br>{url}")

  我们在浏览器中输入 URL http://localhost:8000/app01/index/ 访问:

解决反向解析混乱1

  我们在浏览器中输入 URL http://localhost:8000/app02/index/ 访问:

解决反向解析混乱2.png

  我们也可以通过 namespace 参数指定名称空间,如果设置 namespace 还需要设置 app_name,否则会报如下错误。

django.core.exceptions.ImproperlyConfigured: 
Specifying a namespace in include() without providing an app_name is not supported. 
Set the app_name attribute in the included module, or pass a 2-tuple containing the list of patterns and app_name instead.

  修改 Django 项目下的 urls.py 文件。

from django.contrib import admin
from django.urls import path, re_path, include

urlpatterns = [
    # 路由配置:路径 ---> 视图函数
    path('admin/', admin.site.urls),

    # 路由分发
    re_path(r"^app01/", include("app01.urls", namespace="app01")),
]

  修改 app01 应用下的 urls.py 文件。

from django.urls import re_path

from app01 import views

app_name = "app01"

urlpatterns = [
    re_path("index/", views.index, name="index"),
]

六、URL转换器

  我们在使用 re_path() 函数捕获的参数都是字符串类型,在视图函数中我们需要用什么类型的数据还需要自己手动转换才行。并且,如果多个路由中都使用相同的正则表达式,一旦规则改变之后,我们还需要手动修改其它使用这些正则表达式的路由。在 Django 2.0 版本中,我们可以通过 path() 解决上述问题。

  修改 app 应用中 urls.py 文件:

from django.urls import re_path, path

from app import views

urlpatterns = [
    # 路由配置:路径 ---> 视图函数
    # 这里URL使用正则表达式,^表示开头匹配,$表示结尾匹配
    re_path(r'^articles/2003/$', views.special_case_2003, name="special_case_2003"),
    # ()表示分组,分组的内容也会发送给视图函数作为形参
    re_path(r'^articles/(\d{4})/$', views.year_archive, name="year_archive"),
    # <>表示分组
    path(articles/<int:y>/<int:m>/', views.month_archive, name="month_archive"),
]

  修改 app 应用中 views.py 文件:

from django.shortcuts import HttpResponse
from django.urls import reverse

# Create your views here.
def special_case_2003(request):
    url = reverse("special_case_2003")
    # HttpResponse 响应对象,将数据返回给浏览器
    return HttpResponse(url)

def year_archive(request, year):
    # 如果反向解析出的内容有正则表达式,则需要替换正则表达式
    url = reverse("year_archive", args=(year,))
    print(year)
    print(type(year))
    return HttpResponse(url)

def month_archive(request, m, y):
    url = reverse("month_archive", args=(y, m))
    print(y)
    print(type(y))
    print(m)
    print(type(m))
    return HttpResponse(url)

  path() 方法中使用尖括号(<>)从 URL 中捕获值,捕获值中可以包含一个转换器类型,比如 <int:name> 捕获一个整形变量。若没有转换器,将匹配字符串。

  Django 默认支持以下 5 个转换器:

转换器 含义
str 匹配除了路径分隔符(/)之外的非空字符
int 匹配正整数,包含 0
slug 匹配字母、数字、横杠、下划线组成的字符串
uuid 匹配格式化的 uuid
path 匹配任何非空字符、包含了路径分隔符

? 符号不能作为匹配路径,因为 ? 会作为 GET 请求的路径和参数的分隔符;

  对于一些复杂或者复用的需要,可以定义自己的转换器。转换器是一个类,它的要求有三点:

  1. regex 类属性,字符串类型
  2. to_python(self, value) 方法,value 是由类属性 regex 所匹配的字符串,返回具体的 Python 变量值,以供 Django 传递到对应的视图函数中;
  3. to_url(self, value) 方法,和 to_python() 方法相反,value 是一个具体的 Python 变量值,返回其字符串,通常用于 URL 反向引用。

  在 app 文件夹中新建一个 convert.py 文件,专门用于存放自定义的转换器。

class MonthConvert:
    regex = "\d{2}"

    def to_python(self, value):
        return int(value)

    # 反向解析时使用
    def to_url(self, value):
        return "%02d" % value

  修改 app 应用中 urls.py 文件:

from django.urls import re_path, path, register_converter

from app import views
from app.convert import MonthConvert

# 注册自定义的转换器
register_converter(MonthConvert, "mm")

urlpatterns = [
    # 路由配置:路径 ---> 视图函数
    # 这里URL使用正则表达式,^表示开头匹配,$表示结尾匹配
    re_path(r'^articles/2003/$', views.special_case_2003, name="special_case_2003"),
    # ()表示分组,分组的内容也会发送给视图函数作为形参
    re_path(r'^articles/(\d{4})/$', views.year_archive, name="year_archive"),
    # <>表示分组
    path(articles/<int:y>/<mm:m>/', views.month_archive, name="month_archive"),
]