Django 之路由层

语言: CN / TW / HK

一、Django 请求周期生命流程图

首先,用户在浏览器中输入URL,发送一个GET 或 POST 方法的request 请求。

Django 中封装了socket 的 WSGI 服务器,监听端口接受这个request 请求。

再进行初步封装,然后传送到中间件中,这个request 请求再依次经过中间件。

对请求进行校验或处理,再传输到路由系统中进行路由分发,匹配相对应的视图函数( FBV )。

再将request 请求传输到 views 中的这个视图函数中,进行业务逻辑的处理。

调用modles 中表对象,通过 ORM 拿到数据库(DB)的数据。

同时拿到 templates 中相应的模板进行 渲染 ,然后将这个封装了模板response 响应传输到中间件中。

依次进行处理,最后通过 WSGI 再进行封装处理,响应给浏览器展示给用户。

二、Django 路由配置

路由简单的来说就是根据用户请求的 URL 连接来判断对应的处理程序,并返回处理结果,也就是 URL 与 Django 的视图 建立映射 关系。

Django 路由在 urls.py 配置,urls.py 中的每一条配置对应相应的处理方法。

urls.py 文件

from django.conf.urls import url

# 由一条条映射关系组成的urlpatterns这个列表称之为路由表
urlpatterns = [
     url(regex, view, kwargs=None, name=None), # url本质就是一个函数
]
#函数url关键参数介绍
# regex:正则表达式,用来匹配url地址的路径部分,
        # 例如url地址为:http://127.0.0.1:8001/index/,正则表达式要匹配的部分是index/
# view:通常为一个视图函数,用来处理业务逻辑
# kwargs:略(用法详见有名分组)
# name:略(用法详见反向解析)

案例:

urls.py 文件

from django.conf.urls import url
from django.contrib import admin
from app import views # 导入模块views.py

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

views.py 文件

# 导入HttpResponse,用来生成响应信息
from django.shortcuts import render, HttpResponse

# 新增视图函数index
def index(request):
    return HttpResponse('index page')

测试

python manage.py runserver # 在浏览器输入:http://127.0.0.1:8000/index/ 会看到 index page

1注意一

在浏览器输入: http: //127.0.0.1:8001/index/ ,Django 会拿着路径部分 index/ 去路由表(urls.py文件)中自上而下匹配正则表达式,一旦匹配成功,则会立即执行该路径对应的视图函数,也就是上面路由表(urls.py文件)中的 uelspatterns 列表中的url('^index/$',views.index) 也就是 views.py 视图函数文件的index函数

2注意二

在浏览器中输入: http: //127.0.0.1:8001/index ,Django 同样会拿着路径部分 index 去路由表中自上而下匹配正则表达式,看起来好像是匹配不到正则表达式(r'^index/$' 匹配的是必须以/结尾,所以必会匹配到成功index),但是实际上我们依然在浏览器宽口中看到了 ‘index page’,其原因如下:

在配置文件 settings.py 中有一个参数 APPEND_SLASH ,该参数有连个值True/False

当APPEND_SLASH = True(如果配置文件中没有该配置,则默认值为 True),并且用户请求的 URL 地址的路径部分不是以 / 结尾

例如请求的 URL 地址为:http: //127.0.0.1:8001/index,Django也会拿着部分地址 index 去路由表中匹配正则表达式,发现匹配不成功, 那么Django 会在路径后加/(index/)在去路由表中匹配,去过还匹配不到,会返回路径找不到,如果匹配成功,则会返回 重定向 信息给浏览器,要求浏览器重新向 http: //127.0.0.1:8001/index/ 地址返送请求。

当APPEND_SLASH = False时,则不会执行上述过程,即以但 URL 地址的路径部分匹配失败就 立即 返回路劲未找到,不会做任何的附加操作

# settings.py 
APPEND_SLASH = False

三、路由分组

1无名分组

urls.py 文件

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

urlpatterns = [
    url(r'^admin/', admin.site.urls),
    # 下述正则表达式会匹配url地址的路径部分为:article/数字/
    # 匹配成功的分组部分会以位置参数的形式传给视图函数,有几个分组就传几个位置参数
    url(r'^aritcle/(\d+)/$', views.article), 
]

views.py 文件

from django.shortcuts import render, HttpResponse

# 需要额外增加一个形参用于接收传递过来的分组数据
def article(request,article_id):
    return HttpResponse('id为 %s 的文章内容...' %article_id)

测试

python manage.py runserver

在浏览器输入: http://127.0.0.1:8000/article/1/ 会看到: id为 1 的文章内容... 

2有名分组

urls.py 文件

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

urlpatterns = [
    url(r'^admin',admin.site.urls),
    
    # 下面的正则表达式会匹配url地址的路径部分为:article/数字/
    # 匹配成功的分组部分会以 关键字参数(article_id=..)的形式传给视图函数
    # 有几个分组就传几个关键字参数
    # (\d+)代表匹配数字1-无穷个
    url(r'aritcle/(?P<article_id>\d+)/$', view.article),
]

views.py 文件

from django.shortcuts import render, HttpResponse

# 需要额外增加一个形参,形参名必须为article_id
def article(request, article_id):
    return HttpResponse('id为 %s 的文章内容...' %article_id)

测试

python manage.py runserver

在浏览器输入: http://127.0.0.1:8000/article/1/ 会看到: id为 1 的文章内容...

总结: 有名分组和无名分组都是为了获取路径中的参数, 并传递给视图函数,区别在于无名分组是以 位置参数 的形式传递,有名分组是以 关键字参数 的形式传递。

强调:无名分组和有名分组 不要 混合使用

四、路由分发

随着项目功能的增加,app会越来越多,路由也越来越多,每个app都会有属于自己的路由,如果再将所有的路由都放到一张路由表中,会导致结构不清晰,不便于管理,所以我们应该将app 自己的路由交由自己管理,然后在 总路由 表中做 分发

1创建两个 app,记得 注册

# 新建项目mystie
G:\src\django>django-admin startproject mysite
# 切换到项目目录下
G:\src\django>cd mysite
# 创建app01和app02
G:\src\django\mysite>python manage.py startapp app01
G:\src\django\mysite>python manage.py startapp app02

2在每个app下手动 创建 urls.py 来存放自己的路由

app01 下的urls.py 文件

from django.conf.urls import url
# 导入app01 的views
from app01 import views

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

app01 下的views.py文件

from django.shortcuts import render, HttpResponse

def index(request):
    return HttpResponse('我是app01 的index页面...')

app02下 的urls.py文件

from django.conf.urls import url
# 导入app02的views
from app02 import views

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

app02 下的views.py文件

from django.shortcuts import render, HttpResponse

def index(request):
    return HttpResponse('我是app02 的index页面...')

3在总路由表的 urls.py 文件中(mysite文件夹下的 urls.py)

注意:总路由中,一级路由的后面千万不加 $符号 ,不然不能进行分发路由的操作,表示结束匹配。

from django.conf.urls import url, include
from django.contrib import admin

# 总路由表
# from app01 import urls as app01_urls
# from app02 import urls as app02_urls
urlpatterns = [
    url(r'^admin/', admin.site.urls),

    # 1.路由分发
    # url(r'^app01/',include(app01_urls)),  # 只要url前缀是app01开头 全部交给app01处理
    # url(r'^app02/',include(app02_urls))   # 只要url前缀是app02开头 全部交给app02处理
    
    # 新增两条路由,注意不能以$结尾
    # include 函数就是做分发操作的,当在浏览器输入 http://127.0.0.1:8001/app01/index/ 时
    # 会先进入到总路由表中进行匹配,正则表达式 r'^app01/' 会先匹配成功路径app01/
    # 然后 include 功能会去 app01 下的urls.py 中继续匹配剩余的路径部分
    # 推荐使用
    url(r'^app01/', include('app01.urls')),
    url(r'^app02/', include('app02.urls')),
]

4测试

python manage.py runserver

在浏览器输入: http://127.0.0.1:8000/app01/index/ 会看到:我是app01 的index页面...
在浏览器输入: http://127.0.0.1:8000/app02/index/ 会看到:我是app02 的index页面... 

五、反向解析

在软件开发初期,URL 地址的路径设计可能并不完美,后期需要进行调整,如果项目中很多地方使用了该路径,一旦该路径发生变化,就意味着所有使用该路径的地方都需要进行修改,这是一个非常 繁琐 的操作。

解决方案就是在编写一条 url(regex, view, kwargs=None, name=None) 时,可以通过参数name为 URL 地址的路径部分起一个 别名 ,项目中就可以通过别名来 获取 这个路径。以后无论路径如何变化别名与路径始终保持一致。

上述方案中通过 别名 获取路径的过程称为 反向解析

案例:登录成功跳转到 index.html 页面。

 1在mysite/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'^login/$', views.login, name='login_page'), # 路径login/的别名为login_page
    url(r'^index/$', views.index, name='index_page'), # 路径index/的别名为index_page
]

2在app01/views.py 文件

from django.shortcuts import render, HttpResponse, redirect, reverse # 用于反向解析

def login(request):
    if request.method == 'GET':
        # 当为get 请求时,返回login.html页面,页面中的 {% url 'login_page' %} 会被反向解析成路径:/login/
        return render(request, 'login.html')
	
    # 当为post 请求时,可以从 request.POST 中取出请求体的数据
    name = request.POST.get('name')
    pwd = request.POST.get('pwd')
    if name == 'xyz' and pwd == '123':
        url = reverse('index_page')  # reverse 会将别名 'index_page' 反向解析成路径:/index/       
        return redirect(url) # 重定向到/index/
    else:
        return HttpResponse('用户名或密码错误')

def index(request):
    return render(request, 'index.html')

3templates\login.html 文件

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Login</title>
</head>
<body>
    <!--强调: login_page 必须加引号-->
    <form action="{% url 'login_page' %}" method="post">
        <p>用户名: <input type="text"     name="name"></p>
        <p>密  码: <input type="password" name="pwd"></p>
        <p><input type="submit" value="提交"></p>
    </form>
</body>
</html>

4templates\index.html 文件

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Index</title>
</head>
<body>
    <h3>index page</h3>
</body>
</html>

5测试

python manage.py runserver

在浏览器输入: http://127.0.0.1:8000/login/ 会看到登录页面,输入正确的用户名密码会跳转到 index.html
当我们修改路由表中匹配路径的正则表达式时,程序其余部分均 无需修改

6总结

  • 在 views.py 文件中,反向解析的使用: url = reverse('index_page')
  • 在模版 login.html 文件中,反向解析的使用: {% url 'login_page' %}

7如果路径存在 分组 的反向解析使用

from django.shortcuts import HttpResponse, render, reverse
from django.conf.urls import url
from django.contrib import admin

def index(request, args):
    return HttpResponse('index page')

def user(request, uid):
    return HttpResponse('user page')
    

def home(request):
    # 无名
    index = reverse('index_page', args=(1,))

    # 有名
    user = reverse('user_page', kwargs={'uid': 12})
    
    # 简写
    # user = reverse('user_page', args=(12,))
    return render(request, 'home.html', locals())

urlpatterns = [
    url(r'^admin/', admin.site.urls),
    url(r'^$', home),  # 首页
    url(r'^index/(\d+)/$',       index, name='index_page'),  # 无名分组
    url(r'^user/(?P<uid>\d+)/$', user,  name='user_page'),   # 有名分组
]

8templates\home.html 文件

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>home</title>
</head>
<body>
    <h3>home page</h3>
    <p>{{index}}</p>
    <p>{{user}}</p>
    <p>{% url 'index_page' 1%}</p>
    <p>{% url 'user_page' 12%}</p>
    <p>{% url 'user_page' uid=1 %}</p>
</body>
</html>

无名分组

反向解析出:/index/1/ 这种路径,写法如下 views.py 中。

反向解析的使用

url = reverse('index_page', args=(1,))

在模版 login.html 文件中,反向解析的使用(1 是匹配\d+)

{% url 'index_page' 1 %}

有名分组

反向解析出:/user/1/ 这种路径,写法如下 views.py 中。

反向解析的使用

url = reverse('user_page', kwargs={'uid':1})

在模版 login.html 文件中, 反向解析的使用

{% url 'user_page' uid=1 %}

六、名称空间

当我们的项目下创建了 多个 app,并且每个app下都针对匹配的路径起了 别名 ,如果别名存在 重复 ,那么在反向解析时则会出现 覆盖

1 在每个app下手动创建urls.py 来存放自己的路由,并且为匹配的路径起 别名

app01 下的urls.py 文件

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

urlpatterns = [
    # 为匹配的路径 app01/index/ 起别名 'index_page'
    url(r'^index/$', views.index, name='index_page'), 
]

app02 下的urls.py 文件

from django.conf.urls import url
from app02 import views

urlpatterns = [
    # 为匹配的路径 app02/index/ 起别名 'index_page',与app01 中的别名相同
    url(r'^index/$', views.index, name='index_page'), 
]

2在每个app下的view.py 中编写视图函数,在视图函数中针对别名 ' index_page ' 做反向解析

app01 下的 views.py 文件

from django.shortcuts import render, HttpResponse, reverse

def index(request):
    url = reverse('index_page')
    return HttpResponse('app01 的index页面,反向解析结果为%s' %url)

app02 下的views.py 文件

from django.shortcuts import render, HttpResponse, reverse

def index(request):
    url = reverse('index_page')
    return HttpResponse('app02 的index页面,反向解析结果为%s' %url)

3 在总的urls.py 文件中(mysite 文件夹下的 urls.py)

from django.conf.urls import url, include
from django.contrib import admin

# 总路由表
urlpatterns = [
    url(r'^admin/', admin.site.urls),
    
    # 新增两条路由,注意不能以$结尾
    url(r'^app01/', include('app01.urls')),
    url(r'^app02/', include('app02.urls')),
]

测试

python manage.py runserver

在测试时,无论在浏览器输入: http://127.0.0.1:8001/app01/index/ 还是输入 http://127.0.0.1:8001/app02/index/  针对别名 ' index_page ' 反向解析的结果都是 /app02/index/ ,覆盖了 app01 下别名的解析。

1总urls.py 在路由分发时,指定名称空间

from django.conf.urls import url, include
from django.contrib import admin

# 总路由表
urlpatterns = [
    url(r'^admin/', admin.site.urls),
	
    # 传给include功能一个元组,元组的第一个值是路由分发的地址,第二个值则是我们为名称空间起的名字
    # url(r'^app01/', include(('app01.urls', 'app01'))),
    # url(r'^app02/', include(('app02.urls', 'app02'))),
    
    url(r'^app01/', include(('app01.urls', 'app01'), namespace='app01')),
    url(r'^app02/', include(('app02.urls', 'app02'), namespace='app02'))
]

2修改每个app下的view.py 中视图函数,针对不同名称空间中的别名 ' index_page ' 做反向解析

app01 下的views.py 文件

from django.shortcuts import HttpResponse

def index(request):
    # 解析的是名称空间app01 下的别名 'index_page'
    url = reverse('app01:index_page') 
    return HttpResponse('app01 的index页面,反向解析结果为%s' %url)

app02 下的views.py 文件

from django.shortcuts import HttpResponse

def index(request):
    # 解析的是名称空间app02下的别名'index_page'
    url = reverse('app02:index_page') 
    return HttpResponse('app02 的index页面,反向解析结果为%s' %url)

3测试

python manage.py runserver

浏览器输入: http://127.0.0.1:8000/app01/index/ 反向解析的结果是 /app01/index/

浏览器输入: http://127.0.0.1:8000/app02/index/ 反向解析的结果是 /app02/index/

总结 + 补充

# 在视图函数中基于名称空间的反向解析,用法如下
url = reverse('名称空间的名字:待解析的别名')

# 在模版里基于名称空间的反向解析,用法如下
<a href="{% url '名称空间的名字:待解析的别名'%}">index page</a>

# 其实只要保证名字不冲突 就没有必要使用名称空间

4 namespace 参数

在根目录下的 urls.py 中使用了 include 方法,并且使用了 namespace 参数,如下图

在启动项目时,会报错:'Specifying a namespace in include() without providing an app_name '

这是因为 Django2 相对于 Django1 做了改动,在include 函数里增加了参数 app_name ,表示 app 的名字。

解决方法:

include 中传入该 app 的名字(第二个参数),即

七、Django2.0 版的re_path与path

1re_path

Django2.0 中的 re_pathDjango1.0 的URL一样,传入的第一个参数都是正则表达式

from django.urls      import re_path  # Django3.2 中的re_path
from django.conf.urls import url      # Django3.2 中同样可以导入1.0中的url

urlpatterns = [
    # 用法完全一致
    url(r'^app01/',     include(('app01.urls','app01'))),
    re_path(r'^app02/', include(('app02.urls','app02'))),
]

2path

在Django2.0 中新增了一个 path 功能,用来解决: 数据类型 转换问题与正则表达式 冗余 问题

from django.shortcuts import HttpResponse,
from django.urls import path, re_path

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

# id 关键字参数
def index(request, id):
    print(id, type(id))
    return HttpResponse('index page')

强调

  • path与re_path或者1.0中的 url 的不同之处是,传给 path 的第一个参数不再是正则表达式,而是一个 完全匹配 的路径。相同之处是第一个参数中的匹配字符均无需加前导 斜杠。
  • 使用尖括号(<>)从url中捕获值,相当于有名分组。
  • <>中可以包含一个转化器类型(converter type),比如使用 <int:name> 使用了转换器 int 若果没有转化器,将匹配任何字符串,当然也包括了 / 字符。

除了有默认的五个转换器之外 还支持自定义转换器(了解)

  • str 匹配除了路径分隔符(/)之外的非空字符串,这是默认的形式0。
  • int 匹配正整数,包含0。
  • slug 匹配字母、数字以及横杠、下划线组成的字符串。
  • uuid 匹配格式化的uuid,如 075194d3-6885-417e-a8a8-6c931e272f00。
  • path 匹配任何非空字符串,包含了路径分隔符(/)(不能用?)0。

例如

path('articles/<int:year>/<int:month>/<slug:other>/', views.article_detail)

针对路径 http://127.0.0.1:8000/articles/2009/123/info/
path 会匹配出参数 year=2009,month=123,other='info'  传递给函数 article_detail。

很明显针对月份 month ,转换器 int 是无法精准匹配的,如果我们只想 匹配两个 字符,那么转换器slug也无法满足需求,针对等等这一系列复杂的需要,我们可以定义自己的 转化器 。转化器是一个 接口 ,它的要求有三点:

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

自定义 转换器示例

在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中,使用 register_converter 将其注册到 URL 配置中

from django.urls import path,register_converter
from app01.path_converts import MonthConverter

register_converter(MonthConverter, 'mon')

from app01 import views

urlpatterns = [
    path('articles/<int:year>/<mon:month>/<slug:other>/', views.article_detail, name='xxx'),

]

views.py 文件中的视图函数 article_detail

from django.shortcuts import HttpResponse 

def article_detail(request,year,month,other):
    print(year,  type(year))
    print(month, type(month))
    print(other, type(other))
    print(reverse('xxx', args=(1988, 12, 'info'))) # 反向解析结果/articles/1988/12/info/
    return HttpResponse('article detail page')

测试