【10.0】Django框架之路由层

【零】Django的路由系统

【1】什么是URL配置(URLconf)

  • URL调度器 | Django 文档 | Django (djangoproject.com)

  • URL配置(URLconf)就像Django 所支撑网站的目录。

    • 它的本质是URL与要为该URL调用的视图函数之间的映射表。
  • 你就是以这种方式告诉Django,对于这个URL调用这段代码,对于那个URL调用那段代码。

【2】基本格式

(1)Django1.x版本语法

from django.conf.urls import url

urlpatterns = [
     url(正则表达式, views视图函数,参数,别名),
]

(2)Django2.x+版本语法

from django.urls import path

from . import views

urlpatterns = [
    path('articles/2003/', views.special_case_2003),
    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),
]
  • 注意
    • 要从 URL 中取值,使用尖括号。
    • 捕获的值可以选择性地包含转换器类型。比如,使用 <int:name> 来捕获整型参数。如果不包含转换器,则会匹配除了 / 外的任何字符。
    • 这里不需要添加反斜杠,因为每个 URL 都有。比如,应该是 articles 而不是 /articles

(3)参数说明

from django.conf.urls import url

urlpatterns = [
     path(路径名, views视图函数,参数,别名),
]
  • 路径名:
    • 在浏览器端口后请求的路径名
  • views视图函数:
    • 一个可调用对象,通常为一个视图函数或一个指定视图函数路径的字符串
  • 参数:
    • 可选的要传递给视图函数的默认参数(字典形式)
  • 别名:
    • 一个可选的name参数

【3】请求示例

  • /articles/2005/03/ 会匹配 URL 列表中的第三项。Django 会调用函数 views.month_archive(request, year=2005, month=3)
  • /articles/2003/ 将匹配列表中的第一个模式不是第二个,因为模式按顺序匹配,第一个会首先测试是否匹配。请像这样自由插入一些特殊的情况来探测匹配的次序。在这里 Django 会调用函数 views.special_case_2003(request)
  • /articles/2003 不匹配任何一个模式,因为每个模式要求 URL 以一个斜线结尾。
  • /articles/2003/03/building-a-django-site/ 会匹配 URL 列表中的最后一项。Django 会调用函数 views.article_detail(request, year=2003, month=3, slug="building-a-django-site")

【4】使用正则表达式

  • 如果路径和转化器语法不能很好的定义你的 URL 模式,你可以可以使用正则表达式。如果要这样做,请使用 re_path() 而不是 path()

  • 在 Python 正则表达式中,命名正则表达式组的语法是 (?P<name>pattern) ,其中 name 是组名,pattern 是要匹配的模式。

  • 这里是先前 URLconf 的一些例子,现在用正则表达式重写一下:

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),
]
  • 这实现了与前面示例大致相同的功能,除了:

    • 将要匹配的 URLs 将稍受限制。比如,10000 年将不在匹配,因为 year 被限制长度为4。

    • 无论正则表达式进行哪种匹配,每个捕获的参数都作为字符串发送到视图。

  • 当从使用 path() 切换到 re_path() (反之亦然),要特别注意,视图参数类型可能发生变化,你可能需要调整你的视图。

【一】路由匹配

【1】路径参数相似

# 路由匹配
path('test', views.test),
path('testadd', views.testadd),

无法跳转到 testadd

  • url方法第一个参数是路径参数
  • 只要第一个参数能够匹配到内容,就会立刻停止匹配,执行视图函数

【2】解决路径参数相似问题

# 路由匹配
path('test/', views.test),
path('testadd/', views.testadd),

在参数尾部加一个 /

  • 在输入 url 的时候会默认加一个 /
    • Django内部会帮助我们做一个重定向
      • 一次匹配不行
      • 那就加一个 / 再尝试一次

【3】路由系统自动添加 /

  • 在配置文件中,有一个参数可以帮助我们干这件事
APPEND_SLASH = True
  • 当设置为 True 时,如果请求的 URL 不符合 URLconf 中的任何模式,并且不以斜线结尾,则会发出一个 HTTP 重定向到相同的URL,并附加一个斜线。
  • 注意,重定向可能会导致 POST 请求中提交的任何数据丢失。

APPEND_SLASH 的配置只有在安装了 CommonMiddleware 的情况下才会使用(参见 中间件)。也请参见 PREPEND_WWW

【4】完整的路由匹配

  • 这才是完整版的路由匹配格式
path('test/', views.test),
  • 匹配首页的路由格式
path('', views.test),

【二】无名分组

  • 分组就是将某段正则表达式用()括起来
# 无名分组
re_path(r'^text/(\d+)/', views.test),
def test(request,data):
    print(data)
    return HttpResponse('test')
  • 无名分组就是将括号内正则表达式匹配到的内容当做位置参数传给后面的视图函数

【三】有名分组

  • 可以给正则表达式起一个别名
# 有名分组
re_path(r'^testadd/(?P<year>\d+)', views.testadd),
def testadd(request, year):
    print(year)

    return HttpResponse("testadd")
  • 有名分组就是将括号内正则表达式匹配到的内容当做关键字参数传给后面的视图函数

【四】无名有名混用

  • 无名分组和有名分组不能混用
# 无名有名混合使用
re_path(r'^index/(\d+)/(?P<year>\d+)/', views.index),

# 访问路由获取不到 (\d+) 的内容
  • 但是同一个分组可以使用多次
re_path(r'^test/(\d+)/(\d+)/',views.test),
re_path(r'^test/(?P<xxx>\d+)/(?P<year>\d+)/',views(r'^test/(?P<xxx>\d+)/(?P<year>\d+)/',viewsre_path.test),
def index(request,args,year):
    return HttpResponse("index")

【五】反向解析

【0】引言

(1)基础的URL配置

  • 在实际的Django项目中,经常需要获取某个具体对象的URL,为生成的内容配置URL链接。
  • 比如,我要在页面上展示一列文章列表,每个条目都是个超级链接,点击就进入该文章的详细页面。
  • 现在我们的urlconf是这么配置的:
path('post/<int:pk>/',views.some_view),

(2)问题引入

  • 在前端中,这就需要为HTML的<a>标签的href属性提供一个诸如http://www.xxx.com/post/3/的值。其中的域名部分,Django会帮你自动添加无须关心,我们关注的是post/3/
  • 此时,一定不能硬编码URL为post/3/,那样费时费力、修改困难,而且容易出错。试想,如果哪天,因为某种原因,需要将urlconf中的表达式改成entry/<int:pk>/,为了让链接正常工作,必须修改对应的herf属性值,于是你去项目里将所有的post/.../都改成entry/.../吗?显然这是不现实的!
  • 我们需要一种安全、可靠、自适应的机制,当修改URLconf中的代码后,无需在项目源码中大范围搜索、替换失效的硬编码URL。

(3)解决方案

  • 为了解决这个问题,Django提供了一种解决方案,只需在URL中提供一个name参数,并赋值一个你自定义的、好记的、直观的字符串。
  • 通过这个name参数,可以反向解析URL、反向URL匹配、反向URL查询或者简单的URL反查。

(4)解决方案实现

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

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

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

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

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

from django.urls import path

from . import views

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

[1]前端使用

  • 可以在模板的代码中使用下面的方法获得它们(可以结合模板语法章节来理解下面的内容):
<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>

[2]后端使用

  • 在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,这就会带来麻烦。

  • 为了解决这个问题,又引出了下面的命名空间。

【1】反向解析的本质

  • 先给路由起一个别名,然后,通过一些方法去解析别名,可以得到这个别名对应的路由地址

  • 先给路由与视图函数起一个别名

    re_path(r'index/(\d+)/(\d+)/', views.func,name="index"),
    

【2】后端反向解析

from django.shortcuts import render, HttpResponse,reverse
def home(request):
  reverse('index', args=(1, 2)))

【】前端反向解析

<a href="{% url 'index' 123 234 %}">111</a>

【六】无名有名分组反向解析

  • 本质上还是通过一些方法得到一个结果,该结果可以访问到对应的url从而触发相应的视图和功能

【1】无名分组反向路由解析

(1)引入

1.1 路由

# 无名分组反向解析
re_path(r'^index/(\d+)/', views.index, name='xxx'),

1.2 视图

def home(request):
    print(reverse('xxx')
    return render(request, 'home.html')

1.3 前端

<a href="">{% url 'xxx' %}</a>
  • 我们会发现这样跳转到我们的 xxx 指定路由字段的时候会报错,提示匹配不到我们写的路由表达式
  • 这是因为我们返回数据的时候,因为我们的路由正则表达式是 r'^index/(\d+)/'路由地址后面是需要跟数字的
  • 我们现在在根路由的后面不跟参数,就不能被匹配到,所以主动抛出异常

(2)解决办法

2.1 路由

# 无名分组反向解析
re_path(r'^index/(\d+)/', views.index, name='xxx'),

2.2 视图

def home(request):
    print(reverse('xxx', args=(1,)))
    return render(request, 'home.html')

2.3 前端

<a href="">{% url 'xxx' 123 %}</a>

当我们跳转到指定路由地址的时候,在后面携带参数,这样就可以避免像上面一样,抛出异常,匹配不到

(3)小结

  • 这个数字存在的意义到底是什么?
    • 数字一般情况下放的是数据的主键值
    • 数据的标记和删除功能

通过在根路径的后面拼接指定的 地址 ,从而对这个指定的地址进行数据的更改

# 一个小的实例演示

# 路由层
re_path('edit/<int:pk>/',views.edit,name='edit')

# 视图层
def edit(request,edit_id):
    reverse("edit",args=(edit_id,))

# 前端
{% for user.obj in user_queryset %}
<a href="{% url 'eidt' user_obj.id %}">
{% endfor %}

【2】有名分组反向解析

(1)问题引入

2.1 路由

# 有名分组反向解析
re_path(r'^fuc/(?P<year>\d+)', views.func, name='ooo')

2.2 视图

def func(request,year):
    print(reverse('000')
    return render(request, 'home.html')
  • 问题同上面,还是没有匹配到符合正则表达式的路由
  • 因为我们的正则表达式是 r'^func/(?P<year>\d+)'
  • 理论上我们传入的数据格式应该对year进行指定 func/year=1/

(2)解决方案

2.1 路由

# 有名分组反向解析
re_path(r'^fuc/(?P<year>\d+)', views.func, name='ooo')

2.2 视图

def home(request,year):
    # 有名分组的标准写法
    # print(reverse('ooo', kwargs={"year": 123}))
    # 也可以写成
    year = 123
    print(reverse("ooo",args=(year,))
    return render(request, 'home.html')

2.3 前端

<a href="">{% url 'ooo' year=123 %}</a>有名分组

当我们向指定的形参进行参数传递时,就能被指定的路由匹配表达式所匹配,从而访问到指定的页面

【七】路由分发

【1】前言

  • Django每一个应用都可以拥有属于自己的

    • templates文件夹
    • urls.py
    • static文件夹
  • 正是基于上述的特点,Django可以很好的做到自己的分组开发(每个人只写自己的app)

  • 最后只需要将所有的app拷贝到新的文件,并将这些APP注册到配置文件中,然后再利用路由分发的特点,将所有的APP整合起来

  • 当一个项目中的URL特别多的时候,总路由urls.py的代码非常冗余而且不好维护,这个时候就可以利用路由分发来减轻总路由的压力

  • 利用路由分发之后,总路由不再干预路由与视图函数的直接对应关系

    • 而是做一个分发处理
    • 识别当前的url是属于哪个应用下的,直接分发给对应的应用去处理

【2】语法

  • 通常,我们会在每个app里,各自创建一个urls.py路由模块,然后从根路由出发,将app所属的url请求,全部转发到相应的urls.py模块中。

(1)方式一:转发app下的urls

  • 例如,下面是Django网站本身的URLconf节选。 它包含许多其它URLconf:
from django.urls import include, path   

urlpatterns = [
    # ... 省略...
    path('community/', include('aggregator.urls')),
    path('contact/', include('contact.urls')),
    # ... 省略 ...
]
  • 路由转发使用的是include()方法,需要提前导入,它的参数是转发目的地路径的字符串,路径以圆点分割。
  • 每当Django 遇到include()时,它会去掉URL中匹配的部分并将剩下的字符串发送给include的URLconf做进一步处理,也就是转发到二级路由去。

(2)方式二:转发url列表

  • 另外一种转发其它URLconf的方式是使用一个path()实例的列表。 例如,下面的URLconf:
from django.urls import include, path

from apps.main import views as main_views
from credit import views as credit_views

extra_patterns = [
    path('reports/', credit_views.report),
    path('reports/<int:id>/', credit_views.report),
    path('charge/', credit_views.charge),
]

urlpatterns = [
    path('', main_views.homepage),
    path('help/', include('apps.help.urls')),
    path('credit/', include(extra_patterns)),
]
  • 在此示例中,/credit/reports/URL将由credit_views.report()视图处理。
  • 这种做法,相当于把二级路由模块内的代码写到根路由模块里一起了,不是很推荐。

【3】示例

(1)总路由

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

import app01
import app02

urlpatterns = [
    path('admin/', admin.site.urls),
    path('app01/', include(app01.urls)),
    path('app02/', include(app02.urls)),
]

(2)子路由

# 自己的逻辑路径
from django.contrib import admin
from django.urls import path, re_path, include
from app01 import login

urlpatterns = [
    path('login/', views.login),
]

【4】捕获参数

目的地URLconf会收到来自父URLconf捕获的所有参数,看下面的例子:

# 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,再进一步传递给对应的视图。

【5】向视图传递额外的参数

  • 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模式捕获的命名关键字参数和在字典中传递的额外参数有可能具有相同的名称,这会发生冲突,将使用字典里的参数来替代捕捉的参数。

【6】传递额外的参数给include()

  • 类似上面,也可以传递额外的参数给include()。参数会传递给include指向的urlconf中的每一行。
  • 例如,下面两种URLconf配置方式在功能上完全相同:

(1)配置一

# main.py
from django.urls import include, path

urlpatterns = [
    path('blog/', include('inner'), {'blog_id': 3}),
]

# inner.py
from django.urls import path
from mysite import views

urlpatterns = [
    path('archive/', views.archive),
    path('about/', views.about),
]

(2)配置二

# main.py
from django.urls import include, path
from mysite import views

urlpatterns = [
    path('blog/', include('inner')),
]

# inner.py
from django.urls import path

urlpatterns = [
    path('archive/', views.archive, {'blog_id': 3}),
    path('about/', views.about, {'blog_id': 3}),
]
  • 注意,只有当你确定被include的URLconf中的每个视图都接收你传递给它们的额外的参数时才有意义,否则其中一个以上视图不接收该参数都将导致错误异常。

【八】名称空间

【1】应用命名空间(app_name)

(1)引入

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

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

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

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

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

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

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

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

(2)解决方案

  • 最佳解决问题的办法就是隔离分治,命名空间解耦,这也是常用的设计模式。
  • Django提供了一个叫做app_name的属性,帮我们实现应用级别的命名空间,这样,虽然大家都叫‘大伟’,但一个是‘张大伟’,一个是‘王大伟’,自然就分清楚了甲乙丙丁。
from django.urls import path

from . import views

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

urlpatterns = [
    ...
]

(3)使用方法

  • 我么只需要在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之间是冒号分隔。

【2】实例命名空间namespace

(1)引入

  • 首先我们要知道,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的多个实例!

(2)问题

  • 但这种做法有个明显的问题,就是对于每条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全局唯一。

【3】案例

(1)创建两个app

  • 当我们有两个APP时,如 app01 和 app02
  • 并且在这两个APP下,存在同名的带有反向解析的路由映射关系
# app01/urls.py
from django.urls import path
from app01.views import index

urlpatterns = [
    path("index/", index, name="index_view")
]

# app01/view.py
from django.shortcuts import render, reverse, HttpResponse

def index(request):
    print(f"from app01 view :>>>>> {reverse('index_view')}")
    return HttpResponse("FROM APP01 INDEX_VIEW")
# app02/urls.py
from django.urls import path
from app02.views import index

urlpatterns = [
    path("index/", index, name="index_view")
]

# app02/view.py
from django.shortcuts import render, reverse, HttpResponse

def index(request):
    print(f"from app02 view :>>>> {reverse('index_view')}")
    return HttpResponse("FROM APP02 INDEX VIEW")
# 根路由 urls.py
from django.contrib import admin
from django.urls import path
from django.urls.conf import include

urlpatterns = [
    path('admin/', admin.site.urls),
    # 路由分发,将 app01 的前缀的路由分发
    path("app01/", include(arg="app01.urls")),
    # 路由分发,将 app02 的前缀的路由分发
    path("app02/", include(arg="app02.urls")),
]

(2)启动Django访问路由

  • 此时启动Django项目,
  • 分别访问这两个路由地址
# http://127.0.0.1:8000/app01/index

# http://127.0.0.1:8000/app02/index
  • 此时查看控制台输出,并查看当前解析到的同名的路由名是否一致
# app02/views.py
# from app02 view :>>>> /app02/index/
# [28/Feb/2024 05:08:02] "GET /app02/index/ HTTP/1.1" 200 21

# app01/views.py
# from app01 view :>>>>> /app02/index/
# [28/Feb/2024 05:08:06] "GET /app01/index/ HTTP/1.1" 200 21
  • 此时我们会发现,Django自带的反向解析并没有自动识别到各自的路由映射关系上,而是都解析到了位于最后位置的解析上
  • 即Django不会自动解析当前路由前缀

(3)解决路由解析一致问题

[1]解决方案一

  • 解决的思路就是给各自的app创建一个名称空间,让彼此解析只在自己的名称空间内解析
# app01/urls.py
from django.urls import path
from app01.views import index

# 在路由分发文件中给当前APP创建一个app的应用命名空间
app_name="app01"

urlpatterns = [
    path("index/", index, name="index_view")
]

# app01/view.py
from django.shortcuts import render, reverse, HttpResponse

def index(request):
  	# 解析路由映射关系时指定当前APP名字
    print(f"from app01 view :>>>>> {reverse('app01:index_view')}")
    return HttpResponse("FROM APP01 INDEX_VIEW")
# app02/urls.py
from django.urls import path
from app02.views import index

# 在路由分发文件中给当前APP创建一个app的应用命名空间
app_name="app02"

urlpatterns = [
    path("index/", index, name="index_view")
]

# app02/view.py
from django.shortcuts import render, reverse, HttpResponse

def index(request):
  	# 解析路由映射关系时指定当前APP名字
    print(f"from app02 view :>>>> {reverse('app02:index_view')}")
    return HttpResponse("FROM APP02 INDEX VIEW")
# 根路由 urls.py
from django.contrib import admin
from django.urls import path
from django.urls.conf import include
urlpatterns = [
    path('admin/', admin.site.urls),
 	  # 路由分发,将 app01 的前缀的路由分发
    # 给当前路由分发指定实例名称空间
    path("app01/", include(arg="app01.urls", namespace="app01")),
    # 路由分发,将 app02 的前缀的路由分发
  	# 给当前路由分发指定实例名称空间
    path("app02/", include(arg="app02.urls", namespace="app02")),
]
  • 再次访问前端路由
# http://127.0.0.1:8000/app01/index

# http://127.0.0.1:8000/app02/index
  • 此时输出如下
# app02/views.py
# from app02 view :>>>> /app02/index/
# [28/Feb/2024 05:08:02] "GET /app02/index/ HTTP/1.1" 200 21

# app01/views.py
# from app01 view :>>>>> /app01/index/
# [28/Feb/2024 05:08:06] "GET /app01/index/ HTTP/1.1" 200 21
  • 我们会发现这两个app解析到了各自的名称空间中的路由映射关系

[2]解决方案二

  • 解决的思路是在总路由分发时给定应用名称空间
# app01/urls.py
from django.urls import path
from app01.views import index

urlpatterns = [
    path("index/", index, name="index_view")
]

# app01/view.py
from django.shortcuts import render, reverse, HttpResponse

def index(request):
  	# 解析路由映射关系时指定当前APP名字
    print(f"from app01 view :>>>>> {reverse('app01:index_view')}")
    return HttpResponse("FROM APP01 INDEX_VIEW")
# app02/urls.py
from django.urls import path
from app02.views import index

urlpatterns = [
    path("index/", index, name="index_view")
]

# app02/view.py
from django.shortcuts import render, reverse, HttpResponse

def index(request):
    # 解析路由映射关系时指定当前APP名字
    print(f"from app02 view :>>>> {reverse('app02:index_view')}")
    return HttpResponse("FROM APP02 INDEX VIEW")
# 根路由 urls.py
from django.contrib import admin
from django.urls import path
from django.urls.conf import include
urlpatterns = [
    path('admin/', admin.site.urls),
 	  # 路由分发,将 app01 的前缀的路由分发
    # 给当前路由分发指定实例名称空间
    path("app01/", include(arg=("app01.urls","app01"), namespace="app01")),
    # 路由分发,将 app02 的前缀的路由分发
  	# 给当前路由分发指定实例名称空间
    path("app02/", include(arg=("app02.urls","app02"), namespace="app02")),
]
  • 再次访问前端路由
# http://127.0.0.1:8000/app01/index

# http://127.0.0.1:8000/app02/index
  • 此时输出如下
# app02/views.py
# from app02 view :>>>> /app02/index/
# [28/Feb/2024 05:08:02] "GET /app02/index/ HTTP/1.1" 200 21

# app01/views.py
# from app01 view :>>>>> /app01/index/
# [28/Feb/2024 05:08:06] "GET /app01/index/ HTTP/1.1" 200 21
  • 我们会发现这两个app解析到了各自的名称空间中的路由映射关系

(4)补充:前端解析方法

  • 同后端解析方式一样,只需要指定上指定的APP名字即可完成反向解析
{% url 'app01:index' %}
{% url 'app02:index' %}

【九】伪静态

  • 静态网页
    • 数据是写死的
  • 伪静态
    • 将一个动态网页伪装成静态网页
  • 伪装的目的在于增大本网站的seo查询力度
    • 并且增加搜索引擎收藏本网页的概率
  • 搜索引擎本质上就是一个巨大的爬虫程序

总结:无论怎么优化,怎么处理,始终还是干不过RMB玩家

【十】虚拟环境

  • 在正常开发中,我们会给每一个项目独有的解释器环境
  • 该环境内只有该项目用到的模块,用不到的一概不装

【1】虚拟环境

  • 每创建一个虚拟环境就类似于重新下载了一个纯净的python解释器
  • 但是虚拟器不建议下载太多,创建虚拟环境是需要消耗磁盘空间的

【2】模块管理文件

  • 每个项目都需要用到很多模块,并且每个模块的版本可能是不一样的

  • 这种情况下我们会给每一个项目配备一个requirements.txt文件,里面存放着我们这个项目所安装的所有模块及版本

  • 只需要一条命令即可安装所有模块及版本

【3】模块文件导出和安装

(1)导出项目模块文件

pip freeze > requirements.txt
  • 这行命令的含义是 "freeze"(冻结)当前环境中的所有已安装包及其版本信息,并将其输出重定向到 requirements.txt 文件中。
  • 执行此命令后,requirements.txt 将列出你项目所需的全部依赖及其对应版本号。

(2)安装项目模块文件

pip install -r requirements.txt
  • 这将按照 requirements.txt 中列出的顺序及版本要求安装相应的 Python 包。

【十一】Django版本的区别

【1】路由匹配规则

  • Django1.x路由层使用的是url方法
  • 在Django2.x版本以后在路由层使用的是path方法
    • url() 第一个参数支持正则
    • path() 第一个参数不支持正则,写什么就匹配到什么

【2】正则匹配规则

  • 在Django2.x以后也可以使用正则表单式,但是使用的方法是re_path
from django.urls import path, re_path

re_path(r'^fuc/(?P<year>\d+)', views.func)

# 等价于

url(r'^fuc/(?P<year>\d+)', views.func)

【3】路径转换器

(1)初识

  • 虽然path不支持正则,但是其内部支持五种转换器
path('index/<int:id>/',views.index)
# 将第二个路由里面的内容先转成整型,然后以关键字的形式传递给后面的视图函数
from django.urls import path,re_path

from app01 import views

urlpatterns = [
    # 问题一的解决方案:
    path('articles/<int:year>/', views.year_archive), 
    # <int:year>相当于一个有名分组,其中int是django提供的转换器,相当于正则表达式,专门用于匹配数字类型,而year则是我们为有名分组命的名,并且int会将匹配成功的结果转换成整型后按照格式(year=整型值)传给函数year_archive

    # 问题二解决方法:用一个int转换器可以替代多处正则表达式
    path('articles/<int:article_id>/detail/', views.detail_view), 
    path('articles/<int:article_id>/edit/', views.edit_view),
    path('articles/<int:article_id>/delete/', views.delete_view),
]

(2)五种转换器

  • str
    • 匹配除了 '/' 之外的非空字符串。
    • 如果表达式内不包含转换器,则会默认匹配字符串。
  • int
    • 匹配 0 或任何正整数。返回一个 int 。
  • slug
    • 匹配任意由 ASCII 字母或数字以及连字符和下划线组成的短标签。
    • 比如,building-your-1st-django-site 。
  • uuid
    • 匹配一个格式化的 UUID 。为了防止多个 URL 映射到同一个页面,必须包含破折号并且字符都为小写。
    • 比如,075194d3-6885-417e-a8a8-6c931e272f00。返回一个 UUID 实例。
  • path
    • 匹配非空字段,包括路径分隔符 '/' 。
    • 它允许你匹配完整的 URL 路径而不是像 str 那样匹配 URL 的一部分。

【4】自定义转换器

  • Django支持自定义转换器

(1)初识

  • 转换器是一个类,包含如下内容:

    • 字符串形式的 regex 类属性。

    • to_python(self, value) 方法,用来处理匹配的字符串转换为传递到函数的类型。如果没有转换为给定的值,它应该会引发 ValueErrorValueError 说明没有匹配成功,因此除非另一个 URL 模式匹配成功,否则会向用户发送404响应。

    • 一个 to_url(self, value) 方法,它将处理 Python 类型转换为字符串以用于 URL 中。如果不能转换给定的值,它应该引发 ValueErrorValueError 被解释为无匹配项,因此 reverse() 将引发 NoReverseMatch,除非有其他 URL 模式匹配。

Changed in Django 3.1:

支持引发 ValueError 以表示没有匹配项被添加。

(2)自定义转换器示例

  • 在app01下新建文件 path_ converters.py(文件名可以随意命名)

    class FourDigitYearConverter:
        # 此属性名为正则表达式(regex),用于匹配四位数的年份字符串
        regex = r'[0-9]{4}' 
    
        def to_python(self, value):
            """
            将接收到的字符串值解析为Python整数类型表示四位数的年份。
            示例:输入 "2024" 会转换为 2024
            """
            return int(value)
    
        def to_url(self, value):
            """
            根据给定四位数年份(value)将其格式化为URL安全的四位数字形式,例如 "2024" -> "2024".
            注意这里的 "匹配的regex是四个数字" 应更改为 "此方法针对四位数的年份字符串"
            
            例如:输入 "2024" 会返回 "2024", 保持四位数且无前导零。
            """
            return '%04d' % value
    

(3)使用自定义转换器

  • 在urls.py中,使用register_converter() 将其注册到URL配置中:

    # 引入所需的模块和从当前目录下的views.py导入视图函数
    from django.urls import path, register_converter
    from . import converters, views
    
    # 在Django应用中注册自定义的日期转换器类,即位于converters文件中的FourDigitYearConverter
    # 并为其指定别名 'yyyy', 这个别名可以方便地在URL模式中引用该转换器
    register_converter(converters.FourDigitYearConverter, 'yyyy')
    
    # 配置URL模式,其中:
    #   path('/articles/2003/', views.special_case_2003) - 特殊情况处理,当访问 '/articles/2003/' 时调用 special_case_2003 视图函数
    #   path('articles/<yyyy:year>/', views.year_archive) - 处理常规年份归档页面请求,
    #       其中 '<yyyy:year>' 表示 URL 模板中 '<year>' 前面带有 'yyyy' 转换器别名,
    #       Django将在实际匹配到的 URL 字符串中找到四位数的年份并传递给 views.year_archive 视图函数作为参数
    
    urlpatterns = [
        path('articles/2003/', views.special_case_2003),
        path('articles/<yyyy:year>/', views.year_archive),
        ...
    ]
    
    

(4)转换器使用示例

  • app01/path_converters.py
class MonthConverter:
    regex = '\d{2}'  # 属性名必须为regex

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

    def to_url(self, value):
        return value  # 匹配的regex是两个数字,返回的结果也必须是两个数字
  • urls.py
from django.contrib import admin
from django.urls import path
from app01 import views

register_converter(converters.FourDigitYearConverter, 'yyyy')

urlpatterns = [
    path('admin/', admin.site.urls),
    path('articles/<int:year>/<int:month>/<slug:other>/', views.article_detail)
    # 针对路径http://127.0.0.1:8000/articles/2009/123/hello/,path会匹配出参数year=2009,month=123,other='hello'传递给函数article_detail
]
  • app01/views.py

    from django.shortcuts import render,HttpResponse,reverse
    from django.urls import reverse
    
    def article_detail(request, year, month, other):
        print(year, type(year))
        print(month, type(month))
        print(other, type(other))
        print(reverse('article_detail', args=(1988, 12, 'hello')))  # 反向解析结果/articles/1988/12/hello/
    
        '''
        2009 <class 'int'>
        12 <class 'int'>
        hello <class 'str'>
        /articles/1988/12/hello/
        '''
        return HttpResponse('xxxx')
    
  • 测试

    # 在浏览器输入http://127.0.0.1:8000/articles/2009/12/hello/
    # path会成功匹配出参数year=2009,month=12,other='hello'传递给函数article_detail
    
    # 在浏览器输入http://127.0.0.1:8000/articles/2009/123/hello/
    # path会匹配失败,因为我们自定义的转换器mon只匹配两位数字,而对应位置的123超过了2位
    

【5】级联更新

  • 模型层在1.x外键默认都是级联更新,级联删除的
  • 但是2.x 版本以后需要自己手动配置参数
posted @ 2024-03-18 22:58  Chimengmeng  阅读(22)  评论(0编辑  收藏  举报
/* */