一起学习Django框架(八)Ajax请求;分页器;form组件校验字段;form组件源码分析

2021/4/10 12:27:50

本文主要是介绍一起学习Django框架(八)Ajax请求;分页器;form组件校验字段;form组件源码分析,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

目录

  • Ajax
    • 一、什么是Ajax
    • 二、基于jQuery实现Ajax
    • 三、使用Ajax注意的问题
    • 四、Ajax上传文件
    • 五、Ajax上传JSON格式
  • 分页器
    • 1、实验环境搭建
    • 2、分页实现
      • 2.1 分页器介绍
      • 2.2 分页效果
  • form组件
    • 1、介绍form组件
    • 2、form组件渲染页面的方式
    • 3、form组件渲染错误信息
    • 5、form组件的参数配置
    • 6、局部钩子
    • 7、全局钩子
    • 8、整合使用
  • form组件源码分析
    • 1、局部钩子源码
    • 2、全局钩子源码


简介:

在以往章节中,我们都是使用form表单进行请求提交的,在本章节我们将了解到一种新的提交请求的方式:Ajax。它是一种新的与后端交互的方式,我们一起来了解一下吧!



Ajax

一、什么是Ajax

AJAX(Asynchronous Javascript And XML)翻译成中文就是“异步Javascript和XML”。即使用Javascript语言与服务器进行异步交互,传输的数据为XML(当然,传输的数据不只是XML,现在更多使用json数据)。

同步交互:客户端发出一个请求后,需要等待服务器响应结束后,才能发出第二个请求;

异步交互:客户端发出一个请求后,无需等待服务器响应结束,就可以发出第二个请求。

AJAX除了异步的特点外,还有一个就是:浏览器页面局部刷新;(这一特点给用户的感受是在不知不觉中完成请求和响应过程)

例如:
在这里插入图片描述
我们并没有点击按钮提交,这是因为Ajax在我们输入完毕后点击其它地方时就自动把我们输入的数据提交到了后端进行并在数据库匹配,然后拿到匹配结果返回到前端。

Ajax的优点:

  • AJAX使用Javascript技术向服务器发送异步请求
  • AJAX无须刷新整个页面


二、基于jQuery实现Ajax

目前Ajax一般不会使用原生JavaScript来编写,因为需要考虑不同浏览器的兼容性。我们这里通过jQuery来实现,更简单、不需要考虑不同浏览器引发的兼容问题。

过程:我们通过在前端向一个URL发送Ajax请求,来让后端处理这个请求后返回数据给Ajax接收。

先在Django配置路由

from django.contrib import admin
from django.urls import path
from app01 import views

urlpatterns = [
    path('admin/', admin.site.urls),
    path('index/', views.index),
    path('sum/', views.sums) # ajax会向这个路由提交请求,执行susm视图函数
]

注意:一定要在settings.py配置文件里面注释中间件的一行内容
在这里插入图片描述
这行代码的作用后续还讲解,目前先注释掉,不然请求会提交不成功!

views.py处理请求

from django.shortcuts import render,HttpResponse

# Create your views here.

def index(request): # 返回一个index页面
    return render(request,'index.html')

def sums(request): # 处理ajax请求
    num1 = int(request.POST.get('num1'))
    num2 = int(request.POST.get('num2')) # 获取post请求里面携带的数据,并进行类型转换

    return HttpResponse(num1+num2) # 返回计算后的值

index.html文件,定义Ajax与后端进行交互

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>index</title>
    <script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.4.1/jquery.min.js"></script>
    <script src="https://cdn.bootcdn.net/ajax/libs/twitter-bootstrap/3.4.1/js/bootstrap.min.js"></script>
    <link href="https://cdn.bootcdn.net/ajax/libs/twitter-bootstrap/3.4.1/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
<input type="text" id="num1" placeholder="数字1">+
<input type="text" id="num2" placeholder="数字2">=
<input type="text" id="num3" placeholder="总和">
<button id="btn">计算</button>

<script>
    $('#btn').click(function () { // 当按钮被点击时触发一个匿名函数
        var num1 = $('#num1').val() // 点击按钮后获取id属性值为num1的值
        var num2 = $('#num2').val() // 点击按钮后获取id属性值为num2的值
        $.ajax({ // 定义ajax发送请求
            url: '/sum/', // 请求发送的地址
            method: 'post', // 请求发送的方式
            data: {'num1': num1, 'num2': num2}, // 请求携带的数据
            success: function (data) { // 异步等待,当后端响应成功会回调执行匿名函数,并将数据传递给data形参
                $('#num3').val(data) // 将数据设置给id属性值为num3的文本框
            }
        })
    })
</script>
</body>
</html>

我们已经编写好了一个简单的小案例,通过Ajax向后端发送请求,后端处理完数据响应给Ajax,再将得到的数据在页面使用以此来达到局部更新页面效果。而form表单要达到这一效果需要全局更新数据,使用重定向来实现。

最终效果:
在这里插入图片描述



三、使用Ajax注意的问题

1、Ajax不要与form表单同时提交

  • 如果在form表单中,写了button按钮或者input是submit类型的话,点击会触发form表单的提交。

  • 如果点击按钮既触发Ajax提交又触发了form表单的提交,那么就会发送两个请求,导致页面数据可能错乱或者不显示的问题。

  • 要是无可避免同时使用Ajax提交form表单下的表单控件时,form表单内的input按钮请使用button类型,这样该按钮绑定了Ajax请求的话,就只会提交Ajax请求,而不是form

    <!-- 这种类型的按钮,无法触发form表单的提交 -->
    <input type="button" value="提交">
    
    <!-- 如果该button在form内,则会触发form表单的提交 -->
    <button>提交</button>
    

2、后端响应格式问题

  • 后端如果是通过非JsonResponse返回值的话,响应格式为text/html,前端得到数据后需要手动转换成对象的形式。

        if request.is_ajax(): # 判断请求是否为Ajax
            import json
            dic = {'name':'jack'}
            dic_json = json.dumps(dic,ensure_ascii=False)
            return HttpResponse(dic_json)
    

    就算我们在后端已经将字典转换成了Json格式,但是通过HttpResponse返回以后,还是变成了一堆字符串到前端,并且响应格式为:text/html

    <script>
        $('#btn').click(function () {
            $.ajax({
                url: '/test/',
                method: 'post',
                data: {'name': 'tom'},
                success: function (data) {
                    console.log(data)
                    console.log(data.name) // 如果返回的是Json的话这样是可以打印出来的
                }
            })
        })
    </script>
    

    查看后端响应的数据格式:

    在这里插入图片描述
    控制台效果:

    在这里插入图片描述
    需要在前端手动将数据转换成JSON格式,那么下次就会先将数据转换成JSON,然后再打印了

    success: function (data) {
        data = JSON.parse(data)
        console.log(data)
        console.log(data.name)
    }
    
  • 如果我们是通过JsonResponse返回的话,那么响应状态码就会是application/json,并且在Ajax内接收到的也会是一个JSON格式的数据

    from django.http import JsonResponse
    
    def test(request):
        if request.is_ajax():
            dic = {'name':'jack'}
            return JsonResponse(dic)
        return render(request,'test.html')
    

    AJax内接收到以后不需要手动转换成JSON格式

    success: function (data) {
        console.log(data)
        console.log(data.name)
    }
    

    查看响应头编码格式

    在这里插入图片描述
    控制台打印效果:

    在这里插入图片描述

3、使用了Ajax作为请求后的注意事项

  • 后端不要返回render、redirect、HttpResponse。因为这些内容会被统一当做字符串返回给Ajax

    def test(request):
        if request.is_ajax():
            return render(request,'login.html')
    
        return render(request,'test.html')
    

    控制台打印效果:

    在这里插入图片描述

总结:使用了AJax作为请求以后,建议使用JsonResponse返回数据,虽说最终还是通过HttpResponse返回到web的,但是JsonResponse内部就做了响应格式的转换

在这里插入图片描述
这种和我们直接通过HttpResponse返回一个JSON格式数据是不一样的。AJax只会根据响应码来做处理



四、Ajax上传文件

在form表单内,我们是通过指定了一下编码格式才可以将文件上传到后端,而通过Ajax我们将不再借助form表单来实现这一效果

file_upload.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>file_upload</title>
    <script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.4.1/jquery.min.js"></script>
    <script src="https://cdn.bootcdn.net/ajax/libs/twitter-bootstrap/3.4.1/js/bootstrap.min.js"></script>
    <link href="https://cdn.bootcdn.net/ajax/libs/twitter-bootstrap/3.4.1/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>

<h2>基于Ajax上传文件</h2>

<p>
    <input type="file" id="myfils">

</p>

<p>
    <button id="btn">文件上传</button>

</p>

<script>
    $("#btn").click(function () {

        var form_data = new FormData() // 实例化一个对象,FormData 对象用来保存key/value结构的数据,通常用于form传输数据

        var file = $('#myfils')[0].files[0] // $('#myfils')[0]获取原生JS、$('#myfils')[0].files获取用户上传的所有文件、$('#myfils')[0].files[0]获取用户上传的第一个文件
        form_data.append('myfile',file) // 给该文件定义一个取值名称 第一个参数:自定义,第二个参数:值
        $.ajax({
            url: '/upload/',
            method:'post',
            processData: false, // 不预处理数据
            contentType: false, //不指定编码格式,使用formdata对象的默认编码就是formdata格式,支持文件传输
            data: form_data,
            success:function (data) {
                console.log(data.msg)
            }
        })
    })
</script>
</body>
</html>

后端views.py,建议在app下面建立一个media文件夹来存储用户上传的文件

def upload_file(request):
    if request.is_ajax():
        file = request.FILES.get('myfile')

        import os
        DIR_PATH = os.path.dirname(__file__)

        with open(DIR_PATH + '/media/' + file.name, 'wb') as f:
            for i in file:
                f.write(i)

        return JsonResponse({'status':100,'msg':'上传成功'})

    return render(request, 'file_upload.html')

最终效果:

在这里插入图片描述
如果我们要实现多个文件上传,稍微做出一些改动即可:

file_upload.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>file_upload</title>
    <script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.4.1/jquery.min.js"></script>
    <script src="https://cdn.bootcdn.net/ajax/libs/twitter-bootstrap/3.4.1/js/bootstrap.min.js"></script>
    <link href="https://cdn.bootcdn.net/ajax/libs/twitter-bootstrap/3.4.1/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
<h2>基于Ajax上传文件</h2>

<p>
    <input type="file" id="myfils" multiple> <!-- 允许上传多个文件  -->
</p>

<p>
    <button id="btn">文件上传</button>
</p>

<script>
    $("#btn").click(function () {

        var form_data = new FormData() // 实例化一个对象,FormData 对象用来保存key/value结构的数据,通常用于form传输数据

        var file = $('#myfils')[0].files // 获取需要上传的所有文件,拿到的是一个列表的形式

        for(var i = 0;i < file.length;i++) { // 根据上传文件的数量来进行遍历
            form_data.append(file[i].name, file[i]) // 将每个文件的名称作为key,文件作为value追加到FormData对象内
        }
        
        $.ajax({
            url: '/upload/',
            method:'post',
            processData: false, // 不预处理数据
            contentType: false, //不指定编码格式,使用formdata对象的默认编码就是formdata格式,支持文件传输
            data: form_data,
            success:function (data) {
                console.log(data)
            }
        })
    })
</script>
</body>
</html>

Python后端views.py

def upload_file(request):
    if request.is_ajax():
        import os
        DIR_PATH = os.path.dirname(__file__)
        file = request.FILES

        for k in file:
            fl = file.get(k)
            with open(DIR_PATH + '/media/' + fl.name, 'wb') as f:
                for i in fl:
                    f.write(i)

        return JsonResponse({'status':100,'msg':'上传成功'})

    return render(request, 'file_upload.html')

执行结果:

在这里插入图片描述



五、Ajax上传JSON格式

通过在Ajax内指定好编码格式,然后将JS的数据类型转换成JSON格式数据上传给后端

test.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.4.1/jquery.min.js"></script>
</head>
<body>

<h1>ajax提交json格式</h1>


<p>用户名: <input type="text" id="id_name"></p>
<p>密码: <input type="password" id="id_password"></p>
<button id="id_button">提交</button>
</body>

<script>
    $('#id_button').click(function () {
        var username = $('#id_name').val()
        var password = $('#id_password').val()
        $.ajax({
            url: '/ajax_json/',
            method: 'post',
            contentType: 'application/json',  // 指定上传的编码格式
            data: JSON.stringify({name: username, password: password}), // 将对象通过JSON.stringify转换成JSON格式
            success: function (data) {
                console.log(data)
            }
        })
    })
</script>
</html>

后端views.py:并不能同以往来处理请求里面的数据了

def ajax_json(request):
    if request.is_ajax():
        # json格式,从POST中取不出来
        name = request.POST.get('name')
        print(type(request.POST)) # 打印<class 'django.http.request.QueryDict'>

        print(name) # 打印:None

        # Ajax上传的JSON格式数据在request.body中,且是bytes类型的
        print(request.body) # b'{"name":"123","password":"123"}'

        request.data = json.loads(request.body) # 在Python3.6之后loads可以将bytes里面如果有JSON格式数据,可以转换出来

        name = request.data.get('name')
        password = request.data.get('password')
        print(name) # 123
        print(password) # 123
        return JsonResponse({'status':200,'msg':'提交成功'})

    return render(request, 'test.html')

最终效果:

在这里插入图片描述



分页器

这里我们将利用Django提供给我们的一个分页模块来帮助我们的页面达到一个分页的效果

1、实验环境搭建

我们首先建立一个模型类来存储数据,因为要实现分页需要一定的数据量。

models.py;建议连接到MySQL数据库下进行实验

class UserInfo(models.Model):
    name = models.CharField(max_length=10)
    age = models.CharField(max_length=10)
    address = models.CharField(max_length=30)

记得先执行数据库迁移命令:makemigrations、migrate

urls.py

from django.contrib import admin
from django.urls import path
from app01 import views

urlpatterns = [
    path('admin/', admin.site.urls),
    path('info/',views.info)
]

tests.py 制作我们待会进行分页的数据,并将其写入数据库内

import os

if __name__ == '__main__':
    os.environ.setdefault("DJANGO_SETTINGS_MODULE", "Day06.settings")
    import django

    django.setup()

    from app01 import models
    from faker import Faker  # 生成加的数据
    import random  # 用来创建随机年龄

    obj_lis = []

    for i in range(100): # 制作100条虚拟数据
        fake = Faker(locale='zh-CN') # 选择地区

        name = fake.name()  # 生成随机名称
        age = random.randint(15, 45)  # 生成随机年龄
        address = fake.address()  # 生成随机地址

        obj = models.UserInfo(name=name,age=age,address=address,birthday=birthday) # 生成一个个用户对象

        obj_lis.append(obj)

    models.UserInfo.objects.bulk_create(obj_lis) # 将一个个对象写入数据库内保存,一个对象对应一条记录

views.py:先简单的搭建出模型,在web能看到数据效果

def info(request):
    
    data = models.UserInfo.objects.all()
    
    return render(request,'info.html',locals())

info.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>info</title>
    <script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.4.1/jquery.min.js"></script>
    <script src="https://cdn.bootcdn.net/ajax/libs/twitter-bootstrap/3.4.1/js/bootstrap.min.js"></script>
    <link href="https://cdn.bootcdn.net/ajax/libs/twitter-bootstrap/3.4.1/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
<div class="container">
    <div class="row">
        <div class="col-md-2"></div>
        <div class="col-md-8">
            <table class="table table-striped">
                <thead>
                <tr>
                    <th class="text-center">ID</th>
                    <th class="text-center">姓名</th>
                    <th class="text-center">年龄</th>
                    <th class="text-center">住址</th>
                </tr>
                </thead>

                <tbody>
                {% for obj in data %}
                    <tr class="text-center">
                        <td>{{ obj.id }}</td>
                        <td>{{ obj.name }}</td>
                        <td>{{ obj.age }}</td>
                        <td>{{ obj.address }}</td>
                    </tr>
                {% endfor %}

                </tbody>

            </table>
        </div>
    </div>
</div>
</body>
</html>

最终页面整体效果:

在这里插入图片描述


2、分页实现

2.1 分页器介绍

我们需要先对数据进行分页,再根据用户需要哪一页数据,我们就返回对应的。

在Django内有一个模块可以帮助我们实现分页,那么我们来了解一下它的使用方式:

from django.core.paginator import Paginator # 实现分页的模块

def info(request):
    user_list = models.Books.objects.all() # 获取全部数据对象
    paginator = Paginator(user_list,10) # 将数据拆分成每页显示10条
    
    # Paginator对象的属性
    print(paginator.count) # 数据总条数
    print(paginator.num_pages) # 总页数
    print(paginator.per_page) # 每页显示条数
    print(paginator.page_range) # 迭代的形式显示当前页数:range(1, 101)

    # Page对象的属性和方法
    page = paginator.page(2) # 获取取出第二页的全部数据
    print(page.has_next())
    print(page.next_page_number())
    print(page.has_previous())
    print(page.previous_page_number())
    print(page.object_list)
    print(page.number)
    
	# has_next              是否有下一页
    # next_page_number      下一页页码
    # has_previous          是否有上一页
    # previous_page_number  上一页页码
    # object_list           当前页的数据列表
    # number                当前页码

2.2 分页效果

根据上序分页器的使用方式,我们来编辑视图函数

from django.core.paginator import Paginator

def info(request):

    data_list = models.UserInfo.objects.all()

    paginator = Paginator(data_list,10) # 每页显示10条数据

    nums = paginator.num_pages # 获取总页数

    page = request.GET.get('page',1) # 获取页面输入的页码,如果用户没有输入,默认获取数字1

    try:
        data = paginator.page(page) # 根据页码获取指定的页的数据
    except: # 避免浏览器输入错误的URL找不到页面
        
        page = 1
        data = paginator.page(page) # 获取第一页的数据


    return render(request,'info.html',locals())

info.html;根据页面提示来理解代码的意思

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>info</title>
    <script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.4.1/jquery.min.js"></script>
    <script src="https://cdn.bootcdn.net/ajax/libs/twitter-bootstrap/3.4.1/js/bootstrap.min.js"></script>
    <link href="https://cdn.bootcdn.net/ajax/libs/twitter-bootstrap/3.4.1/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
<div class="container">
    <div class="row">
        <div class="col-md-2"></div>
        <div class="col-md-8">
            <table class="table table-striped">
                <thead>
                <tr>
                    <th class="text-center">ID</th>
                    <th class="text-center">姓名</th>
                    <th class="text-center">年龄</th>
                    <th class="text-center">住址</th>
                </tr>
                </thead>

                <tbody>
                {% for obj in data %} <!-- 获取当前页的数据 -->
                    <tr class="text-center">
                        <td>{{ obj.id }}</td>
                        <td>{{ obj.name }}</td>
                        <td>{{ obj.age }}</td>
                        <td>{{ obj.address }}</td>
                    </tr>
                {% endfor %}

                </tbody>

            </table>
        </div>
    </div>
</div>

<nav aria-label="Page navigation" class="text-center">
    <ul class="pagination">
        {% if data.has_previous %} <!-- 判断是否有上一页 -->
            <li>
                <a href="?page={{ data.previous_page_number }}" aria-label="Previous"> <!-- 获取上一页页码 -->
                    <span aria-hidden="true">&laquo;</span>
                </a>
            </li>
        {% endif %}

        <li class="active"><a href="#">{{ data.number }}</a></li> <!-- 当前页 -->

        {% if data.has_next %} <!-- 判断是否有下一页 -->
            <li>
                <a href="?page={{ data.next_page_number }}" aria-label="Next">
                    <span aria-hidden="true">&raquo;</span>
                </a>
            </li>
        {% endif %}

    </ul>
</nav>
</body>
</html>

那么此时页面达到效果:

在这里插入图片描述
当我们点击下一页,然后首先会获取下一页的页码,然后发送给后端,后端根据page关键字取出来后面的数字进行取对应页的数据。

但此时我们可以点击按钮只有三个,如果我们想要点击到n页,那么就要对按钮显示个数做出一个跳转,但必须是由后端来做的。

views.py

def info(request):

    data_list = models.UserInfo.objects.all()

    paginator = Paginator(data_list,10) # 每页显示10条数据

    nums = paginator.num_pages # 获取总页数

    page = request.GET.get('page',1) # 获取页面输入的页码,如果用户没有输入,默认获取数字1

    try:
        data = paginator.page(page) # 根据页码获取指定的页的数据
    except: # 避免浏览器输入错误的URL找不到页面
        page = 1
        data = paginator.page(page) # 获取第一页的数据

    page = int(page) # 将数据类型转换一下


    if paginator.num_pages > 7: # 判断总页码数是否大于指定的数字
        if page - 5 < 1:
            page_range = range(1, 7) # 返回给前端,用于打印分页按钮的个数(根据不同的判断,分页按钮的个数也将不同)
        elif page + 5 > paginator.num_pages:
            page_range = range(paginator.num_pages - 6, paginator.num_pages + 1)
        else:
            page_range = range(page - 3, page + 5)
    else:
        page_range = paginator.page_range

    return render(request,'info.html',locals())

info.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>info</title>
    <script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.4.1/jquery.min.js"></script>
    <script src="https://cdn.bootcdn.net/ajax/libs/twitter-bootstrap/3.4.1/js/bootstrap.min.js"></script>
    <link href="https://cdn.bootcdn.net/ajax/libs/twitter-bootstrap/3.4.1/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
<div class="container">
    <div class="row">
        <div class="col-md-2"></div>
        <div class="col-md-8">
            <table class="table table-striped">
                <thead>
                <tr>
                    <th class="text-center">ID</th>
                    <th class="text-center">姓名</th>
                    <th class="text-center">年龄</th>
                    <th class="text-center">住址</th>
                </tr>
                </thead>

                <tbody>
                {% for obj in data %}
                    <tr class="text-center">
                        <td>{{ obj.id }}</td>
                        <td>{{ obj.name }}</td>
                        <td>{{ obj.age }}</td>
                        <td>{{ obj.address }}</td>
                    </tr>
                {% endfor %}

                </tbody>

            </table>
        </div>
    </div>
</div>

<nav aria-label="Page navigation" class="text-center">
    <ul class="pagination">
        {% if data.has_previous %} <!-- 判断是否有上一页 -->
            <li>
                <a href="?page={{ data.previous_page_number }}" aria-label="Previous"> <!-- 获取上一页页码 -->
                    <span aria-hidden="true">&laquo;</span>
                </a>
            </li>
        {% endif %}

        {% for foo in page_range %} <!-- 当前页面可以显示的按钮个数,以数字的形式 -->
            {% if foo == data.number %} <!-- 如果遍历创建按钮的同时,匹配到和当前页码一样的数字,那么就是当前页,给它进行变色 -->
                <li class="active"><a href="#">{{ foo }}</a></li> <!-- 当前页 -->
            {% else %}
                <li><a href="?page={{ foo }}">{{ foo }}</a></li> <!-- 通向其它页面的按钮 -->
            {% endif %}

        {% endfor %}

        {% if data.has_next %} <!-- 判断是否有下一页 -->
            <li>
                <a href="?page={{ data.next_page_number }}" aria-label="Next">
                    <span aria-hidden="true">&raquo;</span>
                </a>
            </li>
        {% endif %}

    </ul>
</nav>
</body>
</html>

那么此时页面达到的效果就是:可供用户选择到达页面的按钮个数变得更可观了,如图所示:

在这里插入图片描述
此时,我们的页面已经可以实现分页的效果了。页面按钮的个数,取决于后端的判断,通常返回一个可迭代类型给前端,如:range(1,5),指定循环创建按钮的个数



form组件

1、介绍form组件

Django Form 组件用于对页面进行初始化,生成 HTML 标签,此外还可以对用户提交对数据进行校验(显示错误信息)。

通常对用户提交的数据都会在后端进行校验是否合格,如:账号长度、密码长度、是否为空等等。如果这些写在前端JS内,完全是可以被手动更改或删除,然后将不符合要求的数据提交到后端,造成极大数据安全隐患问题。

一般会创建一个py文件来存放我们的forms组件,来校验用户数据的时候再来调用。

接下来我们在应用下创建一个名为:my_form.py

from django import forms
from django.core.exceptions import ValidationError


class UserForm(forms.Form):  # 校验用户信息的组件
    name = forms.CharField(max_length=8, min_length=4, label='用户名', error_messages={
        'max_length': '用户名不能超过8位',
        'min_length': '用户名不能低于4位',
        'required': '用户名不能为空'})

    password = forms.CharField(max_length=8, min_length=4, label='密码', error_messages={
        'max_length': '密码不能超过8位',
        'min_length': '密码不能低于4位',
        'required': '密码不能为空'})

    re_password = forms.CharField(max_length=8, min_length=4, label='确认密码', error_messages={
        'max_length': '密码不能超过8位',
        'min_length': '密码不能低于4位',
        'required': '确认密码不能为空'})

    email = forms.EmailField(label='邮箱', error_messages={'required': '邮箱不能为空'})

forms定义的方式有点类似于模型,我们来介绍一下它的字段参数意义:

  • label:输入框前面的文本信息
  • max_length:规定输入框内容的最大长度
  • min_length:规定输入框内容的最小长度
  • error_message:自定义显示的错误信息,属性值是字典, 其中 required 为设置不能为空时显示的错误信息的 key。

views.py

def myform(request):
    form = UserForm(request.POST)

    return render(request,'myform.html',locals())

myform.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>myform</title>
    <script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.4.1/jquery.min.js"></script>
    <script src="https://cdn.bootcdn.net/ajax/libs/twitter-bootstrap/3.4.1/js/bootstrap.min.js"></script>
    <link href="https://cdn.bootcdn.net/ajax/libs/twitter-bootstrap/3.4.1/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
<h2>通过form组件自动渲染一</h2>
<form action="" method="post">
    <p>用户名:{{ form.name }}</p>
    <p>密码:{{ form.password }}</p>
    <p>确认密码:{{ form.re_password }}</p>
    <p>邮箱:{{ form.email }}</p>
    <input type="submit" value="提交">
</form>
</body>
</html>

页面效果:通过form组件生成的文本框,并且携带了一些限制属性以及id属性值
在这里插入图片描述



2、form组件渲染页面的方式

上面只是渲染的方式一,再来介绍两种form组件渲染方式

方式二:推荐、推荐、推荐

<!-- 省略html起始,读者一定记得加上,以下代码应该被包含在body中 -->
<h2>通过form组件自动渲染</h2>
<form action="" method="post">
    {% for item in form %}
        <p>{{ item.label }}:{{ item }}</p>
    {% endfor %}
    <input type="submit" value="提交">
</form>

页面效果:
在这里插入图片描述
方式三:

<!-- 省略html起始,读者一定记得加上,以下代码应该被包含在body中 -->
<h2>通过form组件自动渲染</h2>
<form action="" method="post">
    {{ form.as_p }}
    <input type="submit" value="提交">
</form>

页面效果:这种方式对于页面拓展性很差,我们不能够增加别的标签,并且还将错误信息直接显示了出来。
在这里插入图片描述
总结:综合上序的三种,最常用的就是第二种方式来通过form组件自动渲染模板了



3、form组件渲染错误信息

视图

def myform(request):
    if request.POST:
        form = UserForm(request.POST)
        if form.is_valid(): # 判断前端输入的内容是否合格
            print(form.cleaned_data) # 所有干净的字段以及对应的值,也就是用户输入的是符合我们组件定义的规则:合格的数据
        else:
            print(form.cleaned_data)
            print(form.errors) # # ErrorDict : {"校验错误的字段":["错误信息",]}

        return render(request,'myform.html',locals())

    form = UserForm()

    return render(request,'myform.html',locals())

myform.html

<!-- 以下代码请放入body标签内 -->
<h2>通过form组件自动渲染</h2>
<form action="" method="post">
    {% for item in form %}
        <p>{{ item.label }}:{{ item }} <span style="color: red">{{ item.errors.0 }}</span><!-- 显示错误提示信息 --></p>
    {% endfor %}
    <input type="submit" value="提交">
</form>

第一步:这里主要演示提交不符合规则的数据到后端
在这里插入图片描述
第二部:后端会对不符合文本框的页面返回错误提示信息

在这里插入图片描述



5、form组件的参数配置

form.CharFeild默认创建的是text类型的输入框,我们可以在其基础上直接转换它的类型,并且加上一些额外属性。

from django import forms
from django.forms import widgets # 导入模块

class UserForm(forms.Form):  # 校验用户信息的组件
    name = forms.CharField(max_length=8, min_length=4, label='用户名', error_messages={
        'max_length': '用户名不能超过8位',
        'min_length': '用户名不能低于4位',
        'required': '用户名不能为空'},
        widget=widgets.TextInput(attrs={'class':'form-control'})) # 显示text类型的输入框,并且加上class='from-control'属性
        

    password = forms.CharField(max_length=8, min_length=4, label='密码', error_messages={
        'max_length': '密码不能超过8位',
        'min_length': '密码不能低于4位',
        'required': '密码不能为空'},
        widget=widgets.PasswordInput(attrs={'class':'form-control'})) # 显示password类型的输入框,并且加上class='from-control'属性                            


6、局部钩子

如果上面的操作不能满足我们对某个字段的完整校验,那么运用到局部钩子可以对某个字段进行额外校验,最终让这个字段的值达到我们所期望的样子。

在my_form.py里增加

# 针对name字段的局部钩子
def clean_name(self):
    name = self.cleaned_data.get('name') # 获取name字段数据

    if name.startswith('sb'): # name字段数据不能以sb开头
        raise ValidationError('不能以sb开头') # 校验不通过,抛出异常

    else: # 校验通过,返回name对应的值。
        return name

局部钩子的实现是通过函数clean_字段名来进行的,以此来帮助我们达到再次校验某个字段的数据。

页面效果:
在这里插入图片描述
在当前局部钩子内只能get到对应字段的值



7、全局钩子

在全局钩子内:我们可以取到任意字段的值,对这些值再做一个整体校验然后返回。

my_form.py.py

# 针对所有字段的全局钩子
def clean(self):
    password = self.cleaned_data.get('password')
    re_password = self.cleaned_data.get('re_password')

    if password == re_password: # 校验用户两次输入的密码是否一致
        return self.cleaned_data # 因为是全局钩子,一致的话需要返回全部数据,不然的某些字段会接收不到值。
    else:
        raise ValidationError('两次密码输入不一致')

我们基于下面的整合来整体演示全局钩子、局部钩子带给我们的效果



8、整合使用

我们将使用fomr组件,内部并设置有局部钩子与全局钩子对数据做多次校验,最终将通过检验的数据存入我们的数据库内。以下便是前后端全部代码

my_form.py组件

from django import forms
from django.core.exceptions import ValidationError
from django.forms import widgets


class UserForm(forms.Form):  # 校验用户信息的组件
    name = forms.CharField(max_length=8, min_length=4, label='用户名', error_messages={
        'max_length': '用户名不能超过8位',
        'min_length': '用户名不能低于4位',
        'required': '用户名不能为空'},
                           widget=widgets.TextInput(attrs={'class': 'from-control'})
                           )

    password = forms.CharField(max_length=8, min_length=4, label='密码', error_messages={
        'max_length': '密码不能超过8位',
        'min_length': '密码不能低于4位',
        'required': '密码不能为空'},
                               widget=widgets.PasswordInput(attrs={'class': 'from-control'})
                               )

    re_password = forms.CharField(max_length=8, min_length=4, label='确认密码', error_messages={
        'max_length': '密码不能超过8位',
        'min_length': '密码不能低于4位',
        'required': '确认密码不能为空'},
                                  widget=widgets.PasswordInput(attrs={'class': 'from-control'})

                                  )

    email = forms.EmailField(label='邮箱', error_messages={'required': '邮箱不能为空'})

    # 针对name字段的局部钩子
    def clean_name(self):
        name = self.cleaned_data.get('name')  # 获取name字段数据
        if name.startswith('sb'):  # name字段数据不能以sb开头
            raise ValidationError('不能以sb开头')  # 校验不通过,抛出异常

        else:  # 校验通过,返回name对应的值。
            return name

    # 针对所有字段的全局钩子
    def clean(self):
        password = self.cleaned_data.get('password')
        re_password = self.cleaned_data.get('re_password')

        if password == re_password: # 校验用户两次输入的密码是否一致
            return self.cleaned_data # 因为是全局钩子,一致的话需要返回全部数据,不然的某些字段会接收不到值。
        else:
            raise ValidationError('两次密码输入不一致')

视图

from django.shortcuts import render, HttpResponse
from app01.my_form import UserForm
from app01 import models


def myform(request):
    if request.POST:
        form = UserForm(request.POST)
        if form.is_valid(): # 判断前端输入的内容是否合格
            form.cleaned_data.pop('re_password')
            models.userinfo.objects.create(**form.cleaned_data) # cleaned_data得到本身就是字典,我们通过**将其打散成关键字=数据的形式向数据库添加值。

            return HttpResponse('账号注册成功')

       	try:
           	error = form.errors.get('__all__')[0] # 获取全局钩子的错误信息
        except Exception as e:
       		error = '' # 避免全局钩子没有返回错误信息导致报错

        return render(request,'myform.html',locals())

    form = UserForm()

    return render(request,'myform.html',locals())

前端:myform.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>myform</title>
    <script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.4.1/jquery.min.js"></script>
    <script src="https://cdn.bootcdn.net/ajax/libs/twitter-bootstrap/3.4.1/js/bootstrap.min.js"></script>
    <link href="https://cdn.bootcdn.net/ajax/libs/twitter-bootstrap/3.4.1/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
<h2>注册页面</h2>
<form action="" method="post">
    {% for item in form %}
        <p>{{ item.label }}:{{ item }} <span style="color: red">{{ item.errors.0 }}</span></p>
    {% endfor %}
    <input type="submit" value="提交"> {{ error }}
</form>
</body>
</html>

页面效果:

在这里插入图片描述
至于页面的布局排版可运用CSS或JS来实现。

那么以上就是form组件的使用方式,我们再来了解一下form组件的实现原理



form组件源码分析

我们使用form组件对字段数据进行校验时,必须要调用form组件所在的类实例化出一个对象,然后调用这个对象的is_valid()方法才能进行校验,该方法只会返回两个值:True、False

所以我们要对form源码进行一个分析的话,入门便是is_valid()方法

点进去is_valid()后发现以下代码,它告诉我们这个方法form组件没有返回错误的话,则返回True、否则返回False

def is_valid(self):
    """Return True if the form has no errors, or False otherwise."""
    return self.is_bound and not self.errors

我们再点击self.errors查看,发现了一个封装成类属性的方法,self._errors默认值是None,所以会执行self.full_clean()方法

@property
def errors(self):
    """Return an ErrorDict for the data provided for the form."""
    if self._errors is None:
        self.full_clean()
    return self._errors

关于这个方法,我们只需要关注以下两个方法,它们才是我们form组件的重点

def full_clean(self):
    self._clean_fields() # 局部钩子实现
    self._clean_form() # 全局钩子实现

1、局部钩子源码

self._clean_fields()就是局部钩子的实现方法

def _clean_fields(self):
	'''
		这个self就是我们在视图里面基于form组件类实例化出来的对象
		self.fields.items()获取我们form组件类下面的类型属性和值,其取值方式等同于字典。
		name就是属性名、field就是:CharField等内容对象
	'''
    for name, field in self.fields.items():
        if field.disabled:
			# 获取表单数据,value等于我们输入框提交的值
            value = self.get_initial_for_field(field, name)
        else:
            value = field.widget.value_from_datadict(self.data, self.files, self.add_prefix(name))
        try:
        	# 判断对象是否为FileField类型的,很显然我们目前并没有定义过这种的,所以跳过
            if isinstance(field, FileField):
                initial = self.get_initial_for_field(field, name)
                value = field.clean(value, initial)
            else:
                value = field.clean(value) # 调用这个对象自身的一些校验功能(我们自己定义的)
            # 通过对象自身的校验,将数据保存在cleaned_data字典内,name就是属性名,value就是我们输入框提交的值。
            self.cleaned_data[name] = value 

			# 通过反射判断我们写的form组件类里面有没有局部钩子
            if hasattr(self, 'clean_%s' % name):
	
				# 有的话调用这个局部钩子,那到这个局部钩子返回的值(现在知道为什么要返回局部钩子校验字段的值了吧)
                value = getattr(self, 'clean_%s' % name)() 
                self.cleaned_data[name] = value # 再次将这个值保存到字典内,这里name字段的数据等同于二次校验了
        
        # try以内的代码抛出了ValidationError类型的异常(我们手动定义的异常)
        except ValidationError as e:
            self.add_error(name, e) # 根据字段名将异常信息添加到一个字典内,但这个字典的值都是一个个列表,而错误信息保存在列表内。说明有时候错误可能不止一个

2、全局钩子源码

self._clean_form()就是全局钩子的实现方法。

全局钩子的实现相对于局部钩子较为简单、因为它是在自身对象校验、局部钩子校验之后触发的。

def _clean_form(self):
    try:
    	# 调用我们form组件类里面clean方法(这就是为什么我们要返回self.cleaned_data,因为全部校验后的数据都是保存在里面)
        cleaned_data = self.clean()
    except ValidationError as e:
        self.add_error(None, e)
    else:
        if cleaned_data is not None: # 如果拿到的不是一个None的话,说明我们返回了值
            self.cleaned_data = cleaned_data
            # self.cleaned_data = self.clean()方法返回的值(也就是clean方法必须要返回self.cleaned_data)

如果我们的form组件类里面没有写clean方法,则会调用form源码里面的clean方法

def clean(self):
    """
    Hook for doing any extra form-wide cleaning after Field.clean() has been
    called on every field. Any ValidationError raised by this method will
    not be associated with a particular field; it will have a special-case
    association with the field named '__all__'.
    """
    return self.cleaned_data

而源码里面的clean方法什么也没有做,只是返回了数据,所以我们需要使用全局钩子时,重写该方法即可,因为全局钩子是在字段自身校验、局部钩子校验完之后执行的,所以它可以拿到所有字段数据。


如果本文对您有帮助,别忘一键3连,您的支持就是笔者最大的鼓励,感谢阅读!

下一章传送门:Django内的Cookie、Session机制


技术小白记录学习过程,有错误或不解的地方请指出,如果这篇文章对你有所帮助请点赞 收藏+关注 子夜期待您的关注,谢谢支持!



这篇关于一起学习Django框架(八)Ajax请求;分页器;form组件校验字段;form组件源码分析的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!


扫一扫关注最新编程教程