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: