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. 小結
本節講解了后端的實現,使用思維導圖概括如下: