1-Django - 路由

bofore

Django2.2 + Python3.6.8
Django2.0 url官档:https://docs.djangoproject.com/zh-hans/2.0/topics/http/urls/

关于前/后置斜杠的说明

urls.py中:

from django.conf.urls import url
from django.contrib import admin
from app01 import views

urlpatterns = [
    url(r'^admin/', admin.site.urls),
    url(r'^index/$', views.index), 
]

如上,前置导航说的是^index之间的/,即'^/index/',前置导航斜杠不需要我们手动写,Django会自动加;后置导航斜杠指的是'^/index/',需要我们手动写。

当我们从浏览器的地址栏访问Djando时,如果地址栏写的是这样的:

http://127.0.0.1:8888/index     # 重定向到带斜杠的路径再访问
http://127.0.0.1:8888/index/    # 正常访问

也就是index后没有/,那么Djando会发一个重定向的请求,告诉浏览器加上/再来访问。

如果要设置不需要带后置导航的/,那么需要在Djando的settings中配置:

# settings.py
APPEND_SLASH = False

默认的,APPEND_SLASH = True 时,先会根据前端传来的url,先检测这个url能不能访问资源,如果可以访问的话,那么就去执行相应的业务代码,最后返回。如果这个url不能访问到资源的话,会判断这个url最后有没有/,有/的话,则返回404错误;如果没有/的话,便会帮你加上/,生成一个新的url,再去检测这个新的url能不能访问到资源,如果能访问的话,则返回301的状态码,并将这个新的url传到前端,进行重定向操作,如果这个新的url(帮你加上/的)还是不能访问到资源的话,也会返回404错误。

那么APPEND_SLASH=False的情况,就不会帮你加/,你前台传怎样的url,那就用这个url去访问资源,能不能访问还得看你url对不对。

如果你APPEND_SLASH = False,那么对应的urls.py中的路径也不需要设置后置的斜杠:

from django.conf.urls import url
from django.contrib import admin
from app01 import views

urlpatterns = [
    url(r'^admin/', admin.site.urls),
    url(r'^index', views.index),
]

但一般我们还是将APPEND_SLASH = True

注意,如果你实操演示时,总是带上后置斜杠,这是浏览器的缓存在作怪,你记得清空下缓存。

有名分组和无名分组

Django中的有名分组和无名分组对应的是正则中的有名和无名分组,这里主要来看其在Django的写法。

无名分组

from django.contrib import admin
from django.urls import path, re_path   # Django2.x版本中,正则匹配要用 re_path
from app01 import views

urlpatterns = [
    path('admin/', admin.site.urls),
    re_path('books/(\d+)/', views.books),  
    # http://127.0.0.1:8000/books/2019/
    # def books(request, y):  # 匹配出来的,都是字符串
    
    re_path('books/(\d+)/', views.books),  
    # http://127.0.0.1:8000/books/2019/7/
    # def books(request, y, m):
]

有名分组

from django.contrib import admin
from django.urls import path, re_path   # Django2.x版本中,正则匹配要用 re_path
from app01 import views

urlpatterns = [
    path('admin/', admin.site.urls),
    re_path('books/(?P<year>\d+)/', views.books),  
    # http://127.0.0.1:8000/books/2019/
    # def books(request, year):   # 参数必须叫做year, 跟路由中的有名分组保持一致
    
    re_path('books/(?P<year>\d+)/(?P<month>\d+)/', views.books),
    # http://127.0.0.1:8000/books/2019/7/
    # def books(request, year, month):
]

小结:

  • 在Django2.x版本中,路由中使用正则要用re_path
  • 无论有名分组还是无名分组,视图中匹配出来的都是字符串。
  • 无名分组中,视图函数接收的参数必须严格按照位置传参来接收。
  • 在有名分组中的名字要和views中的参数要保持一致。
  • 有名分组中,视图函数接收可以按照按关键字传参来接收,也就是year在前还是month在前都无所谓。

别名反向解析

总之一句话, 别名反向解析解决了路由升级,比如/books/v1升级到/books/v2导致的,所有引用这个路径的代码都要跟着修改的问题。

示例:

# http://127.0.0.1:8000/books/

# urls.py
# 在urls中,通过使用name来创建别名
from django.contrib import admin
from django.urls import path, re_path
from app01 import views

urlpatterns = [
    path('admin/', admin.site.urls),
    path('books/', views.books, name='books'),
    # 无名分组
    re_path('add_book/(\d+)/', views.add_book, name='add_book'),
    re_path('t1/(\d+)/(\d+)/', views.t1, name='t1'),

    # 有名分组
    re_path('edit_book/(?P<book_id>\d+)/', views.edit_book, name='edit_book'),
    re_path('t2/(?P<id_1>\d+)/(?P<id_2>\d+)/', views.t2, name='t2'),

]

views.py

from django.shortcuts import render, HttpResponse, redirect
from django.urls import reverse  # 用前先导入


def books(request):
    print(reverse('books'))  # /books/
    # 当url使用无名分组时,使用 arg 跟元组来传参,从左到右按位置传参,多个参数,如下面 t1 示例
    print(reverse('add_book', args=(1,)))  # /add_book/1/
    print(reverse('t1', args=(1, 2)))  # /t1/1/2/
    # 当url有名分组使用 kwargs 跟字典来传参,多个路径写多个键值对,如下面的 t2 示例
    print(reverse('edit_book', kwargs={"book_id": 1}))  # /edit_book/1/
    print(reverse('t2', kwargs={"id_1": 1, "id_2": 2}))  # /t2/1/2/

    return HttpResponse("books")


def add_book(request, id):
    return HttpResponse("add_book")


def edit_book(request, book_id):
    return HttpResponse("edit_book")


def t1(request, id1, id2):
    return HttpResponse("t1")


def t2(request, id_1, id_2):
    return HttpResponse("t2")

另外,redirect中内置了别名反向解析,所以下面两个写哪个都行:

from django.shortcuts import render, HttpResponse, redirect
from django.urls import reverse  # 用前先导入

def books(request):
    # return redirect('books')
    return redirect('/books/')

在HTML页面中使用别名反向解析:

<!-- 无参url反向解析 -->
{% url 'books' %}  
<!-- 有参url反向解析,多个参数以空格分割-->
<!-- 以下两个为无名分组前端的写法,并且都是按位置传参,注意位置!!! -->
{% url 'add_book' book.id %}
{% url 't1' book.id1 book.id2 %}

<!-- 以下为有名分组前端的写法 -->
{% url 't2' id_1=book.id  %}
{% url 't2' id_1=book.id id_2=book.id  %} <!-- 按关键字传参,谁在前谁在后都为无所谓了 -->
{% url 't2' book.id book.id  %}  <!-- 这么写也行,但注意这是按照位置传参,上面是按照关键字传参 -->

路由分发

在多app环境,把所有的路由都配置在项目下的urls.py中,就比较臃肿,就要用到了路由分发来为总路由文件"减负",也就是将属于某一app的路由进行归类。

来看示例。

目前我们只有app01这一个app,所以,需要再创建一个app02

# python manage.py startapp app02

# 然后别忘了注册到settings中
INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'app01.apps.App01Config',  # 注册app01
    'app02.apps.App02Config'   # 注册app02
]

完事之后,再各自app目录下各创建一个urls.py,然后views.py中写个视图函数。

app01的配置:

# app01/urls.py

from django.urls import path, re_path
from app01 import views

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

]


# app01/views.py

from django.shortcuts import render, HttpResponse

def index(request):
    return HttpResponse("app01 index ok")

app02的配置:

# app02/urls.py

from django.urls import path, re_path
from app02 import views

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

]

# app02/views.py

from django.shortcuts import render,HttpResponse

def index(request):
    return HttpResponse("app02 index ok")

项目总路由的配置,即在项目文件下的urls.py

# 项目下的urls.py
from django.contrib import admin
from django.urls import path, re_path, include   # 用前先导入
from app01 import views

urlpatterns = [
    path('admin/', admin.site.urls),
    # 使用include做路由分发
    path('app01/', include('app01.urls')),
    path('app02/', include('app02.urls')),
	# 其他路由还是按照原来的写即可
    path('books/', views.books, name='book'),
]

然后就可以从浏览器访问了:

http://127.0.0.1:8000/app01/index/
http://127.0.0.1:8000/app02/index/

当用户访问路径为app01/index时,会先找到总路由,总路由先匹配路径前缀,然后通过include找到对应的应用下的urls.py文件中的路径,来匹配剩下部分路径,紧接着触发对应的视图函数执行。

注意,在路由分发中,注意路由路由别名的唯一性,如果有别名重复,会用settings中最后一个注册的应用的别名。那有没有更优雅的解决办法?答案是有的,这就要用到了命名空间这个知识点了,往下看!!

命名空间

https://docs.djangoproject.com/zh-hans/2.0/topics/http/urls/#reversing-namespaced-urls

在路由分发中,通过命名空间,来解决别名重名问题。

具体来看怎么用。

在项目总路由urls.py中:

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

urlpatterns = [
    path('admin/', admin.site.urls),
    # # 通过namespace来声明不同的命名空间,一般都是各app的名字
    path('app01/', include('app01.urls'), namespace='app01'),  
    path('app02/', include('app02.urls'), namespace='app02'),
]

在各应用的urls.py中:

# app01/urls.py

from django.urls import path, re_path
from app01 import views

# Django2.x中必须要声明 app_name
app_name = 'app01'

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

]

在视图中这样用:

# app01/views.py

from django.shortcuts import render, HttpResponse, redirect
from django.urls import reverse


def index(request):
    # 根据你定义的名称空间名:别名 来区分
    print(reverse('app01:index'))
    print(reverse('app02:index'))
    return HttpResponse("app01 index ok")

在模板中:

{% url 'app01:index' %}   <!-- 注意,必须声明命名空间后,才能这样用,否则报错 -->

url/path/re_path傻傻分不清楚

这就要说到这个版本差异了啊。
关于路由这块,通常在匹配时,有完全匹配,即匹配普通的字符串;还有就是支持正则的匹配。那么在Django1.x中,url函数同时支持这两种匹配:

from django.conf.urls import url
from django.contrib import admin
from stark.service.v1 import site


urlpatterns = [
    url(r'^admin/', admin.site.urls),
    url(r'^stark/', site.urls),
]

看看源码就知道内部都换成了正则匹配:

但从Django2开始,不这么玩了,它将Django1.x的url的功能拆分了path和re_path:

from django.contrib import admin
from django.urls import path, re_path
from api import views
urlpatterns = [
    path('admin/', admin.site.urls),
    path('index/', views.index),    # 不支持正则
    re_path('^edit/(?P<pk>\d+)/$', views.edit),  # 支持正则
]

路由转发器

django3.2

我们对于需要使用正则的路由,在django2开始,通常使用re_path来解决,当然,我们也可以通过路由转发器来完成。并且可以指定更复杂的正则。
先来看urls.py

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

from api import views


# 自定义路由转换器
class MobileConverter(object):
    regex = "1[3-9]\d{9}"

    def to_python(self, value):
        print(type(value))
        # 将匹配结果传递到视图内部时使用
        # 返回str还是int主要看需求,纯数字的可以返回int
        return value

    def to_url(self, value):
        # 将匹配结果用于反向解析传值时使用
        return value


# register_converter(路由转换器的类名,调用别名)
register_converter(MobileConverter, "mobile")

urlpatterns = [
    path('admin/', admin.site.urls),
    # <正则规则:传递给视图函数的参数>  这两个变量名不必一样
    path("info/<mobile:mobile>", views.info),
    # 下面这个和上面的其实是等价的
    re_path(r"info2/(?P<mobile>1[3-9]\d{9})", views.info)
]

再来看views.py

from django.shortcuts import render, HttpResponse

def info(request, mobile):
    return HttpResponse(f"传过来的的手机号是: {mobile}")

that's all, see also:

python-django中的APPEND_SLASH实现

posted @ 2021-01-04 10:18  听雨危楼  阅读(724)  评论(0编辑  收藏  举报