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)
程序包括两个蓝图: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
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. 小结
本节讲解了后端的实现,使用思维导图概括如下: