Skip to content

flask + layui 实现注册功能

视频地址: https://www.bilibili.com/video/BV1ZN411k7nv/

已经看了 layui 表单相关的知识,接下来就可以实现注册功能,功能逻辑如下:
用户
用户
浏览器
浏览器
后端服务器
后端服务器
访问注册页面
访问注册页面
输入手机号 ,请求注册
输入手机号 ,请求注册
校验手机号格式是否正常
校验手机号格式是否正常
格式不正常提示错误
格式不正常提示错误
格式正常请求后端
格式正常请求后端
返回结果 ,成功或失败
返回结果 ,成功或失败
MySql
MySql
校验手机号格式
请求第三方服务发送短信
校验手机号格式 请求第三方服务发送短信
校验返回结果、返回信息
校验返回结果、返回信息
返回登录页
返回登录页
失败返回错误信息,重新注册
失败返回错误信息,重新注册
成功之后将用户数据记录到数据库
成功之后将用户数据记录到数据库
返回注册页面
返回注册页面
返回注册页面
返回注册页面
校验返回结果
请求错误返回错误信息
请求成功之后重定向到登录页
校验返回结果请求错误返回错误信息请求成功之后重定向到登录页...
返回信息
返回信息
输入短信验证码,请求注册
输入短信验证码,请求注册
校验信息通过之后请求注册
校验信息通过之后请求注册
校验信息
校验信息
将手机号、验证码记录到数据库
将手机号、验证码记录到数据库
返回结果 ,成功或失败
返回结果 ,成功或失败
Text is not SVG - cannot display

项目创建

  1. 新建 flask 项目
  2. 下载 layui 文件,解压之后复制到指定文件
  3. 编写前端首页、注册页、登录页。注册页使用之前看过的注册页表单
  4. 编写后端路由,实现前后端基本的跳转

新建 flask 项目

新建项目的方式有很多种,只要有任意一种方式创建即刻。在这里我直接选择使用 pycharm 进行创建。

新建成功后目录如下

txt
D:\flask-register>tree /f
D:.
│  app.py
├─static
└─templates

下载 layui 文件

访问 https://layui.dev/ ,点击直接下载之后就能得到 layui 的静态文件,将其复制到 static 目录下,完成之后的目录结构如下

txt
D:\flask-register>tree /f
卷 软件 的文件夹 PATH 列表
卷序列号为 65F3-3762
D:.
│  app.py

├─static
│  │  layui.js
│  │
│  ├─css
│  │      layui.css
│  │
│  └─font
│          iconfont.eot
│          iconfont.svg
│          iconfont.ttf
│          iconfont.woff
│          iconfont.woff2

└─templates

编写静态页面

html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>首页</title>
    <link rel="stylesheet" href="/static/css/layui.css">
</head>
<body>
<div class="layui-layout layui-layout-admin">
    <div class="layui-header">
        <div class="layui-logo layui-hide-xs layui-bg-black">正心全栈编程</div>
        <ul class="layui-nav layui-layout-left">
            <li class="layui-nav-item"><a href="">最新活动</a></li>
            <li class="layui-nav-item layui-this"><a href="">核心编程</a></li>
            <li class="layui-nav-item"><a href="">爬虫开发</a></li>
            <li class="layui-nav-item"><a href="">数据分析</a></li>

            <li class="layui-nav-item">
                <a href="JavaScript:;">网站开发</a>
                <dl class="layui-nav-child">
                    <dd><a href="">前端基础</a></dd>
                    <dd><a href="">flask 框架</a></dd>
                    <dd><a href="">flask 项目</a></dd>
                </dl>
            </li>
        </ul>

        <ul class="layui-nav layui-layout-right">
            <li class="layui-nav-item layui-hide layui-show-sm-inline-block">
                <a href="javascript:;">
                    <img src="//unpkg.com/outeres@0.0.10/img/layui/icon-v2.png" class="layui-nav-img">
                    登录 / 正心
                </a>
                <dl class="layui-nav-child">
                    <dd><a href="javascript:;">个人中心</a></dd>
                    <dd><a href="javascript:;">设置</a></dd>
                    <dd><a href="javascript:;">退出</a></dd>
                </dl>
            </li>
        </ul>
    </div>
</div>
<script src="/static/layui.js"></script>
</body>
</html>
html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>注册页</title>
    <link rel="stylesheet" href="/static/css/layui.css">
</head>
<body>
<style>
    .demo-reg-container {
        width: 320px;
        margin: 21px auto 0;
    }

    .demo-reg-other .layui-icon {
        position: relative;
        display: inline-block;
        margin: 0 2px;
        top: 2px;
        font-size: 26px;
    }
</style>
<form class="layui-form" lay-filter="register-form">
    <div class="demo-reg-container">
        <div class="layui-form-item">
            <div class="layui-input-wrap">
                <div class="layui-input-prefix">
                    <i class="layui-icon layui-icon-username"></i>
                </div>
                <input type="text" name="nickname" value="" lay-verify="required" placeholder="昵称" autocomplete="off"
                       class="layui-input" lay-affix="clear">
            </div>
        </div>
        <div class="layui-form-item">
            <div class="layui-row">
                <div class="layui-col-xs7">
                    <div class="layui-input-wrap">
                        <div class="layui-input-prefix">
                            <i class="layui-icon layui-icon-cellphone"></i>
                        </div>
                        <input type="text" name="mobile" value="" lay-verify="required|phone" placeholder="手机号"
                               lay-reqtext="请填写手机号" autocomplete="off" class="layui-input" id="reg-cellphone">
                    </div>
                </div>
                <div class="layui-col-xs5">
                    <div style="margin-left: 11px;">
                        <button type="button" class="layui-btn layui-btn-fluid layui-btn-primary"
                                lay-on="reg-get-vercode">获取验证码
                        </button>
                    </div>
                </div>
            </div>
        </div>
        <div class="layui-form-item">
            <div class="layui-input-wrap">
                <div class="layui-input-prefix">
                    <i class="layui-icon layui-icon-vercode"></i>
                </div>
                <input type="text" name="vercode" value="" lay-verify="required" placeholder="验证码"
                       lay-reqtext="请填写验证码" autocomplete="off" class="layui-input">
            </div>
        </div>
        <div class="layui-form-item">
            <div class="layui-input-wrap">
                <div class="layui-input-prefix">
                    <i class="layui-icon layui-icon-password"></i>
                </div>
                <input type="password" name="password" value="" lay-verify="required" placeholder="密码"
                       autocomplete="off" class="layui-input" id="reg-password" lay-affix="eye">
            </div>
        </div>
        <div class="layui-form-item">
            <div class="layui-input-wrap">
                <div class="layui-input-prefix">
                    <i class="layui-icon layui-icon-password"></i>
                </div>
                <input type="password" name="confirmPassword" value="" lay-verify="required|confirmPassword"
                       placeholder="确认密码" autocomplete="off" class="layui-input" lay-affix="eye">
            </div>
        </div>

        <div class="layui-form-item">
            <input type="checkbox" name="agreement" lay-verify="required" lay-skin="primary" title="同意">
            <a href="#terms" target="_blank" style="position: relative; top: 6px; left: -15px;">
                <ins>用户协议</ins>
            </a>
        </div>
        <div class="layui-form-item">
            <button class="layui-btn layui-btn-fluid" lay-submit lay-filter="demo-reg">注册</button>
        </div>
        <div class="layui-form-item demo-reg-other">
            <label>社交账号注册</label>
            <span style="padding: 0 21px 0 6px;">
        <a href="javascript:;"><i class="layui-icon layui-icon-login-qq" style="color: #3492ed;"></i></a>
        <a href="javascript:;"><i class="layui-icon layui-icon-login-wechat" style="color: #4daf29;"></i></a>
        <a href="javascript:;"><i class="layui-icon layui-icon-login-weibo" style="color: #cf1900;"></i></a>
      </span>
            <a href="/login">登录已有帐号</a>
        </div>
    </div>
</form>
<script src="/static/layui.js"></script>
<script>
    layui.use(function () {
        var $ = layui.$;
        var form = layui.form;
        var layer = layui.layer;
    });
</script>
</body>
</html>
html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>登录页</title>
    <link rel="stylesheet" href="/static/css/layui.css">
</head>
<body>
<style>
    .demo-login-container {
        width: 320px;
        margin: 21px auto 0;
    }

    .demo-login-other .layui-icon {
        position: relative;
        display: inline-block;
        margin: 0 2px;
        top: 2px;
        font-size: 26px;
    }
</style>
<form class="layui-form">
    <div class="demo-login-container">
        <div class="layui-form-item">
            <div class="layui-input-wrap">
                <div class="layui-input-prefix">
                    <i class="layui-icon layui-icon-cellphone"></i>
                </div>
                <input type="text" name="mobile" value="" lay-verify="required" placeholder="手机号"
                       lay-reqtext="请填写手机号" autocomplete="off" class="layui-input" lay-affix="clear">
            </div>
        </div>
        <div class="layui-form-item">
            <div class="layui-input-wrap">
                <div class="layui-input-prefix">
                    <i class="layui-icon layui-icon-password"></i>
                </div>
                <input type="password" name="password" value="" lay-verify="required" placeholder="密   码"
                       lay-reqtext="请填写密码" autocomplete="off" class="layui-input" lay-affix="eye">
            </div>
        </div>
        <div class="layui-form-item">
            <div class="layui-row">
                <div class="layui-col-xs7">
                    <div class="layui-input-wrap">
                        <div class="layui-input-prefix">
                            <i class="layui-icon layui-icon-vercode"></i>
                        </div>
                        <input type="text" name="captcha" value="" lay-verify="required" placeholder="验证码"
                               lay-reqtext="请填写验证码" autocomplete="off" class="layui-input" lay-affix="clear">
                    </div>
                </div>
                <div class="layui-col-xs5">
                    <div style="margin-left: 10px;">
                        <img src="https://www.oschina.net/action/user/captcha"
                             id="captchaImage">
                    </div>
                </div>
            </div>
        </div>
        <div class="layui-form-item">
            <input type="checkbox" name="remember" lay-skin="primary" title="记住密码">
            <a href="#forget" style="float: right; margin-top: 7px;">忘记密码?</a>
        </div>
        <div class="layui-form-item">
            <button class="layui-btn layui-btn-fluid" lay-submit lay-filter="demo-login">登录</button>
        </div>
        <div class="layui-form-item demo-login-other">
            <label>社交账号登录</label>
            <span style="padding: 0 21px 0 6px;">
                <a href="javascript:;"><i class="layui-icon layui-icon-login-qq" style="color: #3492ed;"></i></a>
                <a href="javascript:;"><i class="layui-icon layui-icon-login-wechat" style="color: #4daf29;"></i></a>
                <a href="javascript:;"><i class="layui-icon layui-icon-login-weibo" style="color: #cf1900;"></i></a>
              </span>
            或 <a href="/register">注册帐号</a>
        </div>
    </div>
</form>
<script src="/static/layui.js"></script>
<script>
    layui.use(function () {
        var form = layui.form;
        var layer = layui.layer;
        // 提交事件
        form.on('submit(demo-login)', function (data) {
            var field = data.field; // 获取表单字段值
            // 显示填写结果,仅作演示用
            layer.alert(JSON.stringify(field), {
                title: '当前填写的字段值'
            });
            // 此处可执行 Ajax 等操作
            // …
            return false; // 阻止默认 form 跳转
        });
    });
</script>
</body>
</html>

后端适配

前端编写好了之后,需要适配后端路由才能实现访问。

python
from flask import Flask, render_template

app = Flask(__name__)


@app.route('/')
def index_view():
    return render_template('index.html')


@app.route('/register')
def register_view():
    return render_template('register.html')


@app.route('/login')
def login_view():
    return render_template('login.html')


if __name__ == '__main__':
    app.run(debug=True)

后端路由编写好了之后,如果想要实现联调,就需要修改前端页面的逻辑。

  1. 没有登录之前不显示个人信息,只显示左上角的登录按钮,点击登录按钮调整到登录页面。
  2. 登录是没有账号,就先进行注册。注册页面如果有账号就跳转到登录页面。
  3. 注册账号成功之后跳转到登录页面

实现注册逻辑

注册逻辑观看最开始的时序图。

在实现逻辑之前,先完善一下前端跳转页面

1、没有登录之前不显示个人信息,只显示左上角的登录按钮,修改 index.html 的内容。

html

<ul class="layui-nav layui-layout-right">
    <li class="layui-nav-item layui-hide layui-show-sm-inline-block">
        {% if not is_login %}
        <a href="/login">
            登录
        </a>
        {% else %}
        <a href="javascript:;">
            <img src="//unpkg.com/outeres@0.0.10/img/layui/icon-v2.png" class="layui-nav-img">
            登录 / 正心
        </a>
        <dl class="layui-nav-child">
            <dd><a href="javascript:;">个人中心</a></dd>
            <dd><a href="javascript:;">设置</a></dd>
            <dd><a href="javascript:;">退出</a></dd>
        </dl>
        {% endif %}
    </li>
</ul>

2、登录是没有账号,就先进行注册。注册页面如果有账号就跳转到登录页面。

修改 login.html

html
<a href="/register">注册帐号</a>

修改 register.html

html
<a href="/login">登录已有帐号</a>

前端校验手机号

用户输入昵称、手机号码,点击获取验证码之前前端需要校验格式是否符合。

发送短信接口请看附录

1、输入手机号之后进行校验

html

<script>
    layui.use(function () {
        var $ = layui.$;
        var form = layui.form;
        var layer = layui.layer;
        var util = layui.util;

        // 普通事件
        util.on('lay-on', {
            // 获取验证码
            'reg-get-vercode': function (othis) {
                var isvalid = form.validate('#reg-cellphone'); // 主动触发验证,v2.7.0 新增
                // 验证通过
                if (isvalid) {
                    // {#layer.msg('手机号规则验证通过');#}
                    // 此处可继续书写「发送验证码」等后续逻辑
                    let data = form.val('register-form');
                    // 发送请求
                    fetch('/api/send_register_sms', {
                        method: 'POST',
                        headers: {'Content-Type': 'application/json'},
                        body: JSON.stringify({'mobile': data.mobile})
                    }).then(response => response.json()).then(data => {
                        // 处理后端的响应数据
                        if (!data.code) {
                            layer.msg(data.message, {icon: 1})
                        } else {
                            layer.msg(data.message, {icon: 2})
                        }
                    })
                }
            }
        });
    });
</script>

点击获取验证码之后,就会发送请求到后端,然后后端处理完之后返回结果给前端。

后端发送短信验证码

后端获取数据之后

  1. 解析前端传递过来的手机号,然后进行校验
  2. 调用第三方工具下发注册短信验证码(腾讯云、阿里云就有对于的服务)
  3. 将手机号、短信验证码记录到数据库。下次提交时再进行校验
python
import logging
import re
import random
from flask import Flask, render_template, request, session

app = Flask(__name__)
app.secret_key = 'notes.zhengxinonly.com'


@app.post('/api/send_register_sms')
def send_register_sms():
    # 1. 解析前端传递过来的数据
    data = request.get_json()
    mobile = data['mobile']

    # 2. 校验手机号码
    pattern = r'^1[3-9]\d{9}$'
    ret = re.match(pattern, mobile)
    if not ret:
        return {
            'message': '电话号码不符合格式',
            'code': -1
        }

    # 3. 发送短信验证码,并记录
    session['mobile'] = mobile
    # 3.1 生成随机验证码
    code = random.choices('123456789', k=6)
    session['code'] = ''.join(code)
    logging.warning(code)
    return {
        'message': '发送短信成功',
        'code': 0
    }

因为是一个简单的案例,就没有调用腾讯云短信进行测试了,而是直接把正确结果返回。对于短信与验证码也没有存储到数据库了,而是放在 session 里面进行返回,下次请求注册时,再从里面进行提取。

实现注册逻辑

前端部分

在手机收到短信验证码之后,前端填入验证码,然后再输入两次密码,点击同意协议按钮,就能实现注册。在这同时需要做验证验证码与密码。

html

<script>
    layui.use(function () {
        var $ = layui.$;
        var form = layui.form;
        var layer = layui.layer;
        var util = layui.util;

        // 自定义验证规则
        form.verify({
            // 确认密码
            confirmPassword: function (value, item) {
                var passwordValue = $('#reg-password').val();
                if (value !== passwordValue) {
                    return '两次密码输入不一致';
                }
            }
        });

        // 提交事件
        form.on('submit(demo-reg)', function (data) {
            var field = data.field; // 获取表单字段值

            // 是否勾选同意
            if (!field.agreement) {
                layer.msg('您必须勾选同意用户协议才能注册');
                return false;
            }

            // 此处可执行 Ajax 等操作
            fetch('/api/register', {
                method: 'POST',
                headers: {'Content-Type': 'application/json'},
                body: JSON.stringify(field)
            }).then(response => response.json()).then(data => {
                // 处理后端的响应数据
                if (!data.code) {
                    layer.msg(data.message, {icon: 1})
                    setTimeout(function () {
                        location.href = '/login'
                    }, 2000)
                } else {
                    layer.msg(data.message, {icon: 2})
                }
            })

            return false; // 阻止默认 form 跳转
        });
    });
</script>

后端逻辑

数据库

小案例,使用 sqlite 是最方便的。

python
import sqlite3


class Database:
    def __init__(self):
        self.conn = sqlite3.connect('flask-layui.sqlite')
        self.cursor = self.conn.cursor()

    def create_table(self):
        sql = """create table user
            (
                id       integer primary key,
                nickname varchar(255),
                mobile   varchar(50),
                password varchar(50)
            );
            """
        self.cursor.execute(sql)
        self.conn.commit()

    def insert(self, nickname, mobile, password):
        sql = 'insert into user(nickname, mobile, password) values (?, ?, ?);'
        self.cursor.execute(sql, (nickname, mobile, password))
        self.conn.commit()

    def search(self, mobile):
        sql = 'select password from user where mobile=?;'
        self.cursor.execute(sql, (mobile,))
        return self.cursor.fetchone()


db = Database()

if __name__ == '__main__':
    db.create_table()
    # db.insert('正心全栈编程', '18675867241', '123456')
    # ret = db.search('18675867241')
    # print(ret)
python
from db import Database


@app.post('/api/register')
def register_api():
    # 1. 解析前端传递过来的数据
    data = request.get_json()
    vercode = data['vercode']
    vercode2 = session['code']
    if vercode != vercode2:
        return {
            'message': '短信验证码错误',
            'code': -1
        }

    nickname = data['nickname']
    mobile = data['mobile']
    password = data['password']
    if not all([nickname, mobile, password]):
        return {
            'message': '数据缺失',
            'code': -1
        }
    Database().insert(nickname, mobile, password)
    return {
        'message': '注册用户成功',
        'code': 0
    }

实现登录逻辑

登录时序图

用户
用户
浏览器
浏览器
后端服务器
后端服务器
访问登录页面
访问登录页面
返回登录页面
返回登录页面
返回登录页面
返回登录页面
请求图片验证码
请求图片验证码
返回图片验证码
返回图片验证码
输入信息进行登录
输入信息进行登录
校验信息是否正确
校验信息是否正确
格式不正常提示错误
格式不正常提示错误
格式正常请求登录
格式正常请求登录
返回结果 ,成功或失败
返回结果 ,成功或失败
MySql
MySql
校验信息是否正常
校验信息是否正常
记录当前验证码
记录当前验证码
登录成功之后重定向到首页
登录成功之后重定向到首页
返回首页
返回首页
失败返回错误信息,重新登录
失败返回错误信息,重新登录
查询数据库
查询数据库
Text is not SVG - cannot display

图片验证码

  1. 客户端请求服务器获得图片验证码
  2. 服务器返回图片,将生成的验证码保存到数据库
  3. 客户端携带验证码进行请求,服务器校验数据
  4. 登录成功之后跳转到首页,登录失败则重新登录

前端发送请求

生成 uuid 用于记录验证码

html

<script>
    // 生成 uuid
    function generateUUID() {
        var d = new Date().getTime();
        if (window.performance && typeof window.performance.now === 'function') {
            d += performance.now(); //use high-precision timer if available
        }
        var uuid = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g,
                function (c) {
                    var r = (d + Math.random() * 16) % 16 | 0;
                    d = Math.floor(d / 16);
                    return (c == 'x' ? r : (r & 0x3 | 0x8)).toString(16);
                });
        return uuid;
    }

</script>

图片验证码更新

html

<script>
    layui.use(function () {
        var form = layui.form;
        var layer = layui.layer;
        var $ = layui.$;
        var captcha_uuid = '';

        // 点击按钮更新验证码
        $('#captchaImage').click(function () {
            // 浏览器要发起图片验证码请求/captcha_code?captcha_code_uuid=xxxxx
            captcha_uuid = generateUUID();
            document.getElementById('captchaImage').src = '/get_captcha?captcha_uuid=' + captcha_uuid;
        });

        captcha_uuid = generateUUID();
        document.getElementById('captchaImage').src = '/get_captcha?captcha_uuid=' + captcha_uuid;
    });
</script>

后端生成图片验证码

安装依赖

shell
pip install captcha

然后编写后端的视图

python
# filename: /get_captcha.py
from io import BytesIO
from random import choices

from captcha.image import ImageCaptcha
from flask import make_response
from PIL import Image


def gen_captcha(content="0123456789"):
    """生成验证码"""
    image = ImageCaptcha()
    # 获取字符串
    captcha_text = "".join(choices(content, k=4))
    # 生成图像
    captcha_image = Image.open(image.generate(captcha_text))
    return captcha_text, captcha_image


# 生成验证码
def get_captcha_code_and_content():
    code, image = gen_captcha()
    out = BytesIO()
    image.save(out, "png")
    out.seek(0)
    content = out.read()  # 读取图片的二进制数据做成响应体
    return code, content


if __name__ == '__main__':
    code, content = get_captcha_code_and_content()
    print(code, content)
python
from get_captcha import get_captcha_code_and_content


@app.get('/get_captcha')
def get_captcha_view():
    # 1. 获取参数
    captcha_uuid = request.args.get("captcha_uuid")
    # 2. 生成验证码
    code, content = get_captcha_code_and_content()

    # 3. 记录数据到数据库(用session代替)
    session['code'] = code
    resp = make_response(content)  # 读取图片的二进制数据做成响应体
    resp.content_type = "image/png"
    # 4. 错误处理

    # 5. 响应返回
    return resp

前端请求进行登录

html

<script>
    layui.use(function () {
        var form = layui.form;
        var layer = layui.layer;
        var captcha_uuid = '';

        // 提交事件
        form.on('submit(demo-login)', function (data) {
            var field = data.field; // 获取表单字段值
            // 显示填写结果,仅作演示用
            field['captcha_uuid'] = captcha_uuid

            // 此处可执行 Ajax 等操作
            fetch('/api/login', {
                method: 'POST',
                headers: {'Content-Type': 'application/json'},
                body: JSON.stringify(field)
            }).then(response => response.json()).then(data => {
                if (!data.code) {
                    layer.msg(data.message, {icon: 1})
                    setTimeout(function () {
                        location.href = '/'
                    }, 2000)
                } else {
                    layer.msg(data.message, {icon: 2})
                }
            })
            return false; // 阻止默认 form 跳转
        });
    });
</script>

后端验证登录

编写登录接口

python
@app.post('/api/login')
def login_api():
    data = request.get_json()
    ret = Database().search(data['mobile'])
    code = session['code']
    if code != data['captcha']:
        return {
            'message': '验证码错误',
            'code': -1
        }
    if not ret:
        return {
            'message': '用户不存在',
            'code': -1
        }
    pwd = ret[0]
    if pwd != data['password']:
        return {
            'message': '用户密码错误',
            'code': -1
        }
    session['is_login'] = True  # 记录用户登录
    return {
        'message': '用户登录成功',
        'code': 0
    }

完善 index.html 渲染

python
@app.route('/')
def index_view():
    is_login = session.get('is_login')  #
    return render_template('index.html')  #
    return render_template('index.html', is_login=is_login)  #

附录:API

https://apifox.com/apidoc/shared-1705c3ba-b68b-4ae4-b635-eacc8f7949a9