第三节: 视图层

视图层

)

一、请求与响应

视图函数

视图函数,简称视图,属于Django的视图层,默认定义在views.py文件中,是用来处理web请求信息以及返回响应信息的函数,所以研究视图函数只需熟练掌握两个对象即可:请求对象(HttpRequest)和响应对象(HttpResponse)

官网地址:https://docs.djangoproject.com/en/1.11/ref/request-response/

1、请求对象

当一个页面被请求时,django会创建一个包含本次请求原信息(如http协议请求报文中的请求行、首部信息、内容主体)的HttpRequest对象。

之后,django会找到匹配的视图,将该对象传给视图函数的第一个参数,约定俗称该参数名为request(类似于我们自定义框架的environ参数)。

在视图函数中,通过访问该对象的属性便可以提取http协议的请求数据

2、HttpRequest对象属性part1

一.HttpRequest.method
  获取请求使用的方法(值为纯大写的字符串格式)。例如:"GET"、"POST"
   应该通过该属性的值来判断请求方法

   在视图函数中:
   if request.method. == "GET":
       ...
   if request.method == "POST":
       ...

二.HttpRequest.GET
  值为一个类似于字典的QueryDict对象,封装了GET请求的所有参数,可通过HttpRequest.GET.get('键')获取相对应的值

   在视图函数中:
   request.GET.get('name')

三.HttpRequest.POST
   值为一个类似于字典的QueryDict对象,封装了POST请求所包含的表单数据,可通过HttpRequest.POST.get('键')获取相对应的值

   在视图函数中:
   request.POST.get('name')

   针对表单中checkbox类型的input标签、select标签提交的数据,键对应的值为多个,需要用:HttpRequest.POST.getlist("hobbies")获取存有多个值的列表,同理也有HttpRequest.GET.getlist("键")
   针对有多个值的的情况,也可以用HttpRequest.GET.get("键"),默认会获取列表中的最后一个值

案例:

urls.py

from django.urls import re_path
from app01 import views

urlpatterns = [
    re_path(r'^login/$',views.login),
]

Views.py

from django.shortcuts import render,HttpResponse

def login(request):
    if request.method == 'GET':
        # 当请求url为:http://127.0.0.1:8001/login/?a=1&b=2&c=3&c=4&c=5
        # 请求方法是GET,?后的请求参数都存放于request.GET中
        print(request.GET)
        # 输出<QueryDict: {'a': ['1'], 'b': ['2'], 'c': ['3', '4', '5']}>

        # 获取?后参数的方式为
        a=request.GET.get('a') # 1
        b=request.GET.get('b') # 2
        c=request.GET.getlist('c') # ['3', '4', '5']
        c1=request.GET.get('c') # 5

        return render(request,'login.html')
    elif request.method == 'POST':
        # 在输入框内输入用户名egon、年龄18,选择爱好,点击提交
        # 请求方法为POST,表单内的数据都会存放于request.POST中
        print(request.POST) 
        # 输出<QueryDict: {..., 'name': ['egon'], 'age': ['18'], 'hobbies': ['music', 'read']}>

        # 获取表单中数据的方式为
        name=request.POST.get('name') # egon
        age=request.POST.get('age') # 18
        hobbies=request.POST.getlist('hobbies') # ['music', 'read']

        return HttpResponse('提交成功')

在templates目录下新建login.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>登录页面</title>
</head>
<body>

<!--
method="post"代表在提交表单时会以POST方法提交表单数据
action="/login/" 代表表单数据的提交地址为http://127.0.0.1:8001/login/,可以简写为action="/login/",或者action=""
-->
<form action="http://127.0.0.1:8001/login/" method="post">
    {% csrf_token %} <!--强调:必须加上这一行,后续我们会详细介绍-->
    <p>用户名:<input type="text" name="name"></p>
    <p>年龄:<input type="text" name="age"></p>
    <p>
        爱好:
        <input type="checkbox" name="hobbies" value="music">音乐
        <input type="checkbox" name="hobbies" value="read">阅读
        <input type="checkbox" name="hobbies" value="dancing">跳舞
    </p>
    <p><input type="submit" value="提交"></p>

</form>
</body>
</html>

3、HttpRequest对象属性part2

一.HttpRequest.path
  获取url地址的路径部分,只包含路径部分

二.HttpRequest.get_full_path()
  获取url地址的完整path,既包含路径又包含参数部分

如果请求地址是http://127.0.0.1:8001/order/?name=egon&age=10#_label3,
HttpRequest.path的值为"/order/"
HttpRequest.get_full_path()的值为"/order/?name=egon&age=10"

案例:

urls.py

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

urlpatterns = [
    re_path(r'^order',views.order),
]

views.py

from django.shortcuts import render,HttpResponse

# 针对请求的url地址:http://127.0.0.1:8001/order/?name=egon&age=10#_label3
# 从域名后的最后一个“/”开始到“?”为止是路径部分,即/order/
# 从“?”开始到“#”为止之间的部分为参数部分,即name=egon&age=10
def order(request):
    print(request.path) # 结果为“/order/”
    print(request.get_full_path()) # 结果为"/order/?name=egon&age=10"

    return HttpResponse('order page')

4、HttpRequest对象属性part3

一.HttpRequest.META.get(...)
   值为包含了HTTP协议的请求头数据的Python字典,字典中的key及期对应值的解释如下
    CONTENT_LENGTH —— 请求的正文的长度(是一个字符串)。
    CONTENT_TYPE —— 请求的正文的MIME类型。
    HTTP_ACCEPT —— 响应可接收的Content-Type。
    HTTP_ACCEPT_ENCODING —— 响应可接收的编码。
    HTTP_ACCEPT_LANGUAGE —— 响应可接收的语言。
    HTTP_HOST —— 客服端发送数据的目标主机与端口
    HTTP_REFERER —— Referring 页面。
    HTTP_USER_AGENT —— 客户端使用的软件版本信息
    QUERY_STRING —— 单个字符串形式的查询字符串(未解析过的形式)。
    REMOTE_ADDR —— 客户端的IP地址。
    REMOTE_HOST —— 客户端的主机名。
    REMOTE_USER —— 服务器认证后的用户。
    REQUEST_METHOD —— 一个字符串,例如"GET" 或"POST"。
    SERVER_NAME —— 服务器的主机名。
    SERVER_PORT —— 服务器的端口(是一个字符串)。
   从上面可以看到,除 CONTENT_LENGTH 和 CONTENT_TYPE 之外,HTTP协议的请求头数据转换为 META 的键时,
    都会
    1、将所有字母大写
    2、将单词的连接符替换为下划线
    3、加上前缀HTTP_。
    所以,一个叫做 X-Bender 的头部将转换成 META 中的 HTTP_X_BENDER 键。

二、HttpRequest.encoding
  一个字符串,表示提交的数据的编码方式(如果为 None 则表示使用 DEFAULT_CHARSET 的设置,默认为 'utf-8')。
   这个属性是可写的,你可以修改它来修改访问表单数据使用的编码。
   接下来对属性的任何访问(例如从 GET 或 POST 中读取数据)将使用新的 encoding 值。
   如果你知道表单数据的编码不是 DEFAULT_CHARSET ,则使用它。

三、HttpRequest.scheme
   表示请求方案的字符串(通常为http或https)

四、其他
HttpRequest还有很多其他重要属性,比如HttpRequest.body,待我们讲到专门的知识点时再专门详细讲解

5、响应对象

请求对象HttpRequest是由django为我们创建好的,直接使用即可,而响应对象则需要我们负责创建。我们编写的每个视图都应该返回一个HttpResponse对象,响应可以是一个网页的HTML内容,一个重定向,一个404错误,一个XML文档,或者一张图片。用来返回响应对象的常用类如下

from django.shortcuts import HttpResponse
from django.shortcuts import redirect
from django.shortcuts import render

from django.http importJsonResponse

6、HttpResponse

传递字符串

from django.http import HttpResponse
response = HttpResponse("Here's the text of the Web page.")
response = HttpResponse("Text only, please.", content_type="text/plain")

'''
ps:Content-Type用于指定响应体的MIME类型

MIME类型:
mime类型是多用途互联网邮件扩展类型。是设定某种扩展名的文件用一种应用程序来打开的方式类型,当该扩展名文件被访问的时候,浏览器会自动使用指定应用程序来打开

MIME 类型有非常多种,一般常见的有:

  text/html:浏览器在获取到这种文件时会自动调用html的解析器对文件进行相应的处理。

  text/plain:意思是将文件设置为纯文本的形式,浏览器在获取到这种文件时并不会对其进行处理。

  image/jpeg:JPEG格式的图片

  image/gif:GIF格式的图片

  video/quicktime:Apple 的 QuickTime 电影

  application/vnd.ms-powerpoint:微软的powerpoint文件
'''

依次增加字符串(了解)

>>> response = HttpResponse()
>>> response.write("<p>Here's the text of the Web page.</p>")
>>> response.write("<p>Here's another paragraph.</p>")

传递迭代器对象(了解)

# 可以为HttpResponse传递可迭代对象,HttpRespone会将其依次迭代然后存储成字符串,类似文件或者其他带有close()方法的生成器对象,会自动调用close()关闭

def index(request):
    f=open(r'a.txt',mode='r',encoding='utf-8') # a.txt文件内容为111
    response=HttpResponse(f)
    print(response.content) # 打印内容为: b'111'

    # f.read() # 如果执行,则会抛出异常,说明文件已经关闭了,无法读取
return response

设置或删除响应头信息

response = HttpResponse()
response['Content-Type'] = 'text/html; charset=UTF-8'
response['X-Frame-Options'] = 'SAMEORIGIN'
del response['Content-Type']

属性

HttpResponse.status_code:响应的状态码
HttpResponse.charset:响应内容的编码encode格式
HttpResponse.content:响应内容

字符串编码:

# 编码设定原则:编码格式与解码格式保持一致
response = HttpResponse(charset='gbk',content_type='text/html; charset=gbk')

# 参数charset='gbk'指定响应体内容response.content的编码格式
# 参数content_type='text/html; charset=gbk'是设置响应头,用于告诉浏览器响应体内容response.content应该采用何种解码格式,注意:设置时必须加上内容的类型,如text/html

# 强调:
# 如果在实例化HttpResponse时没有指定charset=gbk',将会采用与content_type中设定的解码格式一样的编码,这样统一起来就不会出现乱码问题
# 如果也没有指定content_type,那么django默认会读取配置文件中的配置settings.py.DEFAULT_CHARSET来作为默认的编码与解码格式

def index(request):
    response = HttpResponse(charset='gbk',content_type='text/html; charset=gbk')
    response.charset='gbk' # 实例化后,可以对该属性重新赋值,只要是在write内容前,就会以最新赋值的编码格式为准

    response.write("<p>Hello egon美男子</p>")
    response.write("<p>Hello 林sb</p>")

    # 如果实例化处未指定响应头Content-Type,也可以在此处设置,在一处就可以了,无需重复设置
    response['Content-Type']='text/html; charset=gbk'  
    # response['Content-Type']='text/html; charset=UTF-8' 

    print(response.charset)
    print(response.content) 
    return response

7、render

def render(request, template_name, context=None, content_type=None, status=None, using=None):
    """
    Return a HttpResponse whose content is filled with the result of calling
    django.template.loader.render_to_string() with the passed arguments.
    """
    content = loader.render_to_string(template_name, context, request, using=using)
    return HttpResponse(content, content_type, status)

参数:
    1、request:用于生成响应的请求对象,固定必须传入的第一个参数

    2、template_name:要使用的模板的完整名称,必须传入,render默认会去templates目录下查找模板文件

    3、context:可选参数,可以传入一个字典用来替换模块文件中的变量,默认是一个空字典。如果字典中的某个值是可调用的,视图将在渲染模板之前调用它。

    4、content_type:生成的文档要使用的MIME类型。默认为 DEFAULT_CONTENT_TYPE 设置的值。默认为'text/html'

    5、status:响应的状态码。默认为200。

    6、using: 用于加载模板的模板引擎的名称。

综上,render的功能可以总结为:根据给定字典渲染模板文件,并返回一个渲染后的HttpResponse对象。简单示例如下

urls.py新增路由

from django.urls import re_path
from app01 import views

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

views.py内容如下

from django.shortcuts import render

def index(request):
    return render(request, 'index.html', {'name': 'egon', 'tag': 'dsb'})

# 上述代码等同于
from django.shortcuts import render
from django.template import loader

def index(request):
    t = loader.get_template('index.html')
    dic = {'name': 'egon', 'tag': 'dsb'}

    return HttpResponse(t.render(dic, request))

templates目录下新增文件index.html,内容如下

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<p>{{ name }}</p>
<p>{{ tag }}</p>
</body>
</html>

测试

#启动django,在浏览器中访问url地址http://ip地址:端口号/,会看到{{ name }} {{ tag }}被替换成了egon和dsb

8、redirect

返回重定向对象,返回的状态码为302,第一个参数用来指定浏览器重定向的地址,可以是
#1、一个完全标准的URL地址,如'https://www.yahoo.com/search/'
#2、也可以是一个没有域名的绝对路径,如'/search/'
#3、或者是一个没有域名的相对路径,如'search/',与1、2直接跳转到指定的绝对路径不同,相对路径需要先与当前路径进行拼后才能跳转,例如:如果当前路径为http://127.0.0.1:8080/index/,拼接后的路径为http://127.0.0.1:8080/index/search/

# ps:redirect重定向等同于下述操作
def index(request):
    response=HttpResponse()

    response.status_code=302 # 必须设置响应的状态码,才能重定向
    response['Location']='/register/' # 设置响应头

    return response

示例如下

urls.py新增路由

from django.urls import re_path
from app01 import views

urlpatterns = [
    re_path(r'^index/$',views.index),
    re_path(r'^login/$', views.login),

]

views.py内容如下

from django.shortcuts import HttpResponse,redirect

def index(request):
    return redirect('/login/') # 跳转到http://127.0.0.1:8000/login/
    # return redirect('login/') # 跳转到http://127.0.0.1:8000/index/login/

def login(request):
    return HttpResponse("login page")

测试:

启动django,在浏览器中访问url地址http://ip地址:端口号/index/,跳转到login页面

重定向转态码301与302的区别(了解)

一、301和302的异同。
   1、相同之处:
   301和302状态码都表示重定向,具体点说就是浏览器在拿到服务器返回的这个状态码后会自动跳转到一个新的URL地址(浏览器会从响应头Location中获取新地址),用户看到的效果都是输入地址A后瞬间跳转到了另一个地址B

   2、不同之处:
  301表示永久重定向,旧地址A的资源已经被永久地移除了,即这个资源不可访问了。
  302表示临时重定向,旧地址A的资源还在,即这个资源仍然可以访问。

    A页面永久重定向到B页面,那搜索引擎收录的就是B页面。
    A页面临时重定向到B页面,那搜索引擎收录的就是A页面。
    从SEO层面考虑,302要好于301

二、重定向原因:
   1、网站调整(如改变网页目录结构);
   2、网页被移到一个新地址;
   3、网页扩展名改变(如应用需要把.php改成.Html或.shtml)。
      这种情况下,如果不做重定向,则用户收藏夹或搜索引擎数据库中旧地址只能让访问客户得到一个404页面错误信息,访问流量白白丧失;再者某些注册了多个域名的网站,也需要通过重定向让访问这些域名的用户自动跳转到主站点等。

9、JsonResponse

向前端返回一个json格式字符串的两种方式

方式一:

import json

def my_view(request):
    data=['egon','kevin']
    return HttpResponse(json.dumps(data) )

方式二:

from django.http import JsonResponse

def my_view(request):
    data=['egon','kevin']
    return JsonResponse(data,safe=False)
    #默认safe=True代表只能序列化字典对象,safe=False代表可以序列化字典以外的对象

二、Django视图层-定制错误页面

1、django内置异常处理视图

Web应用在线上运行的过程中出现错误,是不可避免的,常见错误如

1、400:400 Bad Request 是由于明显的客户端错误(例如,格式错误的请求语法,太大的大小,无效的请求消息或欺骗性路由请求),服务器不能或不会处理该请求。
2、403:用户没有访问某一资源的权限
3、404:请求的url地址不存在
4、500:服务端出错

django框架为其中的一些常见错误提供了标准的错误页面,但它们仅限于DEBUG=True调试模式下使用,要想符合生产环境的需求,则需要我们进行自定制,配置过程如下

步骤1:在templates目录下的顶层创建错误页面400.html、403.html、404.html、500.html,注意文件内容自定义,但文件名与放置位置不可改变

templates/400.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<h1>自定义400页面</h1>
</body>
</html>

templates/403.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<h1>自定义403页面</h1>
<p>
    {{ exception }}
</p>
</body>

templates/404.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<h1>自定义404页面</h1>
<p>
    {{ exception }}
</p>
</body>
</html>

templates/500.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<h1>自定义500页面</h1>
</body>
</html>

步骤2.编写处理异常的视图

# 针对上述四种错误,django框架内置了处理异常的视图,如下
from django.views.defaults import bad_request # 用来处理400异常
from django.views.defaults import permission_denied # 用来处理403异常
from django.views.defaults import page_not_found # 用来处理404异常
from django.views.defaults import server_error # 用来处理500异常

# 此处由于篇幅问题,请读者自行查看源码,通过查看源码,可以知道
# 1、内置的异常处理视图会去固定位置找对应的文件名,这就是我们自定制错误页面文件位置与文件名固定的原因
# 2、内置的异常处理视图,针对400和500错误返回的就是静态页面,而针对403和404错误会用异常变量exception来渲染页面,我们在定制403.html、404.html页面时设定一个模板语法{{ exception }}就是用来专门接收异常值的

步骤3.请将 settings.py 中的 DEBUG设置为Fasle,这样,如果出现错误,就会出现我们自己定义的页面

# settings.py
DEBUG = False # 在生产环境中就应该设置成False

# ps:设置完DEBUG=False后,必须设置ALLOWED_HOSTS才能启动django,设为ALLOWED_HOSTS = ['*']即可
ALLOWED_HOSTS = ['*']

完成上述步骤后,我们来测试一下

urls.py

from django.urls import path
from app01 import views

urlpatterns = [
    path('test/400',views.error_test_400),
    path('test/403',views.error_test_403),
    path('test/404',views.error_test_404),
    path('test/500',views.error_test_500),

]

views.py

from django.core.exceptions import SuspiciousFileOperation
from django.core.exceptions import PermissionDenied
from django.http import Http404

# 我们在编写正常视图view时会依据具体的逻辑,主动或被动抛出相对应的异常类型,此处为了测试精简,省略了正常逻辑,直接抛出异常
def error_test_400(request):
    raise SuspiciousFileOperation('抛出400异常') # 异常信息不会展现在页面中

def error_test_403(request):
    raise PermissionDenied('抛出403异常') # 异常信息会展现在页面中

def error_test_404(request):
    raise Http404('抛出404异常') # 异常信息会展现在页面中

def error_test_500(request):
    xxx # 会抛出异常NameError,异常信息不会展现在页面中

测试

# 在浏览器依次输入下述url地址,会执行对应的视图函数,触发异常,django内置的异常处理视图会捕捉异常并返回我们定制的错误页面
http://127.0.0.1:8002/test/400
http://127.0.0.1:8002/test/403
http://127.0.0.1:8002/test/404
http://127.0.0.1:8002/test/500

2、自定义异常处理视图

django内置的异常处理视图应该可以满足大部分Web应用的需求,但不可否认的是缺乏灵活性,比如我们想在400.html页面中也获取异常信息,此时就需要自定制400异常的处理视图了,为了满足读者日后的需求,我们在此将四个异常处理视图都自定义一下,在上例的基础上,作出如下操作

1.修改上例的templates/400.html,其他页面不变

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<h1>自定义400页面</h1>
<p>
    {{ exception }}
</p>
</body>
</html>

2.views.py中新增自定义异常处理视图

# ================》下面是我们正常的应用视图《================
from django.core.exceptions import SuspiciousFileOperation
from django.core.exceptions import PermissionDenied
from django.http import Http404

# 我们在编写正常视图view时会依据具体的逻辑,主动或被动抛出相对应的异常类型,此处为了测试精简,省略了正常逻辑,直接抛出异常
def error_test_400(request):
    raise SuspiciousFileOperation('抛出400异常') # 异常信息不会展现在页面中

def error_test_403(request):
    raise PermissionDenied('抛出403异常') # 异常信息会展现在页面中

def error_test_404(request):
    raise Http404('抛出404异常') # 异常信息会展现在页面中

def error_test_500(request):
    xxx # 会抛出异常NameError,异常信息不会展现在页面中

# ================》新增自定义异常处理视图《================
from django.shortcuts import render

def my_custom_bad_request_view(request, exception):  # 400
    '''
    处理400异常的视图,必须接收两个参数

    参数request:
    参数exception: 捕获的异常值

    返回值: django规定该函数需要返回一个HttpResponseBadRequest类的对象
           可以查看该类的源码,该类是HttpResponse的子类,并设置status_code = 400
           from django.views.defaults import HttpResponseBadRequest

           明白了上述原理,为了方便我们渲染自定义页面,此处我们采用render返回,设置好status_code即可
    '''
    print('====>400')
    response = render(request, '400.html', {'exception': exception})
    response.status_code = 400  # 如果不设置,默认状态码为200
    return response

def my_custom_permission_denied_view(request, exception):  # 403
    '''
    处理403异常的视图,必须接收两个参数

    参数request:
    参数exception: 捕获的异常值

    返回值: django规定该函数需要返回一个HttpResponseForbidden类的对象
           可以查看该类的源码,该类是HttpResponse的子类,并设置status_code = 403
           from django.views.defaults import HttpResponseForbidden

           明白了上述原理,为了方便我们渲染自定义页面,此处我们采用render返回,设置好status_code即可
    '''
    print('====>403')

    response = render(request, '403.html', {'exception': exception})
    response.status_code = 403  # 如果不设置,默认状态码为200
    return response

def my_custom_page_not_found_view(request, exception):  # 404
    '''
    处理404异常的视图,必须接收两个参数

    参数request:
    参数exception: 捕获的异常值

    返回值: django规定该函数需要返回一个HttpResponseNotFound类的对象
           可以查看该类的源码,该类是HttpResponse的子类,并设置status_code = 404
           from django.views.defaults import HttpResponseNotFound

           明白了上述原理,为了方便我们渲染自定义页面,此处我们采用render返回,设置好status_code即可
    '''
    print('====>404')

    response = render(request, '404.html', {'exception': exception})
    response.status_code = 404  # 如果不设置,默认状态码为200
    return response

def my_custom_error_view(request):  # 500
    '''
    处理500异常的视图,只接收一个参数

    返回值: django规定该函数需要返回一个HttpResponseServerError类的对象
           可以查看该类的源码,该类是HttpResponse的子类,并设置status_code = 500
           from django.views.defaults import HttpResponseServerError

           明白了上述原理,为了方便我们渲染自定义页面,此处我们采用render返回,设置好status_code即可
    '''
    print('====>500')

    response = render(request, '500.html', )  # 服务端的错误本就不应该暴露给客户端
    response.status_code = 500  # 如果不设置,默认状态码为200
    return response

3.urls.py中,让异常处理指向自定义视图

from django.urls import path
from app01 import views

urlpatterns = [
    path('test/400',views.error_test_400),
    path('test/403',views.error_test_403),
    path('test/404',views.error_test_404),
    path('test/500',views.error_test_500),

]

# 新增下列四行,下述变量名固定,对应的值为自定义异常处理视图
handler400 = 'app01.views.my_custom_bad_request_view' # 代表处理400异常指向自定义异常处理视图
handler403 = 'app01.views.my_custom_permission_denied_view'

handler404 = 'app01.views.my_custom_page_not_found_view'
handler500 = 'app01.views.my_custom_error_view'

4.settings.py 中的 DEBUG同样需要设置为Fasle

# settings.py
DEBUG = False # 在生产环境中就应该设置成False

ALLOWED_HOSTS = ['*']

5.测试

# 在浏览器依次输入下述url地址,会执行对应的视图函数,触发异常,此时我们自定义的异常处理视图会捕捉异常并返回我们定制的错误页面
http://127.0.0.1:8002/test/400
http://127.0.0.1:8002/test/403
http://127.0.0.1:8002/test/404
http://127.0.0.1:8002/test/500

三、Django视图层-FBV与CBV

1、引入

官网地址:https://docs.djangoproject.com/en/3.0/topics/class-based-views/

视图是可调用的,用来处理请求(request)并且返回响应(response),django的视图有两种形式:FBV和CBV

1、FBV基于函数的视图(Function base views),我们之前一直介绍的都是FBV

2、CBV基于类的视图(Class base views),我们本节主要介绍它

在早期,人们意识到视图开发的过程中存在一些常见的语法和模式,于是引入基于函数的通用视图来抽象这些模式,并简化了常见情况下的视图开发。因此,刚开始的时候只有FBV,django所做的事情就是向你定义的视图函数传递一个HttpRequest,并且希望返回一个HttpResponse。

随着进一步的发展,人们发现,基于函数的通用视图是有问题的,问题在于它很好地覆盖了简单的情况,但针对稍微复杂的场景,它没有办法在某些配置项之外进行扩展或自定义,从而极大地限制了它在许多实际应用程序中的实用性。而考虑到扩展性与自定义,这正是面向对象技术的强大之处,于是诞生了CBV。

基于类的通用视图与基于函数的通用视图目的都一样,都是为了让视图开发更加容易。但是CBV的解决方案是通过使用mixins,并且django内置了一系列通用视图作为工具集提供给我们继承使用,从而使得基于类的通用视图比基于函数的通用视图更具扩展性和灵活性。

如果你之前尝试过FBV,但发现它有缺陷,CBV是一种新的解决方案,而不是单纯地换了一种定义视图的形式,如果CBV只是视图的另外一种定义形式而已,那岂不是很无聊,连详细介绍的必要都没有了。

为了让读者快速掌握CBV的使用,我们只能从简单情况下举例,这看起来会是增加了代码的复杂度,读者可能会对其嗤之以鼻,其实不然,在读者日后遇到复杂的情况时,唯有CBV可以进行更高级的设计

 综上,CBV并非是FBV的完全替代品,但相对于FBV,CBV确实有一些差异和优势,详解下一小节 1、针对不同的HTTP请求方法(如GET、POST),CBV可以分别对应到专门的处理方法上,无需采用条件分支,代码更加精简

2、CBV采用面向对象技术,比如mixins(多重继承)可用于将代码转换成可重用的组件。

2、使用CBV

3、使用不同的实例方法来响应不同的HTTP请求方法

CBV允许我们使用不同的实例方法来响应不同的HTTP请求方法,而不是像FBV那样使用条件分支代码

1.FBV中视图函数处理HTTP的GET请求如下

from django.http import HttpResponse

def my_view(request):
    if request.method == 'GET':
        # <view logic>
        return HttpResponse('GET result')
    elif request.method == 'POST':
        # <view logic>
        return HttpResponse('POST result')

FBV关于url的配置略

2. CBV的实现如下

from django.http import HttpResponse
from django.views import View

class MyView(View):
    def get(self, request):
        # <view logic>
        return HttpResponse('GET result')

    def post(self, request):
        # <view logic>
        return HttpResponse('POST result')

所有基于类的视图都必须继承View类,该类用来处理视图关联到URL的操作,具体分析如下:

由于django的URL解析器期望发送request以及相关参数给一个可调用的函数,而不是一个类,所以基于类的视图有一个as_view()类方法(该方法继承自父类View),调用该方法会返回URL解析器所期望的函数,该函数会在请求到达与关联模式匹配的URL时调用,就像调用视图函数一个样子。查看源码会发现调用该函数首先会创建一个MyView类的实例,然后

1、调用self.setup()方法,用于初始化一系列属性。
2、之后调用self.dispatch(),该方法查看request以确定本次请求是GET还是POST等,如果定义了匹配方法,则将请求转发给匹配方法;如果没有,则引发HttpResponseNotAllowed。本例中将调用我们自定义的get与post方法来响应请求*
*
ps:基于面向对象的知识,上述方法,如果子类MyView未重写,则去父类View中寻找

urls.py的配置如下

# urls.py
from django.urls import path
from myapp.views import MyView

urlpatterns = [
    path('about/', MyView.as_view()),
]

虽然最精简的CBV不需要设置任何类属性就可以使用,但类属性确实在许多基于CBV的设计中非常有用,我们又两种方法可以配置或设置类属性

方法一:python标准的套路,在子类中重写/覆盖父类的属性和方法

from django.views import View

class GreetingView(View):
    greeting = "Good Day"

    def get(self, request):
        return HttpResponse(self.greeting)

# 可以在子类中对属性进行重写
class MorningGreetingView(GreetingView):
    greeting = "Morning to ya"

# 在url配置中使用子类调用as_view()方法即可
from django.urls import path
from myapp.views import MyView

urlpatterns = [
    path('about/', MorningGreetingView.as_view()),
]

方法二:在URL配置里为as_view()方法传递关键字参数来设置类属性

urlpatterns = [
    path('about/', GreetingView.as_view(greeting="G'day")),
]

# 注意1:as_view方法只接受已经是类属性的参数,即在类GreetingView中必须已经有greeting属性,as_view只是一种覆盖操作
from django.views import View

class GreetingView(View):
    greeting = "Good Day" # 必须已经有了该属性

    def get(self, request):
        return HttpResponse(self.greeting)

# 注意2:这种覆盖操作只在本条url匹配成功时生效一次
urlpatterns = [
    # 访问结果为覆盖后的属性值:"G'day"
    path('about/', GreetingView.as_view(greeting="G'day")),
    # 访问结果仍为原值:"Good Day"
    path('about1/', views.GreetingView.as_view()),
]

3、使用mixins

Mixins机制指的是子类混合(mixin)不同类的属性和功能,比如在django内置的通用视图里有一个mixin类叫TemplateResponseMixin,它的主要目的就是来定义一个方法render_to_response()。当该类和View类组合到一起时,就得到了TemplateView类,TemplateView类分发请求到相应的方法上(通过执行继承自View类的dispath()方法),并且使用render_to_response()方法来找到template_name属性读取指定的模板从而返回一个TemplateResponse对象(通过执行继承自TemplateResponseMixin类的render_to_response()方法)

from django.views.generic.base import TemplateResponseMixin

这就极大利用了面向对象重之多继承技术来重用代码的优点,但是鱼与熊掌不可兼得,需要付出的代价就是,在多继承背景下,我们定义的mixin类越来越多,代码的可读性仍然会变得越来越差。

关于mixins的用法详见官网https://docs.djangoproject.com/en/3.0/topics/class-based-views/mixins/,篇幅问题此处不再累述。

需要强调的是,CBV可以继承多个mixins类(推荐写在左边),但是只能继承一个父类,该父类只能是View类或者其子/子孙类(推荐写在右面),同时继承两个父类将会导致你的子类无法按照预期那样工作。

四、django内置的CBV

django内置了基本的CBV可以满足相当多的应用,所有的视图都继承自View类,View类负责处理将视图链接到url、HTTP方法调度和其他常见功能。内置的CBV如RedirectView提供了一个HTTP重定向,TemplateView扩展了基类,使其同时可以渲染一个模板。

from django.views.generic import View,TemplateView,RedirectView

1.最简单的使用

以TemplateView为例来介绍内置CBV的使用。最直接使用方式就是直接在URL配置中创建,如果只想改变该CBV的几个属性,你可以为as_view传递参数,如下

from django.urls import path
from django.views.generic import TemplateView

urlpatterns = [
    path('about/', TemplateView.as_view(template_name="about.html")),
]

一样的道理,如果我们想要重定向一个地址,可以设置RedirectView的url属性

from django.urls import path
from django.views.generic import RedirectView
from app01 import views

urlpatterns = [
    path('index/', views.index),
    path('about/', RedirectView.as_view(url="/index/")),
]

2.继承内置CBV进行重写

第二种更有效的使用内置CBV的方式就是是从现有视图继承并重写子类中的属性(例如template_name)或方法(例如get_context_data),以提供新的值或方法。

例如,我们的需求是要定制一个视图,该视图专门用来显示/渲染模板about.html。Django有一个内置的CBV即TemplateView可以实现,因此我们可以继承它,并重写模板名称template_name以及用来获取渲染模板数据的方法get_context_data

# views.py
from django.views.generic.base import TemplateView

class HomePageView(TemplateView):
    template_name = "home.html" # django会去templates目录下查找到该模板

    def get_context_data(self, **kwargs): # 获取数据,用来渲染模块
        context = super().get_context_data(**kwargs)

        context['name'] = "egon"
        context['age'] = "18"
        context['title'] = "dsb"

        return context

url.py配置如下

from django.urls import path
from app01 import views

urlpatterns = [
    path('home/', views.HomePageView.as_view()),
]

templates/home.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>

<h1>{{ name }}</h1>
<h1>{{ age }}</h1>
<h1>{{ title }}</h1>

</body>
</html>

更多内置CBV详见:

https://docs.djangoproject.com/en/3.0/topics/class-based-views/generic-display/

五、使用CBV处理表单

请看完后续章节中关于表单的使用之后再来读本小节 FBV处理表单代码如下

from django.http import HttpResponseRedirect
from django.shortcuts import render

from .forms import MyForm # forms.py内的MyForm表单类

def myview(request):
    if request.method == "POST":
        form = MyForm(request.POST)
        if form.is_valid():
            # <process form cleaned data>
            return HttpResponseRedirect('/success/')
    else:
        form = MyForm(initial={'key': 'value'})

    return render(request, 'form_template.html', {'form': form})

CBV处理表单代码如下

from django.http import HttpResponseRedirect
from django.shortcuts import render
from django.views import View

from .forms import MyForm

class MyFormView(View):
    form_class = MyForm
    initial = {'key': 'value'}
    template_name = 'form_template.html'

    def get(self, request, *args, **kwargs):
        form = self.form_class(initial=self.initial)
        return render(request, self.template_name, {'form': form})

    def post(self, request, *args, **kwargs):
        form = self.form_class(request.POST)
        if form.is_valid():
            # <process form cleaned data>
            return HttpResponseRedirect('/success/')

        return render(request, self.template_name, {'form': form})

上述只是一个很小的例子,但是我们可以定制这个视图,比如覆盖任意的类属性(例如form_class),或者为as_view()传参,或者继承MyForView并重写其中方法。

更多表单处理详见:

https://docs.djangoproject.com/en/3.0/topics/class-based-views/generic-editing/

六、为CBV添加装饰器

之前我们介绍过,若想要为CBV添加额外的功能,可以采用mixins机制,但除此之外,还可以通过添加装饰器的方法实现。有两个地方可以添加装饰

1、装饰as_view()方法

# 导入内置的装饰器
from django.contrib.auth.decorators import login_required, permission_required 
from django.views.generic import TemplateView

from .views import VoteView

urlpatterns = [
    path('about/', login_required(TemplateView.as_view(template_name="secret.html"))),
    path('vote/', permission_required('polls.can_vote')(VoteView.as_view())),
]

示例:

views.py如下

# views.py
from django.shortcuts import render, HttpResponse
from django.views import View
import time

# 装饰器
def timer(func):
    def wrapper(request, *args, **kwargs):
        start = time.time()
        ret = func(request, *args, **kwargs)
        print("函数执行的时间是{}".format(time.time() - start))
        return ret

    return wrapper

# CBV视图
class LoginView(View):
    def dispatch(self, request, *args, **kwargs):
        obj = super().dispatch(request, *args, **kwargs)
        return obj

    def get(self, request):
        return render(request, "login.html")

    def post(self, request):
        username = request.POST.get('username')
        password = request.POST.get('password')
        if username == 'egon' and password == '123':
            return HttpResponse('登录成功')
        else:
            return HttpResponse('账号或密码错误')

urls.py如下

from django.urls import path
from app01.views import *

urlpatterns = [
    path('login/',timer(LoginView.as_view())),
]

login.html如下

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<form action="" method="post">
    {% csrf_token %}
    <p>
        用户名:
        <input type="text" name="username">
    </p>
    <p>
        密码:
        <input type="password" name="password">
    </p>
    <p>
        <input type="submit" value="登录">
    </p>
</form>
</body>
</html>

装饰as_views()相当于给视图类的中的所有方法(例如get、post)都加上了装饰器

2、装饰视图类

类方法与独立函数不完全相同,所以你不能简单地把函数装饰器加到类方法上,这就用到了method_decorator方法,该方法会将函数装饰器转换为方法装饰器,然后才可以用来装饰类方法,例如:

装饰dispatch方法,基于CBV,当请求过来后会先执行dispatch()这个方法进而分发到对用的get、post方法上,所以如果需要批量装饰处理请求的方法(如get,post等)可以为dispatch方法添加装饰,这与为as_view()添加装饰器效果时一样的,如下

# views.py
from django.shortcuts import render, HttpResponse
from django.views import View
from django.utils.decorators import method_decorator
import time

# 装饰器
def timer(func):
    def wrapper(request, *args, **kwargs):
        start = time.time()
        ret = func(request, *args, **kwargs)
        print("函数执行的时间是{}".format(time.time() - start))
        return ret

    return wrapper

# CBV视图
class LoginView(View):
    #相当于给get,post请求都加上了装饰器
    @method_decorator(timer)
    def dispatch(self, request, *args, **kwargs):
        obj = super().dispatch(request, *args, **kwargs)
        return obj

    def get(self, request):
        return render(request, "login.html")

    def post(self, request):
        username = request.POST.get('username')
        password = request.POST.get('password')
        if username == 'egon' and password == '123':
            return HttpResponse('登录成功')
        else:
            return HttpResponse('账号或密码错误')

ps:url配置中正常调用即可,无需重复装饰,下同

from django.urls import path
from app01.views import *
urlpatterns = [
    path('login/',LoginView.as_view()),
]

我们当然可以单独装饰不同的方法,如

......省略代码,同上

class LoginView(View):
    def dispatch(self, request, *args, **kwargs):
        obj = super().dispatch(request, *args, **kwargs)
        return obj

    @method_decorator(timer) # 只装饰get方法
    def get(self, request):
        return render(request, "login.html")

    def post(self, request):
        username = request.POST.get('username')
        password = request.POST.get('password')
        if username == 'egon' and password == '123':
            return HttpResponse('登录成功')
        else:
            return HttpResponse('账号或密码错误')

更简洁一点,我们可以把装饰器加在类上,通过name参数指定要装饰的方法,如下

# @method_decorator(timer,name='dispatch') # 批量添加
@method_decorator(timer,name='get') # 单独给get方法添加
# @method_decorator(timer,name='post') # 单独给post方法添加
class LoginView(View):
    def dispatch(self, request, *args, **kwargs):
        obj = super().dispatch(request, *args, **kwargs)
        return obj

    def get(self, request):
        return render(request, "login.html")

    def post(self, request):
        username = request.POST.get('username')
        password = request.POST.get('password')
        if username == 'egon' and password == '123':
            return HttpResponse('登录成功')
        else:
            return HttpResponse('账号或密码错误')

如果有多个装饰器需要添加,可以定义一个列表或者元组将其存放起来,然后按照下述方式指定

from django.shortcuts import render, HttpResponse
from django.views import View
from django.utils.decorators import method_decorator
import time

# 定义多个装饰
def deco1(func):
    def wrapper(request, *args, **kwargs):
        print('===>deco1')
        ret = func(request, *args, **kwargs)
        return ret
    return wrapper

def deco2(func):
    def wrapper(request, *args, **kwargs):
        print('===>deco2')
        ret = func(request, *args, **kwargs)
        return ret
    return wrapper

# 定义装饰列表
decorators = [deco1, deco2]

@method_decorator(decorators,name='get') # 为get方法添加多个装饰器
class LoginView(View):
    def dispatch(self, request, *args, **kwargs):
        obj = super().dispatch(request, *args, **kwargs)
        return obj

    def get(self, request):
        return render(request, "login.html")

    def post(self, request):
        username = request.POST.get('username')
        password = request.POST.get('password')
        if username == 'egon' and password == '123':
            return HttpResponse('登录成功')
        else:
            return HttpResponse('账号或密码错误')

上述装饰器会按照列表规定的顺序依次执行装饰器,即先执行deco1,然后deco2。。。与下述添加方式是等同的

......省略代码,同上

@method_decorator(deco1,name='get')
@method_decorator(deco2,name='get')
class LoginView(View):
    def dispatch(self, request, *args, **kwargs):
        obj = super().dispatch(request, *args, **kwargs)
        return obj

    def get(self, request):
        return render(request, "login.html")

    def post(self, request):
        username = request.POST.get('username')
        password = request.POST.get('password')
        if username == 'egon' and password == '123':
            return HttpResponse('登录成功')
        else:
            return HttpResponse('账号或密码错误')

七、支持其他的HTTP方法

假设我们写了一个图书管理系统,如果有人想使用视图作为API通过HTTP协议访问我们的图书库。API客户端想要获取最新的图书数据,需要时不时地发起连接并下载自上次访问以来新出版的图书数据。但是如果自上次访问完后,就没有新的图书出版,那么从数据库查询图书信息,渲染一个完整的reponse并发送给客户端将白白耗费CPU时间和带宽,此时,我们最好的方式,就是在得知有新书出版时才发起一次请求完整数据的的API调用,这就用到了head方法来查询是否有数据更新

urls.py如下

from django.urls import path
from app01.views import BookListView

urlpatterns = [
    path('books/', BookListView.as_view()),
]

views.py

from django.shortcuts import HttpResponse
from django.views.generic import ListView
from app01.models import Book

class BookListView(ListView):
    model = Book

    def head(self, *args, **kwargs):
        last_book = self.get_queryset().latest('publication_date')
        response = HttpResponse()

        response['Last-Modified'] = last_book.publication_date.strftime('%Y-%m-%d %X')
        return response

    def get(self,request):
        '''
        返回完整的数据,此处我们简单地用返回所有书籍名字代表将要返回的完整数据
        '''
        books=Book.objects.all()
        names='|'.join([book.name for book in books])
        return HttpResponse(names)

models.py

from django.db import models

# Create your models here.

class Book(models.Model):
    name=models.CharField(max_length=30)
    publication_date=models.DateTimeField(auto_now_add=True)

settings.py

TIME_ZONE = 'Asia/Shanghai'
USE_TZ = False

编写脚本进行测试

import requests

# 朝API发送head请求,拿到最新一本书的时间
response=requests.head('http://127.0.0.1:8002/books/')
response.headers['Last-Modified']  # 拿到最新一本书的出版时间

# 如果该最新时间是与上次时间不等,则意味着有新数据,需要发起get请求拿到完整数据啦
response=requests.get('http://127.0.0.1:8002/books/')
print(response.text) # 拿到完整的数据
上一篇
下一篇
Copyright © 2022 Egon的技术星球 egonlin.com 版权所有 帮助IT小伙伴学到真正的技术