Flask 項目實戰(zhàn) 2: 后端實現(xiàn)
上一節(jié)介紹了待做清單項目的功能、程序的總體結(jié)構(gòu),程序的總體結(jié)構(gòu)分為前端和后端兩個部分,本節(jié)講解后端的實現(xiàn)。
1. 數(shù)據(jù)庫設(shè)計
1.1 表的設(shè)計
在數(shù)據(jù)庫中存在兩張表:users 和 todos。
表 users 用于記錄已經(jīng)注冊的用戶,包含有如下字段:
字段 | 描述 |
---|---|
userId | 用戶的 ID,表的主鍵 |
name | 姓名 |
password | 密碼 |
表 todos 用于記錄待做事項,包含有如下字段:
字段 | 描述 |
---|---|
todoId | 待做事項的 ID,表的主鍵 |
userId | 所屬用戶的 ID |
status | 待做事項的狀態(tài),“todo” 表示待做,“done” 表示已經(jīng)完成 |
title | 待做事項的標(biāo)題 |
1.2 數(shù)據(jù)庫腳本 db.sql
創(chuàng)建文件 db.sql,內(nèi)容由如下部分構(gòu)成:
1. 創(chuàng)建數(shù)據(jù)庫 todoDB
SET character_set_database=utf8;
SET character_set_server=utf8;
DROP DATABASE IF EXISTS todoDB;
CREATE DATABASE todoDB;
USE todoDB;
如果數(shù)據(jù)庫 todoDB 已經(jīng)存在,則首先刪除,然后再創(chuàng)建數(shù)據(jù)庫 todoDB。
2. 創(chuàng)建表 users
CREATE TABLE users(
userId INT NOT NULL AUTO_INCREMENT,
name VARCHAR(255),
password VARCHAR(255),
PRIMARY KEY(userId)
);
創(chuàng)建表 users,表 users 包含 userId、name、password 等字段。userId 是主鍵,設(shè)置為從 1 自動增長。
3. 創(chuàng)建表 todos
CREATE TABLE todos(
todoId INT NOT NULL AUTO_INCREMENT,
userId INT,
status VARCHAR(255),
title VARCHAR(255),
PRIMARY KEY(todoId)
);
創(chuàng)建表 todos,表 todos 包含 todoId、userId、status、title 等字段。todoId 是主鍵,設(shè)置為從 1 自動增長。
4. 創(chuàng)建測試數(shù)據(jù)
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", "作業(yè)");
為了方便測試,向數(shù)據(jù)庫中插入一些預(yù)定義的數(shù)據(jù)。
在第 1 行,向表 users 中增加一個用戶 guest、密碼為 “123”,因為該用戶是表 users 中的第 1 條數(shù)據(jù),所以 userId 為 1。
在第 2 行到第 3 行,向表 todos 中增加 3 個 userId 為 1 的記錄,相當(dāng)于為 guest 用戶增加 3 個記錄;在第 2 行,插入待做事項 “吃飯”;在第 3 行,插入待做事項 “睡覺”;在第 4 行,插入已完成事項 “作業(yè)”。
最后,啟動 mysql 數(shù)據(jù)庫,在數(shù)據(jù)庫中執(zhí)行 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 中創(chuàng)建 Flask 實例 app,并進行兩項配置:
- config[‘SEND_FILE_MAX_AGE_DEFAULT’],配置緩存的有效時間;
- config[‘SECRET_KEY’],在程序中使用到了 Session,需要使用 SECRET_KEY 進行加密。
3. 入口 main.py
創(chuàng)建文件 main.py,它是 Flask 程序的入口,源代碼由如下部分構(gòu)成:
3.1 導(dǎo)入相關(guān)模塊
#!/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 中導(dǎo)入變量 app,他是 Flask 應(yīng)用程序?qū)嵗辉诘?5 行,導(dǎo)入模塊 db.py,該模塊用于提供了數(shù)據(jù)庫訪問接口。
程序包括兩個藍(lán)圖:users 藍(lán)圖和 todos 藍(lán)圖,在第 8 行和第 9 行,在 Flask 實例中注冊這兩個藍(lán)圖。
3.2 頁面 / 的視圖函數(shù)
@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()
設(shè)置網(wǎng)站的首頁面 / 的處理函數(shù)為 index,該函數(shù)首先查詢 Session 中的變量 hasLogin,如果為真,表示用戶已經(jīng)登錄,顯示用戶已經(jīng)輸入的待做事項和完成事項;如果為假,表示用戶沒有登錄,顯示待做事項和完成事項為空。
在第 5 行,查詢 Session 中的變量 userId,該變量表示已經(jīng)登錄用戶的 Id;在第 6 行,根據(jù) db.getTodos(userId) 獲取數(shù)據(jù)庫該用戶記錄的待做事項。
在第 7 行,獲取待做事項中 status 等于 ‘todo’ 的待做事項,保存在列表 todos 中;在第 8 行,獲取待做事項中 status 等于 ‘done’ 的待做事項,保存在列表 dones 中。
在第 13 行,渲染首頁模板 index.html,傳遞 3 個參數(shù):
- hasLogin,用戶是否登錄;
- todos,該用戶輸入的待做事項;
- dones,該用戶輸入的完成事項。
4. 數(shù)據(jù)庫訪問 db.py
4.1 引入相關(guān)模塊并配置
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 是數(shù)據(jù)庫的用戶名,變量 password 是數(shù)據(jù)庫的密碼,變量 database 是數(shù)據(jù)庫的名稱。在這個例子中,用戶是 root,密碼是 123456,請調(diào)整你的 mysql 設(shè)置。設(shè)置完這 3 個變量后,數(shù)據(jù)庫訪問的 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 映射數(shù)據(jù)庫中的表 users,該表包含 3 個字段 userId、name、password,與類 User 中相同名稱的 3 個屬性一一對應(yīng)。
使用類 Todo 映射數(shù)據(jù)庫中的表 todos,該表包含 4 個字段 todoId、userId、status、title,與類 Todo 中相同名稱的 4 個屬性一一對應(yīng)。
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
函數(shù) login 在表 users 中查找與 name、password 匹配的用戶,如果存在,則表示登錄成功。
函數(shù) register 根據(jù) name、password 創(chuàng)建一個新的用戶,然后插入到表 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
函數(shù) getTodos(userId) 在表中查詢屬于指定用戶的待做事項。
函數(shù) addTodo(userId, status, title) 根據(jù) userId、status、title 創(chuàng)建一個新的待做事項,然后插入到表 todos 中。
函數(shù) updateTodo(todoId,status) 更新待做事項的 status,當(dāng)用戶完成一個待做事項時,需要將待做事項的 status 從 “todo” 更改為 “done”。
函數(shù) deleteTodo(todoId) 刪除待做事項。
5. 藍(lán)圖 users.py
藍(lán)圖 users 包含有 3 個頁面:/users/login、/users/register、/users/logout,代碼由如下部分構(gòu)成:
5.1 導(dǎo)入相關(guān)模塊
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')
導(dǎo)入相關(guān)模塊,然后創(chuàng)建藍(lán)圖對象 blueprint,參數(shù) ‘users’ 是藍(lán)圖的名稱,參數(shù) url_prefix 是頁面的前綴。
藍(lán)圖 users 包含有 3 個頁面 /users/login、/users/register、/users/logout,設(shè)置 url_prefix 為 /users 后,使用 @app.route 注冊頁面的處理函數(shù)時,使用 /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 表單實現(xiàn)登錄表單,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 時,用于向服務(wù)器提交登陸請求。在第 7 行,創(chuàng)建一個 LoginForm 實例,然后調(diào)用 form.validate_on_submit() 驗證表單中的字段是否合法;在第 11 行,調(diào)用 db.login(name, password) 在數(shù)據(jù)庫驗證用戶身份,如果登錄成功,則返回登錄的用戶 user。
在第 12 行,如果登錄成功,在 Session 中設(shè)置 hasLogin 為 Ture,設(shè)置 userId 為登錄用戶的 userId;在第 15 行,調(diào)用 redirect(’/’),用戶登錄成功后,瀏覽器重定向到網(wǎng)站根頁面。
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 表單實現(xiàn)注冊表單,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 時,用于向服務(wù)器提交登陸請求。在第 7 行,創(chuàng)建一個 RegisterForm 實例,然后調(diào)用 form.validate_on_submit() 驗證表單中的字段是否合法;在第 11 行,調(diào)用 db.register(name, password) 在數(shù)據(jù)庫注冊一個新用戶,如果注冊成功,則返回 True。
在第 12 行,如果注冊成功,調(diào)用 redirect(’/’),用戶注冊成功后,瀏覽器重定向到網(wǎng)站根頁面。
5.6 退出系統(tǒng) /logout
@blueprint.route('/logout')
def logout():
session['hasLogin'] = False
return redirect('/')
訪問 /users/logout 頁面時,用戶退出系統(tǒng)。在 Session 中設(shè)置 hasLogin 為 False,調(diào)用 redirect(’/’),用戶退出系統(tǒng)后,瀏覽器重定向到網(wǎng)站根頁面。
6. 藍(lán)圖 todos.py
藍(lán)圖 todos 包含有 3 個頁面:/todos/add、/todos/update、/todos/delete,代碼由如下部分構(gòu)成:
6.1 導(dǎo)入相關(guān)模塊
from flask import Flask, render_template, request, redirect, session, jsonify
from flask import Blueprint
import db
blueprint = Blueprint('todos', __name__, url_prefix='/todos')
導(dǎo)入相關(guān)模塊,然后創(chuàng)建藍(lán)圖對象 blueprint,參數(shù) ‘todos’ 是藍(lán)圖的名稱,參數(shù) url_prefix 是頁面的前綴。
藍(lán)圖 todos 包含有 3 個頁面 /todos/add、/todos/update、/todos/delete,設(shè)置 url_prefix 為 /todos 后,使用 @app.route 注冊頁面的處理函數(shù)時,使用 /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 行調(diào)用 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});
當(dāng)用戶完成一個待做事項后,將待做事項移入到完成事項中,需要使用 POST 方法請求 /todos/update 頁面用于更新待做事項的 status,在第 5 行調(diào)用 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 行調(diào)用 db.deleteTodo(todoId) 刪除指定的待做事項。
在例子中忽略了錯誤處理,在第 5 行,返回錯誤為 None。
7. 小結(jié)
本節(jié)講解了后端的實現(xiàn),使用思維導(dǎo)圖概括如下: