自学Python第二十二天- Django框架(三) AJAX、文件上传、POST 请求类型之间的转换、多APP开发、iframe、验证码、分页器、类视图、中间件、信号、日志、缓存、celery异步

Django官方文档

django 使用 AJAX

django 项目中也可以使用 ajax 技术

前端

前端和其他 web 框架一样,需要注意的是,django 接收 POST 请求时,需要 csrf_token 进行验证,而在 ajax 中获取 csrf_token 比较麻烦。所以通常会在后端免除 csrf_token 验证。

绑定事件方式

{% extends 'layout.html' %}
{% block content %}
    <div class="container">
        <h1>任务管理</h1>
        <input type="button" class="btn btn-primary" value="点击" onclick="clickMe();"/>
    </div>
{% endblock %}
{% block js %}
    <script type="text/javascript">
        function clickMe() {
      
      
            $.ajax({
      
      
                url: "/test/ajax/",
                type: "get",
                data: {
      
      
                	type:'add',
                    n1:123,
                    n2:456
                },
                success: function (res) {
      
      
                    console.log(res);
                }
            })
        }
    </script>
{% endblock %}

以 jQuery 方式

{% extends 'layout.html' %}
{% block content %}
    <div class="container">
        <h1>任务管理</h1>
        <input type="button" class="btn btn-primary" value="点击" id="btn1" />
    </div>
{% endblock %}
{% block js %}
    <script type="text/javascript">
        $(function (){
      
      
            // 页面框架加载完成后代码自动执行
            bindBtn1Event();        // 绑定事件
        })
        function bindBtn1Event(){
      
      
            $("#btn1").click(function (){
      
             // 绑定到 btn1 的 click 事件的函数
                $.ajax({
      
      
                    url: "/task/ajax/",
                    type: "POST",
                    data: $("#addForm").serialize(),
                    dataType:'JSON',
                    success: function (res) {
      
      
                        console.log(res);
                        // console.log(res.res_add)
                        // console.log(res['res_add'])
                    }
                })
            })
        }
    </script>
{% endblock %}

后端

后端部分只要将响应请求 url 绑定视图函数,则可以在视图函数中进行处理

def task_ajax(request):
    """测试ajax"""
    return HttpResponse('成功')

从请求体中获取数据

对于 request.POST 在使用中有一些限制:

  • 只能用于 POST 请求,对于 PUT、DELETE 等非 POST 请求不能使用
  • 只能用于表单数据,即 content-typeapplication/x-www-form-urlencoded 类型的请求

对于其他数据,必须从请求体中获取。因为 request.body 中的数据是字节型,所以需要先解析。

import json

def data_form_body(request):
	req_dict = json.loads(request.body)
	category = req_dict.get('category)

返回 json 数据

可以使用 json 的 dumps 方法将字典转为 json 并返回

import json

def task_ajax(request):
	res_dict = {
    
    'res':'ok', 'data':{
    
    'k1': 'v1', 'k2': 'v2'}}
	res_dict = json.dumps(res_dict)
    return HttpResponse(res_dict)

也可以直接使用 JsonResponse 返回数据

from django.http import JsonResponse

def task_ajax(request):
	res_dict = {
    
    'res':'ok', 'data':{
    
    'k1': 'v1', 'k2': 'v2'}}
    return JsonResponse(res_dict)

禁用 csrf 校验检查

前端发送 post 请求时如果不加载 csrf_token,则后端需要禁用 csrf 校验检查

from django.views.decorators.csrf import csrf_exempt

@csrf_exempt
def task_ajax(request):
	return HttpResponse('成功')

或在注册路由时注明

from django.views.decorators.csrf import csrf_exempt

urlpatterns = [
	path('goods/', csrf_exempt(views.goods), name='goods'),
]

或在settings.py 的中间件中取消 csrf 中间件(不推荐)。

Ajax 结合 ModelForm

Ajax 结合 ModelForm 在前端实际上几乎没有改变,要注意的也就是按钮绑定 Ajax 函数不用 csrf_token

在后端,ModelForm 其实接收到的数据也是 form 的字典格式,包含校验、保存等处理方式没有变化,只是在返回值时有了变化。

处理重定位信息

因为 Ajax 是接收处理数据,所以后端返回 重定位 信息是无法跳转的。如果希望跳转,则需要返回一个 json ,ajax 收到了这个特定的 json 数据后使用 js 进行页面跳转。

处理表单错误信息

另外返回 ModelForm 错误信息时,form.字段.errors 获取的是一个字典,可以整理成 json 格式返回给 ajax 处理。

<!-- novalidate 可以禁止浏览器自身的有效性检测 -->
<form id="addForm" novalidate>
    <div class="clearfix">
        {% for field in form %}
            <!-- field.label 是从数据表定义中的 verbose_name 取的 -->
            <div class="col-xs-6">
                <div class="form-group">
                    <label>{
   
   { field.label }}</label>
                    {
   
   { field }}
                    <!-- 因为不会使用 ModelForm 返回错误信息,所以不再这样写 -->
                    <!-- <span style="color: red">{
    
    { field.errors.0 }}</span> -->
                    <span style="color: red"></span>
                </div>
            </div>
        {% endfor %}
        <div class="col-xs-12">
            <button type="button" id="btnAdd" class="btn btn-primary">添加</button>
        </div>
    </div>
</form>
<script type="text/javascript">
    $(function () {
      
      
        // 页面框架加载完成后代码自动执行
        bindBtnAddEvent();
    })
    function bindBtnAddEvent() {
      
      
        $("#btnAdd").click(function () {
      
      
        	$(".error-msg").empty();        // 清空上一次的错误信息
            $.ajax({
      
      
                url: "/task/add/",
                type: "POST",
                data: $("#addForm").serialize(),
                dataType: 'JSON',
                success: function (res) {
      
      
                    if(res.status){
      
      
                        alert("添加成功!");
                    }else{
      
      
                        console.log(res);
                        $.each(res.error,function (name,data){
      
            // 循环每一个错误,获取键值
                            // 拼接 id_ 和 name,获取对应文本框的 id
                            // 查找文本框元素的下一个元素(span),使其文本(text)为错误信息
                            $("#id_" + name).next().text(data[0]);
                        })
                    }
                }
            })
        })
    }
</script>

文件上传

简单的文件上传

前端会将文件以 post 请求发送至后端,后端可以使用 request.FILES 来接收。需要注意的是 form 需要 enctype 属性。

<form method="post" enctype="multipart/form-data">
    {% csrf_token %}
    <input type="text" name="filename">
    <input type="file" name="file">
    <input type="submit" value="提交">
</form>
file_obj = request.FILES.get('file')  # 获取上传文件的对象
filename = request.POST.get('filename', file_obj.name)      # 获取设置的文件名,如果没有则为原文件名。
with open(filename, mode='wb') as f:
    for chunk in file_obj.chunks():  # 将上传文件分块读取
        f.write(chunk)
    f.flush()	# 文件写入完成,冲刷缓冲区

ajax 上传文件

ajax 上传文件和发送 json 的不同在于,发送的数据是一个 FormData 对象,创建这个对象时可以将表单 form 的 dom 对象传入。

$("#upload").click(function () {
    
    
   var formData = new FormData($('#uploadForm')[0]);
   // 或使用 FormDate 对象添加文件对象的方式
   // var formData = new FormData();
   // formDate.append('file', this.file[0]);		//这里的 this 指向上传文件的 input 标签的dom对象
   // formDate.append('key', value);		// 可以添加其他的数据
   $.ajax({
    
    
    type: 'post',
    url: "https://****:****/fileUpload", //上传文件的请求路径必须是绝对路劲
     data: formData,
     cache: false,
     processData: false,
     contentType: false,
      }).success(function (data) {
    
    
        console.log(data);
        alert("上传成功"+data);
        filename=data;
      }).error(function () {
    
    
         alert("上传失败");
     });
    });

后端接收时,注意接收字段是前端定义的 name ,文件从 requests.FILES 里获取

uid = requests.POST.get('uid')		# 获取数据
file = requests.FILES.get('file')	# 获取文件
# files = requests.FILES.getlist('files')	# 获取多个文件

使用 Form 组件上传文件

使用 Form 时可以定义文件字段 FileField ,定义后就能够上传文件了。

class UpForm(forms.Form):
	name = forms.CharField(label='姓名')
	age = forms.IntegerField(label='年龄')
	img = forms.FileField(label='头像')

def upload_form(request):
	form = UpForm(data=request.POST, files=request.FILES)
	if form.is_valid():
		print(form.cleaned_data)
		# 文件在 form.cleaned_data 的相应字段(img)内
		img = form.cleaned_data.get('img')
		file_path = os.path.join('app01', 'static', 'img', img.name)	# 拼接文件路径
		with open(file_path,'wb') as f:		# 写入文件
			for chunk in img.chunks():
				f.write(chunk)

	else:
		return render(request, 'upload.html', {
    
    'form': form})
<form method="post" enctype="multipart/form-data" novalidate>
	.
	.
	.
</form>

文件上传至上传目录 media

通常使用的静态资源都在 static 文件夹内,如果想要用户上传文件到一个特定文件夹,例如项目根目录下的 media 文件夹,则需要在 urls.py 中进行设置启用。首先引入一些资源,再在 urls.py 文件的 urlpatterns 字段添加:

from django.views.static import serve
from django.urls import path, re_path
from django.conf import settings

re_path(r'^media/(?P<path>.*)$', serve, {
    
    'document_root': settings.MEDIA_ROOT}, name='media'),

然后在 settings.py 中进行配置:

import os

MEDIA_ROOT = os.path.join(BASE_DIR, 'media')		# 项目根目录下的 media 目录
MEDIA_URL = '/media/'

此时上传文件保存的路径可以写为

media_file_path = os.path.join('media', image_obj.name)

这样将用户上传的文件放在 media 目录下,也可以通过 /media/文件名 的 url 来访问,例如 http://127.0.0.1:8000/media/001.png

使用 ModelForm 组件上传文件

ModelForm 组件可以自动上传文件,并存储保存路径到数据库,不用再写相应保存的代码,且能够自动进行重名处理。只是需要设置了上传保存目录。

需注意的是,保存到数据库中的文件路径也是 media 下的相对路径,并且不包含 media ,使用时候注意添加 media。

# models.py

class City(models.Model):
	"""城市"""
	name = models.CharField(verbose_name='名称', max_length=32)
	count = models.IntegerField(verbose_name='人口')

	# 本质上数据库存储的是文件路径,也是CharField,可以自动保存数据
	# upload_to 指的就是上传文件到哪个目录,是 media 目录下的相对路径
	img = models.FileField(verbose_name='logo', max_length=128, upload_to='city/')
# views.py

class UpModelForm(forms.ModelForm)
	class Meta:
		model = models.City
		fields = '__all__'
	
def upload_modal_form(request):
	form = UpModelForm(data=request.POST, files=request.FIELS)
	if form.is_valid():
		# 保存文件,保存 form 字段信息和文件路径并写入数据库
		form.save()
		return HttpResponse('成功')

POST 请求类型之间的转换

POST 和 GET 请求是最常用的两种请求方式。一般简单的请求使用 GET,如果需要附加大量参数、数据,都会选择 POST。之后也有将POST的功能进行具体区分,则出现了 PUT、DELETE、OPTIONS 等请求类型。

常见的四种编码方式

HTTP 协议规定POST 提交的数据必须放在消息主体(entity-body)中,但协议并没有规定数据必须使用什么编码方式。常见的四种编码方式有:

  1. application/x-www-form-urlencoded

最常见的 POST提交数据的方式。浏览器的原生 form表单,如果不设置 enctype属性,那么最终就会以application/x-www-form-urlencoded方式提交数据。

  1. multipart/form-data

另一种常见的 POST数据提交的方式。我们使用表单上传文件时,必须让 form的 enctyped 等于这个值。

  1. application/json

application/json 就是传说中的 json 格式,实际上,现在越来越多的人把它作为请求头,用来告诉服务端消息主体是序列化后的 JSON字符串。由于 JSON规范的流行,除了低版本 IE之外的各大浏览器都原生支持 JSON.stringify,服务端语言也都有处理 JSON的函数。

  1. text/xml

它是一种使用 HTTP作为传输协议,XML 作为编码方式的远程调用规范。

python中读取各类型请求中的数据

转换分为读取和发送,实际上发送的 response 包类型基本都能被浏览器正常识别(根据请求头中的 content-type)。经常使用的有:text/javascript、text/html、application/json 和一些例如 image/png 之类的媒体类响应报文了。

这里使用的环境就是在 django 中。

需注意的是,如果后端返回的响应使用 json 格式,则需要使用 json.dumps() 将其序列化。

application/x-www-form-urlencoded

这种 form 格式虽然发送的是 POST 请求,但实际上是将数据体作为类似于 GET 请求的方式发送的,而不是对象或序列化字符。

url: 'www.test.com/'
body: "id=1&name=zhangsan&age=23"

只是前端发送工具的不同会经过相应处理,例如 jquery 可以直接将 json 对象处理后发送,fetch 不进行处理。

django 可以直接使用 request.POST.get() 方法获取,获取的 POST 数据以字典形式保存,但是字典内部不可以进行嵌套字典。

如果前端发送的是嵌套的对象,django 会将嵌套的对象进行转换,例如:

// 前端发送的数据
{
    
    
	status:true,
	person:{
    
    
				name:'张三',
				age:18,
				sex:'male'
			}
}
# django 会将数据解析成这样,类型为字典
{
    
    	
	'status':'true',
	'person[name]':'张三',
	'person[age]':'18',
	'person[sex]':'male'
}

如果前端进行了序列化,则后端可以反序列化获得相应的数据。方式同 json 数据的序列化与反序列化。

multipart/form-data

前端需要注意的是,multipart/form-data 类型前端是要发送一个 FormData 对象的,直接发送字符串文本、json对象等都无法正常处理。可以将数据对象(包括字符串或json对象)添加到 FormData 对象中发送。使用 jquery 发送时,需要设置 content-type=false ,这样会自动添加请求头为 content-type:multipart/form-data; boundary=----WebKitFormBoundaryyr7L6TwhhOAIoEyn。同时需要设置 processData=false,不处理转换数据。

这种格式可以直接使用 request.POST.get() 方法获取各参数的值,类型是字符串类型。

因为 request.POST 是一个 QueryDict 对象,不能直接反序列化,所以其第一级属性必须直接获取,例如 get() 方法。此方法获取到的都是字符串,哪怕字符串内容是字典类型(前端发送的数据对象)。

如果浏览器将一个数据对象(类似于json的对象)作为 FormData 对象的一个属性值这种方式传送过来,获取的则是字符串类型的:[object Object]。这时需要前端将其(数据对象)序列化,例如使用 js 里的 JSON.stringify() 方法。后端收到这个值类型是字符串,内容是字典数据。

后端接收到后,获取的值是序列化后的字符串,可以使用 json.loads() 来将其转为字典格式。

上传文件也需要通过这种格式,可以使用 requests.FILES.get('file') 来获取。

application/json

前端发送 json 时,注意要使用 JSON.stringify() 将发送的 json 对象序列化,后端才能正常获取。

将请求体使用 json.loads() 反序列化得到字典类型。

if request.method == 'POST' and request.content_type == 'application/json':
	dic = json.loads(request.body)
	param1 = dic.get('param1')		# 如果不知道字典中是否有 param1 参数,可以使用字典的 get 方法,否则会报错
	param2 = dic.get('param2')

多 app 开发

django 多人协作开发时,每人开发不同的 app 也可能会遇到一些问题:对于相同名称的模板或静态资源的使用问题。因为 django 搜索静态资源和模板是安装 app 注册的顺序,在各 app 目录下查找模板或静态资源文件。如果有相同名称的模板或静态资源文件,后注册的 app 就会使用先注册 app 的同名文件了。

所以对于模板文件来说,在各自 app 下的 templates 文件夹下建立 app 名称的文件夹,再建立模板文件,使得各模板文件引用时会加入各自 app 的名称,这样就不会出现引用路径相同的问题了。

对于静态资源文件,因为涉及公共静态资源,所以需要注册静态资源路径。

# settings.py

STATIC_URL = '/static/'		
STATICFILES_DIRS = [
  os.path.join(BASE_DIR, "static"),			# 主静态文件目录
  os.path.join(BASE_DIR, "main", "static"),		# main app 静态文件目录
  os.path.join(BASE_DIR, "login", "static"),	# login app 静态文件目录
]

然后可以在每个APP下的static下建立以APP名相同的文件夹,比如在 login/static/login/ 放入样式JS CSS等

调用时使用 static 结构,加上 app 名称

{% static 'main/img/firefox-logo-small.jpg' %}

{% static 'login/img/name.png' %}

另外对于静态文件的打包整合可以参考这篇文章:

解决django多个app下的静态文件无法访问问题

使用 iframe

django 使用 iframe 时,可能浏览器会出现错误,根据提示信息发现是因为 X-Frame-Options=deny 导致的。

Refused to display xxx in a frame because it set 'X-Frame-Options' to 'deny'.

官方文档中关于点击劫持保护

点击劫持保护

The X-Frame-Options HTTP 响应头是用来给浏览器 指示允许一个页面 可否在 <frame>, <iframe>,<embed> 或者 <object>中展现的标记。站点可以通过确保网站没有被嵌入到别人的站点里面,从而避免点击劫持(clickjacking)攻击。它有三个值:

  • DENY :表示该页面不允许在 frame 中展示,即便是在相同域名的页面中嵌套也不允许
  • SAMEORIGIN :表示该页面可以在相同域名页面的 frame 中展示
  • ALLOW-FROM uri :表示该页面可以在指定来源的 frame 中展示

根据上述 X-Frame-Options的三个值描述,只要修改django的X-Frame-Options为SAMEORIGIN ,那么相同域名页面就可以使用frame中展示。

启用 X-Frame

在 settings.py 中的中间件中,启用了 django.middleware.clickjacking.XFrameOptionsMiddleware 中间件即开启了 X-Frame,django 是默认开启的。

MIDDLEWARE = [
    ...
    'django.middleware.clickjacking.XFrameOptionsMiddleware',
    ...
]

允许同域名使用 iframe

默认情况下,中间件将为每个出站的 HttpResponse将X-Frame-Options 头设置为 DENY。如果希望设置为其他值,可以在 settings.py 中的X_FRAME_OPTIONS设置:

X_FRAME_OPTIONS = 'SAMEORIGIN'

指定视图不使用 X-Frame

如果在使用此中间件的情况下,希望个别视图不使用 X-Frame ,则可以使用装饰器 xframe_options_exempt

from django.http import HttpResponse
from django.views.decorators.clickjacking import xframe_options_exempt

@xframe_options_exempt
def ok_to_load_in_a_frame(request):
    return HttpResponse("此页面可以使用 iframe 加载")

注意:如果要提交表单或访问框架或 iframe 中的会话 cookie,则可能需要修改 CSRF_COOKIE_SAMESITESESSION_COOKIE_SAMESITE 设置。

指定视图使用 X-Frame

在指定视图中使用 X-Frame,也可以使用装饰器

from django.http import HttpResponse
from django.views.decorators.clickjacking import xframe_options_deny
from django.views.decorators.clickjacking import xframe_options_sameorigin

@xframe_options_deny
def view_one(request):
    return HttpResponse("不能在 iframe 中使用")

@xframe_options_sameorigin
def view_two(request):
    return HttpResponse("可以在同域的 iframe 中使用")

验证码

为了防止爬虫程序或其他非人为操作,会使用验证码进行检查,一般常用的验证码方式有:

  • 图片验证码
  • 滑动验证码
  • 短信验证码
  • 邮箱验证码

pillow 库

pillow 库是 python 的一个比较简单方便的图形工具库

pip install pillow
需注意的是,使用 pillow 时,是引入 PIL 库

pillow 有三个主要的类,分别是 Image、ImageDraw 和 ImageFont 类。

画布 Image 类

Image 类可以创建画布,所有的绘制图形都会在画布上进行。创建画布时需指定画布大小、颜色等信息。

画笔 ImageDraw 类

ImageDraw 类用来在画布上进行绘制图案,使用时需指定画笔的颜色、绘制方式(具体的图形或绘制路径)、绘画起始坐标、结束坐标等信息。

字体 ImageFont 类

ImageFont 类可以将文本绘制为图像,使用时需要指定字体、大小、文本内容等信息

绘制流程

绘制流程一般为创建画布、绘制文本到画布上、绘制干扰图像到画布上。

from PIL import Image, ImageDraw, ImageFont

def new_code_img(request):
	# 创建画布
	bg = (220, 220, 180)	# 画布背景色,RGB
	img = Image.new('RGB', (120, 30), bg)	# 创建画布,使用RGB模式,画布大小为120*30,指定背景色
	# 在画布上创建画笔对象
	draw = ImageDraw.Draw(img, 'RGB')		# 指定画布,创建画笔对象,使用RGB模式
	# 随机生成4位字符
	chars = ''
	while len(chars) < 4:
		flag = random.randint(0, 2)
		char = chr(random.randrange(48, 57) if flag == 0 else random.randrange(65, 90) if flag == 1 else random.randrange(97, 122))
		if len(chars) == 0 or chars.count(char) == 0:
			chars += char
	# 保存验证字符到 session
	request.session['code'] = chars
	# 创建字体
	font = ImageFont.truetype(font='static/font/Fangz.ttf',size=25)
	# 绘制内容
	for char in chars:
		# 指定字体颜色
		font_color = (random.randrange(255), random.randrange(255), random.randrange(255))
		# 字符坐标
		xy = (15 + chars.index(char) * 25, random.randint(0, 5))
		# 绘制字符
		draw.text(xy, char,font=font,fill=font_color)
	# 画干扰点
	for i in range(50):
		# 干扰点坐标
		xy = (random.randrange(120), random.randrange(30))
		# 干扰点颜色
		p_color = (random.randrange(255), random.randrange(255), random.randrange(255))
		# 绘制干扰点
		draw.point(xy, p_color)
	# 删除画笔和字体,释放资源
	del draw
	del font
	# 生成图片对象,返回响应
	import io
	# 创建缓冲对象
	buf = io.BytesIO()
	# 保存图片到缓冲区
	img.save(buf, 'png')
	# 从缓冲区获取图片,添加到响应对象中,并返回
	return HttpResponse(content=buf.getvalue(), content_type='image/png')

前端使用验证码

在前端可以使用 img 标签来获取验证码图片,并添加点击重新获取事件

<img src='/new_code_img/' onclick='this.src="/new/code_img/?t="+new Date()*1'>

添加参数是为了确保每次请求时路径不一样,获取的都是新的验证码图片。

分页器 Paginator

django 提供了一个很方便的数据分页工具: 分页器 Paginator,用于数据模型的分页。使用时(通常在视图处理函数中)将查询到的数据模型传入即可。使用分页器可以十分方便的获取当前页号、上页下页页号、是否有上下页等信息。

from django.core.paginator import Paginator

def order_list(request):
	# 获取查询关键字
	kw = request.GET.get('kw', '')
	# 获取展示页码
	page = request.GET.get('p', 1)
	# 获取查询数据
	orders = Order.objects.filter(Q(title__icontains=kw)).all()
	# 创建分页器,参数1是数据模型对象,参数2是每页显示记录数量
	paginator = Paginator(orders, 5)
	# 获取分页后的数据,即第几页内的数据
	pager = paginator.page(page)
	return render(request, 'list.html', locals())

前端使用 pager.object_list 获取数据,还可以使用 paginator.page_range 获取全部页码,还有一些其他分页信息也能方便的获得。

<html lang="en">
	<head>
		<meta charset="UTF-8">
		<title>商品列表</title>
		<style>
			.page {
      
      
				text-decoration: none;
				cursor: pointer;
				padding: 5px;
				margin: 5px;
			}
			.active {
      
      
				background-color: lightblue;
			}
		</style>
	</head>
	<body>
		<form method="GET">
			<input type="text" name='kw' placeholder="商品名称">
			<button>搜索</button>
		</form>
		<h3>订单查询结果</h3>
		<table border="1" cellspacing="0" cellpadding="2" width="100%">
			<thead>
				<th>ID </th>
				<th>名称</th>
				<th>单价</th>
				<th>支付状态</th>
			</thead>
			<tbody>
				{% for order in pager.object_list %}
				<tr>
					<td>{
   
   { order.id }}</td>
					<td>{
   
   { order.title }}</td>
					<td>{
   
   { order.price }}</td>
					<td>{
   
   { order.pay_status }}</td>
				</tr>
				{% empty %}
				<tr>
					<td colspan="">没有查到数据</td>
				</tr>
				{% endfor %}
			</tbody>
		</table>
		<p style="text-align: center;">
			<a {% if pager.has_previous %} href="?p={
     
     { pager.previous_page_number }}&kw={
     
     { kw }}" {% endif %}>&lt;</a>
			{% for p in paginator.page_range %}
				{% if p == pager.number %}
				<a class="page active">{
   
   { p }}</a>
				{% else %}
				<a class="page" href="?p={
     
     { p }}&kw={
     
     { kw }}">{
   
   { p }}</a>
				{% endif %}
			{% endfor %}
			<a {% if pager.has_next %} href="?p={
     
     { pager.next_page_number }}&kw={
     
     { kw }}" {% endif %}>&gt;</a>
		</p>
	</body>
</html>

类视图(CBV)

django 提供了很多视图处理类,来代替视图处理函数(类似于 tornado)。

  • View
  • TemplateView
  • RedirectView
  • ListView
  • EditView
  • FormView
  • DetailView
  • DeleteView

一个简单的 View 例子:

from django.views import View

class GoodsView(View):
	# 处理 get 请求
	def get(self, request):
		pass
	# 处理 post 请求
	def post(self, request):
		pass

需注意的是,注册路由时注册路由处理类并使用其 as_view() 方法

urlpatterns = [
	path('goods/', views.GoodsView.as_view(), name='goods')
]

具体的使用方法可以参考官方文档

官方文档-内置的基于类的视图

django 的中间件

中间件基于 AOP(面向切面编程)的设计思想,目的是扩展业务,即在不修改原业务的基础上,添加新的功能,有些类似于装饰器。

django 收到一个请求后,会经过一系列的中间件后达到视图函数,经过视图函数处理后,再经过这些中间件后将数据返回给用户浏览器。

中间件处理请求的方法是 process_request,处理响应的方法是 process_response。处理顺序为:接收到请求交给 process_request ,然后交给 url 分发器,之后是 process_view,然后交给视图处理函数,接下来是 process_template_response(不常用),然后是处理数据模型、通过视图渲染模板,再下来是 process_response ,最后将响应发回给浏览器。如果从请求到响应的过程中出现错误,则交给 process_exception 处理,然后返回错误信息到浏览器。

如果请求在通过中间件时, process_request 没有返回值(或返回值为 None),则会继续交给下一步处理;如果有返回值(返回值是 HttpResponse、render、redirect),则会禁止请求往下进行,而将返回值交给本类的 process_response 方法返回请求,直接返回给客户端,所以可以将一些放在视图函数之前或之后的行为写在中间件中。

中间件的处理顺序按照在 settings.py 中注册的顺序,请求是顺序处理,响应是逆序处理。

使用中间件

django 使用的中间件可以在 settings.py 中的 MIDDLEWARE 字段定义。此字段是一个字符串列表,每一个字符串就是引用的中间件所在。

自定义中间件

所有自定义的中间件(类)需继承 django.utils.deprecation.MiddlewareMixin 类。并且要在 settings.py 中添加注册

from django.utils.deprecation import MiddlewareMixin
from django.shortcuts import redirect

class AuthMiddleware(MiddlewareMixin):
    """访问验证中间件"""
    def process_request(self, request):
        # 如果请求访问 login 页面则不进行检测
        if request.path_info == '/login/':          # request.path_info 能获取请求URL
            return
        # 1. 读取当前访问用户的 session 信息,如果能读到,说明已经登录过
        username = request.sesssion.get('username')
        if username:
            return
        # 2. 如果没有登录过,
        else:
            return redirect('/login/')
    
    def process_view(self, request):
    	pass

	def process_template_response(self, request, response):
		pass

    def process_response(self, request, response):
        return response

信号机制

django 提供了信号调度机制,用于监听框架中执行某些操作,实现业务与模型或视图之间解耦。django 中某些动作发生的时候,系统会根据信号定义的函数执行相应的操作。

发送信号有点像是调用函数并传参,只是在发送信号时并不知道需要调用什么函数,所以设立了信号机制。只在有需要时才创建接收信号的函数或方法。

另外使用信号时需注意,信号机制是线程安全的,但无法跨进程使用

一般信号写在应用 app 的 __init__.py 文件中。但是 app 在注册中因为加载好,是不能引入 models、urls、views 的,会报错。所以一般此类会产生错误的信号会写在 app 配置中,在 app 准备好后导入信号

# app.apps.py
from django.apps import AppConfig

class MyAppConfig(AppConfig):
    name = 'myApp'

    def ready(self):
        # 这里写入信号,或导入信号文件
        import myApp.signals

常用的内置信号

django 内置了一些信号:

  • 模型和迁移 django.db.models.signals
信号 说明
pre_init model 对象执行其构造方法前,自动触发
post_init model 对象执行其构造方法后,自动触发
pre_save model 对象保存前,自动触发
post_save model 对象保存后,自动触发
pre_delete model 对象删除前,自动触发
post_delete model 对象删除后,自动触发
m2m_changed model 对象使用 ManyToMany 字段操作数据库的第三张表(add/remove)
class_prepared 程序启动时,检测到已注册的model类,对于每一个类,自动触发
pre_migrate 执行migrate命令前,自动触发
post_migrate 执行migrate命令后,自动触发
  • 请求响应和配置文件 django.core.signals
信号 说明
request_started 请求来到前,自动触发
request_finished 请求结束后,自动触发
got_request_exception 请求异常时,自动触发
setting_changed 配置文件改变时,自动触发
  • 模板渲染 django.test.signals
信号 说明
template_rendered 模板执行渲染时,自动触发
  • 数据库 django.db.backends.signals
信号 说明
connection_created 创建数据库连接时,自动触发

信号的使用

信号函数的参数定义是固定的,例如 sender 表示信息发送对象。

信号的监听使用信号的 connect() 方法,其定义为:Singal.connect(receiver, sender=None, weak=True, dispatch_uid=None)。参数含义为:

  • receiver : 将被连接到这个信号的回调处理函数。
  • sender : 可指定监控的信号发送者。如果不指定发送者,则监控所有可能触发信号的发送者
  • weak : django 默认将信号处理程序存储为弱引用,这意味着如果处理程序是一个本地函数,可能会被垃圾回收,所以需要添加 weak=False。此参数最大的作用是对于信号处理函数是某对象的方法时,如果对象被删除了,可以回收信号而不会产生错误。
  • dispatch_uid : 在可能发送重复信号的情况下,信号接收器的唯一标识符。
from dango.db.models.signals import post_save

def post_save_func(sender, **kwargs):
	# 输出相关信息
	print('保存信息:', sender, kwargs)

# 连接信号和处理函数
post_save.connect(post_save_func, weak=False)

也可以使用装饰器的方式

from django.db.models.signals import post_save
from django.dispatch import receiver
from models import Test

@receiver(post_save, weak=False)
def post_save_func(sender, **kwargs):
	# 输出相关信息
	print('保存信息:', sender, kwargs)

信息的传递,接收器函数

def singal_handler(sender, **kwargs):
	pass

django 信号处理函数主要参数有两个,sender 是信号发送者,kwargs 是个字典,存储了传递的基本信息。例如 instance 为引起信号的实例对象,signal 为传递的信号对象等。

自定义信号

除了 django 预设的信号外,我们也可以自定义一些信号

定义信号

定义信号就是

from django import dispatch

# providing_args 发送信息的参数列表
action = dispatch.Signal(providing_args=['name', 'age'])

注册信号

注册信号就是将信号发给接收函数

@receiver(action)
def post_action(sender, **kwargs):
	print(sender, kwargs)

发送信号

在行为发生时(函数、方法执行中)发送信号。发送的数据除了 sender 外,在定义信号时就定义好了。

action.send(sender='sender', name='Joe', age=18)

信号 receiver 的管理

Signal提供了几个实例方法,用于管理receiver:

  • connect(self, receiver, sender=None, weak=True, dispatch_uid=None)
    连接一个 receiver。
  • disconnect(self, receiver=None, sender=None, dispatch_uid=None)
    断开一个receiver。
  • has_listeners(self, sender=None)
    判断某个signal的sender是否存在receiver
  • send(self, sender, **named)
    某个sender发出signal。返回[(receiver, response), … ]
  • send_robust(self, sender, **named)
    send_robustsend 不同的地方在于receiver出现异常时的行为:调用send时,如果receiver发生异常,异常会在send中继续抛出,如果当前receiver后面还有其他receiver,因为异常的发生,将不会调用。调用send_robust时,如果receiver发生异常exc,会被捕获,并以(receiver, exc)作为结果返回给send_robust;没有异常时,则和send一样,以(receiver, response)作为结果返回。

日志 Logging

django 的日志由版本号(version)、格式化(formatters)、处理器(handlers)、记录器(loggers)、过滤器(filters)五部分组成。django 的日志记录器默认存在"django.server"和"django.request"两个。

配置日志需要在 settings.py 中声明 LOGGING,它是一个字典。

LOGGING = {
    
    
	'version': 1,	# 版本号
	'disable_existing_logger': False,	# 是否禁用已经存在的记录器
	'formatters': {
    
    		# 声明 格式化输出
		'simple': {
    
    		# 声明格式化的名称,后面在记录器中会使用
			'format': "%(asctime)s %(module)s.%(funcName)s: %(message)s",	# 输出格式
			'datefmt': '%Y-%m-%d %H:%M:%S'	# 时间格式
		}
	},
	'handlers': {
    
    	# 声明处理器,如文件输出、控制台输出、发送邮件等
		'inf': {
    
    
			'class': 'logging.handlers.TimedRotatingFileHandler',	# 处理器类
			'filename': f'{
      
      BASE_DIR}/out.log',		# 输出文件名
			'when': 'WO',	# 每周一切割日志
			'backupCount': 5,	# 备份数量
			'formatter': 'simple',	# 使用的格式
			'level': 'DEBUG' if DEBUG else 'INFO',	# 处理级别
		},
		'err': {
    
    
			'class': 'logging.handlers.TimedRotatingFileHandler',	# 处理器类
			'filename': f'{
      
      BASE_DIR}/err.log',		# 输出文件名
			'when': 'D',	# 每天切割日志
			'backupCount': 5,	# 备份数量
			'formatter': 'verbose',	# 使用的格式
			'level': 'WARNING',
		},
		'out': {
    
    
			'class': 'logging.StreamHandler',	# 处理器类
			'formatter': 'simple',	# 使用的格式
			'level': 'INFO',
		},
		'file': {
    
    
			'class': 'logging.FileHandler',	# 处理器类
			'formatter': 'simple',	# 使用的格式
			'level': 'WARNING',
			'filename': f'{
      
      BASE_DIR}/warn.log',
			'when': 'D',	# 每天切割日志
			'backupCount': 7,	# 备份数量
		}
	},
	'loggers': {
    
    	# 日志记录器
		'inf': {
    
    
			'handlers': ['inf'],	# 使用的处理器,一个记录器能使用多个处理器
			'level': 'DEBUG',		# 记录级别
			'propagate': True,		# 是否传播
		},
		'err': {
    
    
			'handlers': ['err'],
			'level': 'DEBUG',
			'propagate': True,
		},
		'django': {
    
    
			'handlers': ['out', 'file'],	# 使用的处理器,一个记录器能使用多个处理器
			'level': 'INFO',		# 记录级别
			'propagate': True,		# 是否传播
		}
	}
}

配置好后可以使用,一般会在中间件里,例如对请求进行日志记录

import logging

class LoggingMiddleware(MiddlewareMixin):
	def process_request(self, request):
		ip = request.META.get('REMOTE_ADDR')
		path = request.get_raw_uri()
		msg = "%s 访问 %s" %(ip, path)
		# 获取日志记录器 django,并记录 INFO 级消息
		logging.getLogger('django').info(msg)

缓存

django 有多种缓存方案,可以将渲染好的页面、session 等数据存放在缓存中等待调用。缓存也有很多选择,本地、文件服务器、redis 服务器、数据库服务器等。详细的可以查看官方文档。

官方文档-缓存框架

配置缓存

自定义缓存需要在 settings.py 中进行配置,例如

文件缓存:

CACHES = {
    
    
	'default': {
    
    	# 缓存方案名称
		'BACKEND': 'django.core.cache.backends.filebased.FileBasedCache',	# 缓存方案(使用哪种缓存技术)
		'LOCATION': 'c:/foo/bar',		# 数据存储地点名称
		'TIMEOUT': 300,		# 超时时间,单位是秒
		'OPTIONS': {
    
    		# 其他的一些选项
			'MAX_ENTRIES': 300,		# 最大的实体数
		},
	}
}

内存缓存:

CACHES = {
    
    
	'default': {
    
    
		'BACKEND': 'django.core.cache.backends.locmem.LocMemCache',
		'LOCATION': 'unique-snowflake',
	}
}

原生缓存

原生缓存用来保存一些临时性的数据,有点像 session,但是是不依赖、不区分客户端的。timeout 时间单位是秒。

from django.core.cache import cache

cache.add(key, value, timeout=DEFAULT_TIMEOUT, version=None)			# 添加缓存,如果 key 已存在则放弃
cache.set(key, value, timeout=DEFAULT_TIMEOUT, version=None)			# 设置缓存 key、value、timeout
cache.set_many(dict, timeout)		# 传入字典,一次设置多个缓存
cache.get(key, default=None, version=None)		# 获取缓存 key 的 value
cache.get_many(keys, version=None)		# keys 是列表,返回字典,返回多个缓存
cache.delete(key, version=None)		# 显性删除 key 和其 value
cache.clear()		# 清空所有缓存

更多详细的使用方式可以查看官方文档

官方文档-缓存框架-底层缓存API

缓存方案

cache 缓存默认使用 default 方案,如果希望使用其他方案名称,或需要操作其他方案名称中的缓存数据可以这样指定缓存方案:

from django.core.cache import caches

cache = caches['other_cache']		# 指定缓存方案

然后就可以正常操作 cache 了。

缓存视图

使用缓存保存渲染好的视图结果,可以使用装饰器 @cache_page ,它将自动缓存视图的响应。

from django.views.decorators.cache import cache_page

@cache_page(timeout=60,cache='default',key_prefix=None)		# cache为settings.py中设置的缓存方案
def index(request):
	pass

使用 redis 缓存

redis 的特性决定了它是一个高速缓存的分布式数据库,所以大多数的框架缓存都会使用 redis。django 使用 redis 可以使用其插件 django-redis,然后在 settings.py 的 CACHES 中进行设置

pip install django-redis

CACHES = {
    
    
	'redis': {
    
    	# 缓存方案名称
		'BACKEND': 'django_redis.cache.RedisCache',	# 缓存方案
		'LOCATION': 'redis://127.0.0.1:6379/1',		# redis 的主机地址、端口和数据库编号
		'OPTIONS': {
    
    		# 其他的一些选项
			'CLIENT_CLASS': 'django_redis.client.DefaultClient',	# 连接客户端
			'PASSWORD': 'mysecret',		# 口令
			'SOCKET_CONNECT_TIMEOUT': 5,	# 连接超时时间,单位秒
			'SOCKET_TIMEOUT': 5,	# 读写超时时间,单位秒
			'CONNECTION_POOL_KWARGS': {
    
    'max_connections': 10,}		# 连接池参数
		},
	}
}

django-redis 的连接池

当配置好缓存信息后,可以在代码中使用连接池

from django.core.cache import get_cache
from django_redis import get_redis_connection

r = get_redis_connection('redis')
connection_pool = r.connection_pool

猜你喜欢

转载自blog.csdn.net/runsong911/article/details/127936118