章节索引 :

Flask 项目实战 2: 后端实现

上一节介绍了待做清单项目的功能、程序的总体结构,程序的总体结构分为前端和后端两个部分,本节讲解后端的实现。

1. 数据库设计

1.1 表的设计

在数据库中存在两张表:users 和 todos。

表 users 用于记录已经注册的用户,包含有如下字段:

字段 描述
userId 用户的 ID,表的主键
name 姓名
password 密码

表 todos 用于记录待做事项,包含有如下字段:

字段 描述
todoId 待做事项的 ID,表的主键
userId 所属用户的 ID
status 待做事项的状态,“todo” 表示待做,“done” 表示已经完成
title 待做事项的标题

1.2 数据库脚本 db.sql

创建文件 db.sql,内容由如下部分构成:

1. 创建数据库 todoDB

SET character_set_database=utf8;
SET character_set_server=utf8;
DROP DATABASE IF EXISTS todoDB;
CREATE DATABASE todoDB;
USE todoDB;

如果数据库 todoDB 已经存在,则首先删除,然后再创建数据库 todoDB。

2. 创建表 users

CREATE TABLE users(
    userId INT NOT NULL AUTO_INCREMENT,
    name VARCHAR(255),
    password VARCHAR(255),
    PRIMARY KEY(userId)
);

创建表 users,表 users 包含 userId、name、password 等字段。userId 是主键,设置为从 1 自动增长。

3. 创建表 todos

CREATE TABLE todos(
    todoId INT NOT NULL AUTO_INCREMENT,
    userId INT,
    status VARCHAR(255),
    title VARCHAR(255),
    PRIMARY KEY(todoId)
);

创建表 todos,表 todos 包含 todoId、userId、status、title 等字段。todoId 是主键,设置为从 1 自动增长。

4. 创建测试数据

INSERT INTO users(name, password) VALUES ("guest", "123");
INSERT INTO todos(userId, status, title) VALUES (1, "todo", "吃饭");
INSERT INTO todos(userId, status, title) VALUES (1, "todo", "睡觉");
INSERT INTO todos(userId, status, title) VALUES (1, "done", "作业");

为了方便测试,向数据库中插入一些预定义的数据。

在第 1 行,向表 users 中增加一个用户 guest、密码为 “123”,因为该用户是表 users 中的第 1 条数据,所以 userId 为 1。

在第 2 行到第 3 行,向表 todos 中增加 3 个 userId 为 1 的记录,相当于为 guest 用户增加 3 个记录;在第 2 行,插入待做事项 “吃饭”;在第 3 行,插入待做事项 “睡觉”;在第 4 行,插入已完成事项 “作业”。

最后,启动 mysql 数据库,在数据库中执行 db.sql:

mysql> source db.sql

2. Flask 实例 app.py

from flask import Flask
from datetime import timedelta

app = Flask(__name__)
app.config['SEND_FILE_MAX_AGE_DEFAULT'] = timedelta(seconds=1) 
app.config['SECRET_KEY'] = 'hard to guess string'

在程序 app.py 中创建 Flask 实例 app,并进行两项配置:

  • config[‘SEND_FILE_MAX_AGE_DEFAULT’],配置缓存的有效时间;
  • config[‘SECRET_KEY’],在程序中使用到了 Session,需要使用 SECRET_KEY 进行加密。

3. 入口 main.py

创建文件 main.py,它是 Flask 程序的入口,源代码由如下部分构成:

3.1 导入相关模块

#!/usr/bin/python3
from app import app
from flask import render_template, session

import db
import users 
import todos
app.register_blueprint(users.blueprint)
app.register_blueprint(todos.blueprint)

在第 2 行,从模块 app.py 中导入变量 app,他是 Flask 应用程序实例;在第 5 行,导入模块 db.py,该模块用于提供了数据库访问接口。

程序包括两个蓝图:users 蓝图和 todos 蓝图,在第 8 行和第 9 行,在 Flask 实例中注册这两个蓝图。

3.2 页面 / 的视图函数

@app.route('/')
def index():
    hasLogin = session.get('hasLogin')
    if hasLogin:
        userId = session.get('userId')
        items = db.getTodos(userId)
        todos = [item for item in items if item.status == 'todo']
        dones = [item for item in items if item.status == 'done']
    else:
        items = []
        todos = []
        dones = []
    return render_template('index.html', hasLogin = hasLogin, todos = todos, dones = dones)

app.run()

设置网站的首页面 / 的处理函数为 index,该函数首先查询 Session 中的变量 hasLogin,如果为真,表示用户已经登录,显示用户已经输入的待做事项和完成事项;如果为假,表示用户没有登录,显示待做事项和完成事项为空。

在第 5 行,查询 Session 中的变量 userId,该变量表示已经登录用户的 Id;在第 6 行,根据 db.getTodos(userId) 获取数据库该用户记录的待做事项。

在第 7 行,获取待做事项中 status 等于 ‘todo’ 的待做事项,保存在列表 todos 中;在第 8 行,获取待做事项中 status 等于 ‘done’ 的待做事项,保存在列表 dones 中。

在第 13 行,渲染首页模板 index.html,传递 3 个参数:

  • hasLogin,用户是否登录;
  • todos,该用户输入的待做事项;
  • dones,该用户输入的完成事项。

4. 数据库访问 db.py

db.py 中完成数据库访问相关的函数,db.py 分为如下几个部分:

4.1 引入相关模块并配置

from app import app
from flask_sqlalchemy import SQLAlchemy

user = 'root'
password = '123456'
database = 'todoDB'
uri = 'mysql+pymysql://%s:%s@localhost:3306/%s' % (user, password, database)
app.config['SQLALCHEMY_DATABASE_URI'] = uri
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
orm = SQLAlchemy(app)

变量 user 是数据库的用户名,变量 password 是数据库的密码,变量 database 是数据库的名称。在这个例子中,用户是 root,密码是 123456,请调整你的 mysql 设置。设置完这 3 个变量后,数据库访问的 URI 为:

mysql+pymysql://root:123456@localhost:3306/todoDB

4.2 映射表 users 和表 todos

class User(orm.Model):
    __tablename__ = 'users'
    userId = orm.Column(orm.Integer, primary_key=True)
    name = orm.Column(orm.String(255))
    password = orm.Column(orm.String(255))

class Todo(orm.Model):
    __tablename__ = 'todos'
    todoId = orm.Column(orm.Integer, primary_key=True)
    userId = orm.Column(orm.Integer)
    status = orm.Column(orm.String(255))
    title = orm.Column(orm.String(255))

使用类 User 映射数据库中的表 users,该表包含 3 个字段 userId、name、password,与类 User 中相同名称的 3 个属性一一对应。

使用类 Todo 映射数据库中的表 todos,该表包含 4 个字段 todoId、userId、status、title,与类 Todo 中相同名称的 4 个属性一一对应。

4.3 对表 users 进行操作

def login(name, password):
    users = User.query.filter_by(name = name, password = password)
    user = users.first()
    return user

def register(name, password):
    user = User(name = name, password = password)
    orm.session.add(user)
    orm.session.commit()
    return True

函数 login 在表 users 中查找与 name、password 匹配的用户,如果存在,则表示登录成功。

函数 register 根据 name、password 创建一个新的用户,然后插入到表 users 中。

4.4 对表 todos 进行操作

def getTodos(userId):
    todos = Todo.query.filter_by(userId = userId)
    return todos

def addTodo(userId, status, title):
    todo = Todo(userId = userId, status = status, title = title)
    orm.session.add(todo)
    orm.session.commit()
    return True

def updateTodo(todoId, status):
    todos = Todo.query.filter_by(todoId = todoId)
    todos.update({'status': status})
    orm.session.commit()
    return True

def deleteTodo(todoId):
    todos = Todo.query.filter_by(todoId = todoId)
    todos.delete()
    orm.session.commit()
    return True

函数 getTodos(userId) 在表中查询属于指定用户的待做事项。

函数 addTodo(userId, status, title) 根据 userId、status、title 创建一个新的待做事项,然后插入到表 todos 中。

函数 updateTodo(todoId,status) 更新待做事项的 status,当用户完成一个待做事项时,需要将待做事项的 status 从 “todo” 更改为 “done”。

函数 deleteTodo(todoId) 删除待做事项。

5. 蓝图 users.py

蓝图 users 包含有 3 个页面:/users/login、/users/register、/users/logout,代码由如下部分构成:

5.1 导入相关模块

from flask import Flask, render_template, request, redirect, session
from flask_wtf import FlaskForm
from wtforms import StringField, SubmitField, PasswordField
from wtforms.validators import DataRequired, Length
from flask import Blueprint
import db

blueprint = Blueprint('users', __name__, url_prefix='/users')

导入相关模块,然后创建蓝图对象 blueprint,参数 ‘users’ 是蓝图的名称,参数 url_prefix 是页面的前缀。

蓝图 users 包含有 3 个页面 /users/login、/users/register、/users/logout,设置 url_prefix 为 /users 后,使用 @app.route 注册页面的处理函数时,使用 /login、/register、/logout 作为 URL 即可,省略了前缀 /users。

5.2 登录表单

class LoginForm(FlaskForm):
    name = StringField(
        label = '姓名',
        validators = [
            DataRequired(message = '姓名不能为空')
        ]
    )

    password = PasswordField(
        label = '密码',
        validators =[
            DataRequired(message = '密码不能为空'),
            Length(min = 3, message = '密码至少包括 3 个字符')
        ]
    )

    submit = SubmitField('登录')

使用 WTForms 表单实现登录表单,LoginForm 继承于 FlaskForm,它包含 2 个字段 name 和 password。

name 字段的验证器 DataRequired 要求字段不能为空;password 字段的验证器 DataRequired 要求字段不能为空,验证器 Length 要求密码至少包括 3 个字符。

5.3 请求 /users/login 页面

@blueprint.route('/login', methods = ['GET', 'POST'])
def login():
    if request.method == 'GET':
        form = LoginForm()
        return render_template('login.html', form = form)
    else:
        form = LoginForm()
        if form.validate_on_submit():
            name = form.name.data 
            password = form.password.data
            user = db.login(name, password)
            if user:
                session['hasLogin'] = True
                session['userId'] = user.userId
                return redirect('/')
        return render_template('login.html', form = form)

页面 /users/login 有两种请求方法:GET 和 POST。

使用 GET 方法请求页面 /users/login 时,用于显示登陆界面。在第 5 行,使用 render_template 渲染登陆页面模板 login.html。

使用 POST 方法请求页面 /users/login 时,用于向服务器提交登陆请求。在第 7 行,创建一个 LoginForm 实例,然后调用 form.validate_on_submit() 验证表单中的字段是否合法;在第 11 行,调用 db.login(name, password) 在数据库验证用户身份,如果登录成功,则返回登录的用户 user。

在第 12 行,如果登录成功,在 Session 中设置 hasLogin 为 Ture,设置 userId 为登录用户的 userId;在第 15 行,调用 redirect(’/’),用户登录成功后,浏览器重定向到网站根页面。

5.4 注册表单

class RegisterForm(FlaskForm):
    name = StringField(
        label = '姓名',
        validators = [
            DataRequired(message = '姓名不能为空')
        ]
    )

    password = PasswordField(
        label = '密码',
        validators =[
            DataRequired(message = '密码不能为空'),
            Length(min = 3, message = '密码至少包括 3 个字符')
        ]
    )

    submit = SubmitField('注册')

使用 WTForms 表单实现注册表单,RegisterForm 继承于 FlaskForm,它包含 2 个字段 name 和 password。

name 字段的验证器 DataRequired 要求字段不能为空;password 字段的验证器 DataRequired 要求字段不能为空,验证器 Length 要求密码至少包括 3 个字符。

5.5 请求 /users/register 页面

@blueprint.route('/register', methods = ['GET', 'POST'])
def register():
    if request.method == 'GET':
        form = RegisterForm()
        return render_template('register.html', form = form)
    else:
        form = RegisterForm()
        if form.validate_on_submit():
            name = form.name.data 
            password = form.password.data
            if db.register(name, password):
                return redirect('/')
        return render_template('register.html', form = form)

页面 /users/register 有两种请求方法:GET 和 POST。

使用 GET 方法请求页面 /users/register 时,用于显示注册界面。在第 5 行,使用 render_template 渲染注册页面模板 register.html。

使用 POST 方法请求页面 /users/register 时,用于向服务器提交登陆请求。在第 7 行,创建一个 RegisterForm 实例,然后调用 form.validate_on_submit() 验证表单中的字段是否合法;在第 11 行,调用 db.register(name, password) 在数据库注册一个新用户,如果注册成功,则返回 True。

在第 12 行,如果注册成功,调用 redirect(’/’),用户注册成功后,浏览器重定向到网站根页面。

5.6 退出系统 /logout

@blueprint.route('/logout')
def logout():
    session['hasLogin'] = False
    return redirect('/')

访问 /users/logout 页面时,用户退出系统。在 Session 中设置 hasLogin 为 False,调用 redirect(’/’),用户退出系统后,浏览器重定向到网站根页面。

6. 蓝图 todos.py

蓝图 todos 包含有 3 个页面:/todos/add、/todos/update、/todos/delete,代码由如下部分构成:

6.1 导入相关模块

from flask import Flask, render_template, request, redirect, session, jsonify
from flask import Blueprint
import db

blueprint = Blueprint('todos', __name__, url_prefix='/todos')

导入相关模块,然后创建蓝图对象 blueprint,参数 ‘todos’ 是蓝图的名称,参数 url_prefix 是页面的前缀。

蓝图 todos 包含有 3 个页面 /todos/add、/todos/update、/todos/delete,设置 url_prefix 为 /todos 后,使用 @app.route 注册页面的处理函数时,使用 /add、/update、/delete 作为 URL 即可,省略了前缀 /todos。

6.2 请求 /todos/add 页面

@blueprint.route('/add', methods = ['POST'])
def addTodo():
    userId = session.get('userId')
    status = 'todo'
    title = request.json['title']
    db.addTodo(userId, status, title)
    return jsonify({'error': None});

使用 POST 方法请求 /todos/add 页面用于新增一个待做事项,在第 6 行调用 db.addTodo(userId, status, title) 向表 todos 中插入一行。

在例子中忽略了错误处理,在第 7 行,返回错误为 None。

6.3 请求 /todos/update 页面

@blueprint.route('/update', methods = ['POST'])
def updateTodo():
    todoId = request.json['todoId']
    status = 'done'
    db.updateTodo(todoId, status)
    return jsonify({'error': None});

当用户完成一个待做事项后,将待做事项移入到完成事项中,需要使用 POST 方法请求 /todos/update 页面用于更新待做事项的 status,在第 5 行调用 db.updateTodo(todoId, status) 个更新待做事项的 status。

在例子中忽略了错误处理,在第 6 行,返回错误为 None。

6.4 请求 /todos/delete 页面

@blueprint.route('/delete', methods = ['POST'])
def deleteTodo():
    todoId = request.json['todoId']
    db.deleteTodo(todoId)
    return jsonify({'error': None});

使用 POST 方法请求 /todos/delete 页面用于删除一个待做事项,在第 4 行调用 db.deleteTodo(todoId) 删除指定的待做事项。

在例子中忽略了错误处理,在第 5 行,返回错误为 None。

7. 小结

本节讲解了后端的实现,使用思维导图概括如下:

图片描述