【二】注册功能实现

【一】注册功能

【路由接口】

  • register/

【引言】

  • 之前是在views.py中书写 forms组件代码

  • 但是为了解耦合,应该单独书写

  • 如果项目从始至终只需要一个 forms 组件,那么你可以直接建一个py文件即可

  • 如果项目需要多个 forms 组件,则可以创建一个文件夹,将不同的功能封装到不同的文件中

myforms文件夹
	regform.py
	loginform.py
	userform.py
	orderform.py
	...

【二】需求

  • 需要展示的效果
    • 用户输入用户名
    • 用户输入密码
    • 用户输入二次确认密码
    • 用户输入邮箱
    • 用户选择头像文件上传
  • 要求
    • 用户名、密码、二次确认密码、邮箱在格式不对时,实时展示提示信息,及必要报错
    • 头像有默认展示图片,并且用户在选择头像文件上传后,可以实时更新用户选择的头像
    • 用户点击输入框时,输入框提示信息消失
  • 使用到的技术
    • bootstrap展示样式
    • ajax加载点击动作
    • ajax发送post请求
    • forms组件实时监控用户输入的信息格式
    • input框焦点

【三】功能实现 - 后端 forms 组件

# -*-coding: Utf-8 -*-
# @File : myforms .py
# author: Chimengmeng
# blog_url : https://www.cnblogs.com/dream-ze/
# Time:2023/7/18

# 书写针对用户表的forms 组件代码
from django import forms
from book import models


class MyRegForm(forms.Form):
    username = forms.CharField(label="用户名",
                               min_length=3,
                               max_length=8,
                               error_messages={
                                   "required": "用户名不能为空",
                                   'min_length': "用户名最小3位",
                                   "max_length": "用户名最大8位"
                               },
                               # 还需要让标签有bootstrap样式
                               widget=forms.TextInput(attrs={"class": "form-control"})
                               )
    password = forms.CharField(label="密码",
                               min_length=3,
                               max_length=8,
                               error_messages={
                                   "required": "密码不能为空",
                                   'min_length': "密码最小3位",
                                   "max_length": "密码最大8位"
                               },
                               # 还需要让标签有bootstrap样式
                               widget=forms.PasswordInput(attrs={"class": "form-control"})
                               )
    confirm_password = forms.CharField(label="确认密码",
                                       min_length=3,
                                       max_length=8,
                                       error_messages={
                                           "required": "确认密码不能为空",
                                           'min_length': "确认密码最小3位",
                                           "max_length": "确认密码最大8位"
                                       },
                                       # 还需要让标签有bootstrap样式
                                       widget=forms.PasswordInput(attrs={"class": "form-control"})
                                       )
    email = forms.EmailField(label="邮箱",
                             error_messages={
                                 "required": "邮箱不能为空",
                                 'invalid': "邮箱格式不正确",
                             },
                             # 还需要让标签有bootstrap样式
                             widget=forms.EmailInput(attrs={"class": "form-control"})
                             )

    # 钩子函数 - 局部钩子 - 校验用户名是否已存在
    def clean_username(self):
        # 勾出用户名
        username = self.cleaned_data.get("username")
        # 去数据库中校验
        is_exist = models.UserInfo.objects.filter(username=username)

        if is_exist:
            # 用户存在,添加提示信息
            self.add_error("username", "用户名已存在")

        # 把钩子放回去
        return username

    # 钩子函数  - 全局钩子 - 校验两次密码是否一致
    def clean(self):
        password = self.cleaned_data.get('password')
        confirm_password = self.cleaned_data.get('confirm_password')

        if not password == confirm_password:
            # 两次密码不一致 添加报错信息
            self.add_error("confirm_password", "两次密码不一致")

        # 把钩子放回去
        return self.cleaned_data

【四】功能实现 - 前端输入数据及动态加载头像

{% load static %}
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>

       <!--  CDN 链接 引入方法  -->
    <!--  Bootstrap 的 CSS 样式文件  -->
    <link rel="stylesheet" href="https://cdn.bootcdn.net/ajax/libs/twitter-bootstrap/3.4.1/css/bootstrap.min.css">
    <!--  Bootstrap 的 JS 文件 (动画效果需要jQuery)  -->
    <script src="https://cdn.bootcdn.net/ajax/libs/twitter-bootstrap/3.4.1/js/bootstrap.min.js">// bootstrap </script>
    <!--  jQuery 文件  -->
    <script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.6.4/jquery.min.js"> // jquery</script>
    <!--  以下为 css样式书写区  -->
    <style>


    </style>

</head>
<body>

<div class="container-fluid">
    <div class="row">
        <div class="col-md-8 col-md-offset-2">
            <h1 class="text-center">注册功能</h1>
            {#   不用form表单提交数据     #}
            <form id="myform">
                {% csrf_token %}
                {% for form in form_obj %}
                    <div class="form-group">
                        <label for="{{ form.auto_id }}">{{ form.label }}</label>
                        {{ form }}
                        <span style="color: red" class="pull-right">{{ form.errors.0 }}</span>
                    </div>
                {% endfor %}
                <div class="form-group text-center">
                    <label for="myfile">
                        选择头像<img src="{% static 'avatar/default.png' %}" id="myimg" alt=""
                                     style="height: 200px;width: 200px;margin-left: 20px">
                    </label>
                    <input type="file" id="myfile" name="avatar" style="display: none">
                </div>

                <input type="button" class="btn btn-primary pull-right" value="注册" id="btn_commit">

            </form>

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

<script>
    {#  文本域变化事件  #}
    $("#myfile").change(function () {
        // 文件阅读器对象
        // (1)先生成一个文本阅读器对象
        let myFileReaderObj = new FileReader();
        // (2)获取用户上传的头像文件
        let fileObj = $(this)[0].files[0];
        // (3)将文件对象交给阅读器对象读取
        myFileReaderObj.readAsDataURL(fileObj) // 异步操作 + IO操作
        // (4)利用文件阅读器将文件展示到前端页面  修改src属性
        // 等待 文件阅读器加载完毕之后再执行
        myFileReaderObj.onload = function () {
            $("#myimg").attr('src', myFileReaderObj.result) // 直接写有bug,需要放在 onload 里面
        }
    })

    // 给按钮绑定点击事件,发送ajax请求提交数据
    $('#btn_commit').click(function () {
        // 发送Ajax请求  -- 发送的数据既包括普通键值对又包括文件
        let formDataObj = new FormData();
        // (1) 添加普通的键值对
        // $('#myform').serializeArray() // 返回的数据格式:[{}, {}, {}, {}] --- 只包含普通的键值对
        $.each($('#myform').serializeArray(), function (index, obj) {
            //index:索引  obj:自定义对象
            formDataObj.append(obj.name, obj.value);
        });
        // (2) 添加文件数据
        formDataObj.append('avatar', $('#myfile')[0].files[0])
        // 发送Ajax请求
        $.ajax({
            url: "",
            type: "post",
            data: formDataObj,

            // 两个关键参数
            contentType: false,
            processData: false,

            success: function (args) {
                if (args.code === 1000) {
                    // 跳转到登录页面
                    window.location.href = args.url;
                } else {
                    // 如何将对应的错误提示展示到对应的input框下面
                    // forms 组件渲染的标签的 id值 都是 id_字段名
                    $.each(args.message, function (index, obj) {
                        // index : confirm_password  obj : ['确认密码不能为空']
                        let targetId = '#id_' + index
                        $(targetId).next().text(obj[0]).parent().addClass("has-error")
                    })
                }
            }
        })
    });
    // 给所有的input框绑定焦点事件
    $('input').focus(function () {
        // 将input下面的 span 标签 和 input 外面的div标签修改内容及属性
        $(this).next().text('').parent().removeClass('has-error')
    })

</script>

</body>
</html>

【1】前端页面搭建

  • 首先是页面搭建

    • 页面需要具有的功能
      • 输入用户名
      • 输入密码
      • 输入二次确认密码
      • 输入邮箱
      • 选择头像
  • 整体使用 form 标签包裹,但是不是用form标签提交post请求

    <div class="container-fluid">
        <div class="row">
            <div class="col-md-8 col-md-offset-2">
                <h1 class="text-center">注册功能</h1>
                {#   不用form表单提交数据     #}
                <form id="myform">
                    
                </form>
    
            </div>
        </div>
    </div>
    
  • 不进行 csrf 跨域校验

    {% csrf_token %}
    
  • 展示后端forms组件校验返回的结果

    • 循环从后端传过来的 forms组件生成的form对象中拿到我们的自定义标签名/自动生成的对应标签的id值
      • 自定义标签名
        • 用户名
      • 自动生成的对应标签的id值
        • id_username
    {% for form in form_obj %}
                        <div class="form-group">
                            <label for="{{ form.auto_id }}">{{ form.label }}</label>
                            {{ form }}
                            <span style="color: red" class="pull-right">{{ form.errors.0 }}</span>
                        </div>
                    {% endfor %}
    
  • 展示头像

    • 默认显示默认图片
    • 自定义样式
    • 定义一个 input 框用来给让用户选择头像文件
    <div class="form-group text-center">
                        <label for="myfile">
                            选择头像<img src="{% static 'avatar/default.png' %}" id="myimg" alt=""
                                         style="height: 200px;width: 200px;margin-left: 20px">
                        </label>
                        <input type="file" id="myfile" name="avatar" style="display: none">
                    </div>
    
  • 提交按钮

    <input type="button" class="btn btn-primary pull-right" value="注册" id="btn_commit">
    

【2】前端页面事件绑定

(1)动态更新头像代码分析

<script>
    {#  文本域变化事件  #}
    $("#myfile").change(function () {
        // 文件阅读器对象
        // (1)先生成一个文本阅读器对象
        let myFileReaderObj = new FileReader();
        // (2)获取用户上传的头像文件
        let fileObj = $(this)[0].files[0];
        // (3)将文件对象交给阅读器对象读取
        myFileReaderObj.readAsDataURL(fileObj) // 异步操作 + IO操作
        // (4)利用文件阅读器将文件展示到前端页面  修改src属性
        // 等待 文件阅读器加载完毕之后再执行
        myFileReaderObj.onload = function () {
            $("#myimg").attr('src', myFileReaderObj.result) // 直接写有bug,需要放在 onload 里面
        }

    })
</script>
  • 在Ajax请求过程中,可以监听文本域的多种事件,例如:

    • onkeydown:当用户按下键盘上的任意键时触发。

    • onkeypress:当用户按下键盘上的字符键时触发。

    • onkeyup:当用户释放键盘上的键时触发。

    • onchange:当文本域的值发生改变时触发。

    • oninput:当用户输入或粘贴内容到文本域时触发(包括通过键盘输入、鼠标操作或者通过其他方式)。

    • onselect:当用户选择文本域中的一部分或全部内容时触发。

  • 通过监听这些事件

    • 可以实现对文本域内容的实时监控、自动保存或自动完成等功能。
  • 一般情况下

    • 可以在文本域的HTML标签中添加对应的事件处理函数
    • 也可以使用JavaScript动态绑定事件处理函数。
  • 在上面的逻辑中引入了 ajax的文本域事件中的change方法

    • 即动态监控某一块区域的变化同时给出相应的响应
  • 首先通过定义的id值找到需要监控变化发生的标签

    • $("#myfile")
  • 然后加上文本域变化监控方法

    • $("#myfile").change(function () {}
  • 在方法内书写当变化发生时,产生的逻辑方法

    • 先生成文本阅读器对象 - 掌事者/

      • let myFileReaderObj = new FileReader();
    • 然后拿到用户上传的头像文件

      • let fileObj = $(this)[0].files[0];

        • $(this):表示当前上下文中的jQuery对象。
        • [0]:获取jQuery对象中的第一个元素。
        • .files[0]:获取文件选择器中选中的文件对象数组中的第一个文件对象。
        • 综合起来
          • $(this)[0].files[0] 的意思是从当前上下文中的文件选择器中获取选中的第一个文件对象。
          • 这个文件对象可以是用户选择的文件
          • 例如通过 <input type="file"> 元素实现的文件选择器。
        • 获取到的文件对象可以用于进行进一步的处理
          • 例如读取文件的内容、上传文件到服务器等操作。
    • 将这个文件对象交给我们的掌事者即文本阅读器对象

      • myFileReaderObj.readAsDataURL(fileObj)

        • myFileReaderObj 是一个 FileReader 对象
          • 用于读取文件的内容。
          • 通过调用该对象的 readAsDataURL() 方法来实现读取文件。
        • readAsDataURL(fileObj) 是一个异步操作
          • 它会以异步的方式读取 fileObj 所代表的文件的内容
          • 并将其转换为 Data URL。Data URL 是一种将文件内容直接嵌入到 URL 中的方式
          • 可以用于在浏览器中展示或传输文件的内容。
        • 这行代码中的 fileObj 是之前获取的文件对象
          • 会作为参数传递给 readAsDataURL() 方法
          • 告诉它要读取哪个文件的内容。
        • 需要注意的是
          • readAsDataURL() 是一个涉及 IO 操作的异步操作
          • 所以需要在其完成后才能获取到读取到的结果。
          • 通常会监听 FileReader 对象的 onload 事件
            • 当该事件触发时,表示文件的内容已经读取完成
            • 可以通过 FileReader 对象的 result 属性获取读取到的 Data URL 数据。
    • 因为上述操作是一个异步IO操作,所以我们需要监听FileReader 对象的 onload 事件,通过触发该事件,达到实时替换头像的效果

      • myFileReaderObj.onload
    • 定义触发该事件后,进行的功能

      • $("#myimg").attr('src', myFileReaderObj.result)

        • 这行代码是将读取到的 Data URL 数据设置为指定元素(ID 为 "myimg")的 src 属性值。

        • 具体解释如下:

          • $("#myimg") 是一个 jQuery 选择器
            • 用于选择具有特定 ID 的元素。
            • 在这里,通过选择器 #myimg 来选取 ID 为 "myimg" 的元素。
          • attr('src', myFileReaderObj.result) 是调用了 attr() 方法来设置选中元素的 src 属性值。
            • 其中第一个参数是属性名 'src'
            • 第二个参数是要设置的属性值 myFileReaderObj.result
            • 在这里,myFileReaderObj.result 是之前通过 FileReader 对象读取得到的 Data URL 数据。
          • 所以
            • 这行代码的作用是将 Data URL 数据设置为 ID 为 "myimg" 的元素的 src 属性值。
            • 这样做之后,该元素就会显示 Data URL 所代表的图片或者其他支持的媒体类型。
  • 通过以上方法达到实时展示用户更换头像的效果

(2)给按钮绑定点击事件,发送Ajax请求

// 给按钮绑定点击事件,发送ajax请求提交数据
    $('#btn_commit').click(function () {
        // 发送Ajax请求  -- 发送的数据既包括普通键值对又包括文件
        let formDataObj = new FormData();
        // (1) 添加普通的键值对
        // $('#myform').serializeArray() // 返回的数据格式:[{}, {}, {}, {}] --- 只包含普通的键值对
        $.each($('#myform').serializeArray(), function (index, obj) {
            //index:索引  obj:自定义对象
            formDataObj.append(obj.name, obj.value);
        });
        // (2) 添加文件数据
        formDataObj.append('avatar', $('#myfile')[0].files[0])
        // 发送Ajax请求
        $.ajax({
            url: "",
            type: "post",
            data: formDataObj,

            // 两个关键参数
            contentType: false,
            processData: false,

            success: function (args) {
                if (args.code === 1000) {
                    // 跳转到登录页面
                    window.location.href = args.url;
                } else {
                    // 如何将对应的错误提示展示到对应的input框下面
                    // forms 组件渲染的标签的 id值 都是 id_字段名
                    $.each(args.message, function (index, obj) {
                        // index : confirm_password  obj : ['确认密码不能为空']
                        let targetId = '#id_' + index
                        $(targetId).next().text(obj[0]).parent().addClass("has-error")
                    })
                }
            }
        })
    });
  • 首先获取提交按钮的标签

    • $('#btn_commit').click(function () {}
  • 给提交按钮绑定事件,我们要发送的数据中既包括普通键值对,又包括头像的文件数据

  • 普通键值对数据

    • 先获取到 FormData 数据对象

      let formDataObj = new FormData();

    • 向数据对象中添加数据

      • 通过下面的方法我们可以得到每一个数据对象(只包含普通的键值对)
      • $('#myform').serializeArray()
      • 返回的数据格式[{}, {}, {}, {}]
    • 通过循环,获取每一个键值对中的键和值并添加到 FormData 数据对象

      $.each($('#myform').serializeArray(), function (index, obj) {
                  //index:索引  obj:自定义对象
                  formDataObj.append(obj.name, obj.value);
              });
      
  • 文件数据添加

    • 首先要找到数据文件
      • $('#myfile')[0].files[0]
    • 将文件数据添加到 FormData 数据对象中
      • 前面是键,后边是值
      • formDataObj.append('avatar', $('#myfile')[0].files[0])
  • 定义Ajax请求

    • $.ajax({})

    • url就是当前默认的url

      • url: ""
    • 要发送 post 请求

      • type: "post"
    • 传入的数据是 FormData 数据对象

      • data: formDataObj
    • 将两个默认参数注掉

      • contentType: false
      • processData: false
    • 定义成功后接收到的数据

      • success: function (args) {}
        
      • 登陆成功,跳转到登陆功能

        • if (args.code === 1000) {}
        • window.location.href = args.url
      • 登陆失败,渲染错误信息到输入框

        • else{}
        • $.each(args.message, function (index, obj) {})
        • 取到上面的form组件生成的自动标签
          • let targetId = '#id_' + index
        • 给标签绑定事件
          • 拿到输入框标签
            • 输入框标签下边是 span 标签,渲染错误信息边框颜色(error红色)
            • 输入框标签上边是 总的输入框,渲染错误信息
          • $(targetId).next().text(obj[0]).parent().addClass("has-error")

(3)给所有input框添加焦点事件

// 给所有的input框绑定焦点事件
    $('input').focus(function () {
        // 将input下面的 span 标签 和 input 外面的div标签修改内容及属性
        $(this).next().text('').parent().removeClass('has-error')
    })
  • 当错误信息被展示以后,点击input框清除错误信息

  • 拿到所有的标签

    • $('input').focus(function () {})
  • 将标签的下一个错误信息清空

  • 将标签的上一个错误颜色提示属性删除

【五】功能实现 - 后端校验数据返回错误信息

from django.shortcuts import render, HttpResponse, redirect

from book import models
from book.utils.myforms import MyRegForm
from django.http import JsonResponse

# 注册接口
# Create your views here.
def register(request):
    # 生成 form 对象
    form_obj = MyRegForm()
    if request.method == 'POST':
        back_dict = {"code": 1000, "message": ""}
        # 校验数据是否合法
        form_obj = MyRegForm(request.POST)
        # 判读数据是否合法
        if form_obj.is_valid():
            # 校验通过只要 MyRegForm 中定义的四个字段
            clean_data = form_obj.cleaned_data  # 将校验通过的数据字典赋值给一个变量
            # 将字典中的 confirm_password 键值对删除
            clean_data.pop("confirm_password")
            # 用户头像获取
            file_obj = request.FILES.get('avatar')
            '''
            针对用户头像要判断是否传值 不能直接添加到字典
            我们在数据库中已经定义了默认的头像链接,如果传进来的值是空的,会直接覆写我们原来的数据
            '''
            if file_obj:
                clean_data['avatar'] = file_obj
            # 操作数据库存储数据  **clean_data : 打散键值对再传进去
            models.UserInfo.objects.create_user(**clean_data)

            # 写入成功,跳转到登录界面
            back_dict['url'] = "/login/"
        else:
            back_dict['code'] = 2000
            back_dict['message'] = form_obj.errors
        return JsonResponse(back_dict)
    return render(request, 'register.html', locals())
  • 主要逻辑分析

    • (1)先生成一个 空的 form 对象

      • 为了校验提交过来的数据
    • (2)校验当前请求方式

      • 防止普通get请求乱提交数据
      • 我们只需要post请求的数据
      • 正常访问页面也属于get请求
    • (3)定义返回参数字典

      • 定义返回状态码
      • 定义返回信息
      • 因为是字典,可以主动添加信息
    • (4)将传过来的post请求数据传入空的 form 对象中校验

      • 因为我们在自定义 forms 组件中已经定义了相关的校验方法
      • 多传值进去只会接受我们定义的值
      • 少传值会有错误信息提示
      • 错传值也会有错误信息提示
    • (5)判断校验完的数据是否合法

      • 如果数据全部合法则进行数据的写入工作
    • (6)将校验成功的数据全部取出赋值给变量

      • 用变量接收成功的数据便于操作
    • (7)将数据中的二次确认密码删除

      • 因为我们要通过create_user方法创建用户数据

        def create_user(self, username, email=None, password=None, **extra_fields):
            extra_fields.setdefault('is_staff', False)
            extra_fields.setdefault('is_superuser', False)
            return self._create_user(username, email, password, **extra_fields)
        
      • 我们只需要用户名/密码/邮箱即可

    • (8)获取图片头像文件

      • 通过*request*.FILES.get(key)方法获取到文件对象

      • 注意判断是否有值,如果是空值会默认覆盖我们原来的默认头像

    • (9)操作数据库进行数据的写入

      • 将处理好的数据打散交给create_user方法创建用户数据
    • (10)写入成功

      • 定义下一步跳转的url
        • 登陆界面
    • (11)注册未成功

      • 修改状态码
      • 将校验的错误信息写入
    • (12)返回JSON格式数据

  • 返回请求、注册页面、当前功能的名称空间

posted @ 2023-07-21 15:42  Chimengmeng  阅读(21)  评论(0编辑  收藏  举报
/* */