Python中的flask-todolist

Python中的flask-todolist

五月 22, 2021

Flask 是一个使用 Python 语言编写的 Web 框架,它可以让你高效的编写 Web 程序。我最近用flask+vue搭建一个简单的todolist 项目示例来学习,主要是参考flask-tutorial,感兴趣的可以看看,示例代码

前言

这个项目比较简单,就是用户登录,然后有一个todolist列表,可以简单的增删改,前端界面使用vue框架,所以关于flask template涉及不多,感兴趣的可以自己去学习下。本系列基于python3版本,所以命令跟python2可能有些许出入。

准备工作

基础软件

安装python

编辑软件,一般文本软件软件即可,这个看个人习惯,我这边是用vscode

安装git,window用户推荐使用git bash 内置了很多linux命令

创建目录

1
2
3
mkdir todo-list
cd todo-list
mkdir app serve

我这边项目里面创建两个目录app(vue前端页面,这个这里不多做介绍),serve(flask后端服务),

虚拟环境

1
2
3
4
5
6
7
8
9
cd serve 
# 安装虚拟环境
py -3 -m venv env # Windows or
python3 -m venv env # Linux 和 macOS
# 激活虚拟环境
env\Scripts\activate # Windows
. env/bin/activate # Linux 或 macOS
# 安装flask
pip3 install flask

env这个名字不固定,你也可以使用venv,记得.gitignore忽略掉这个目录。

在激活虚拟环境后,无论操作系统和 Python 版本,都可以统一使用 pythonpip 命令来调用当前虚拟环境内的 Python 和 pip 程序/二进制文件。此时执行 pythonpip 命令指向的程序和激活脚本在同一个目录下,在 Windows 下所在目录为 env\Scripts\,Linux 和 macOS 下所在目录为 env/bin/

Hello Flask

serve根目录下创建app.py

1
2
3
4
5
6
7
8
9
10
11
12
13
# 创建app.py
vim app.py

# app.py内容
from flask import Flask
app = Flask(__name__)

@app.route('/')
def hello():
return 'Welcome to My Watchlist!'

# 启动
flask run

现在打开浏览器,访问 http://localhost:5000 即可访问我们的程序主页

程序发现机制

如果你把上面的程序保存成其他的名字,比如 hello.py,接着执行 flask run 命令会返回一个错误提示。这是因为 Flask 默认会假设你把程序存储在名为 app.pywsgi.py 的文件中。如果你使用了其他名称,就要设置系统环境变量 FLASK_APP 来告诉 Flask 你要启动哪个程序。

1
2
3
export FLASK_APP=hello.py # Linux 或 macOS
$Env:FLASK_APP=hello.py # Window PowerShell
set FLASK_APP=hello.py # Window CMD

管理环境变量

为了不用每次打开新的终端会话都要设置环境变量,我们安装用来管理系统环境变量的 python-dotenv

1
pip3 install python-dotenv

python-dotenv 安装后,Flask 会从项目根目录的 .flaskenv.env 文件读取环境变量并设置。

1
touch .env .flaskenv

.flaskenv 用来存储 Flask 命令行系统相关的公开环境变量;

.env 则用来存储敏感数据,不应该提交进Git仓库,我们把文件名 .env 添加到 .gitignore 文件的结尾(新建一行)来让 Git 忽略它。

开启调试模式:

1
2
vim .flaskenv
FLASK_ENV=development

Routing

Web的核心功能,这个后面经常用,有个印象即可。

Variable Rules

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@app.route('/user/<username>')
def show_user_profile(username):
# show the user profile for that user
return 'User %s' % escape(username)

@app.route('/post/<int:post_id>')
def show_post(post_id):
# show the post with the given id, the id is an integer
return 'Post %d' % post_id

@app.route('/path/<path:subpath>')
def show_subpath(subpath):
# show the subpath after /path/
return 'Subpath %s' % escape(subpath)

注意用户输入可能包含恶意代码,所以最好用escape进行转义处理

URL Building

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@app.route('/')
def index():
return 'index'

@app.route('/login', methods=['GET', 'POST'])
def login():
return 'login'

@app.route('/user/<username>')
def profile(username):
return '{}\'s profile'.format(escape(username))

with app.test_request_context():
print(url_for('index'))
print(url_for('login'))
print(url_for('login', next='/'))
print(url_for('profile', username='John Doe'))
1
2
3
4
/
/login
/login?next=/
/user/John%20Doe

Redirects and Errors

1
2
3
4
5
6
7
8
9
10
from flask import abort, redirect, url_for

@app.route('/')
def index():
return redirect(url_for('login'))

@app.route('/login')
def login():
abort(401)
this_is_never_executed()
1
2
3
4
5
from flask import render_template

@app.errorhandler(404)
def page_not_found(error):
return render_template('page_not_found.html'), 404

Request

1
2
3
4
5
6
7
8
9
10
11
12
13
14
from flask import request

@app.route('/login', methods=['POST', 'GET'])
def login():
error = None
if request.method == 'POST':
if valid_login(request.form['username'],
request.form['password']):
return log_the_user_in(request.form['username'])
else:
error = 'Invalid username/password'
# the code below is executed if the request method
# was GET or the credentials were invalid
return render_template('login.html', error=error)
1
searchword = request.args.get('key', '')
1
2
3
4
5
@app.route('/upload', methods=['GET', 'POST'])
def upload_file():
if request.method == 'POST':
f = request.files['the_file']
f.save('/var/www/uploads/' + secure_filename(f.filename))

Response

return string

1
return 'hello world'

rerun html

1
return '<hr>Hello World</hr>'

render template

1
2
3
4
5
6
return render_template('error.html'), 404

# or
resp = make_response(render_template('error.html'), 404)
resp.headers['X-Something'] = 'A value'
return resp

return json

1
2
3
4
5
6
7
8
@app.route("/me")
def me_api():
user = get_current_user()
return {
"username": user.username,
"theme": user.theme,
"image": url_for("user_image", filename=user.image),
}
1
2
3
4
@app.route("/users")
def users_api():
users = get_all_users()
return jsonify([user.to_json() for user in users])

数据库

这边为了简单,使用Sqlite

flask-sqlalchemy

为了简化数据库操作,我们将使用 SQLAlchemy——一个 Python 数据库工具(ORM,即对象关系映射)。借助 SQLAlchemy,你可以通过定义 Python 类来表示数据库里的一张表(类属性表示表中的字段 / 列),通过对这个类进行各种操作来代替写 SQL 语句。

Flask 有大量的第三方扩展,这些扩展可以简化和第三方库的集成工作。我们下面将使用一个叫做 Flask-SQLAlchemy 的官方扩展来集成 SQLAlchemy。

1
pip3 install flask-sqlalchemy

大部分扩展都需要执行一个“初始化”操作。你需要导入扩展类,实例化并传入 Flask 程序实例:

1
2
3
from flask_sqlalchemy import SQLAlchemy  # 导入扩展类
app = Flask(__name__)
db = SQLAlchemy(app) # 初始化扩展,传入程序实例 app

数据库URL

为了设置 Flask、扩展或是我们程序本身的一些行为,我们需要设置和定义一些配置变量

Flask文档配置页面

Flask-SQLAlchemy 文档的配置页面

  • sqlite:////tmp/test.db
  • mysql://username:password@server/db
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import os
import sys

from flask import Flask
from flask_sqlalchemy import SQLAlchemy

WIN = sys.platform.startswith('win')
if WIN: # 如果是 Windows 系统,使用三个斜线
prefix = 'sqlite:///'
else: # 否则使用四个斜线
prefix = 'sqlite:////'

app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = prefix + os.path.join(app.root_path, 'data.db')
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False # 关闭对模型修改的监控
# 在扩展类实例化前加载配置
db = SQLAlchemy(app)

创建模型

我们这边就只有两个模型,一个用户模型,一个代办项目模型

1
2
3
4
5
6
7
8
9
10
class User(db.Model, UserMixin): # 表名将会是 user
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(20))
password_hash = db.Column(db.String(128))

class TodoItem(db.Model):
__tablename__ = 'todo_item' # 表名将会是 todo_item
id = db.Column(db.Integer, primary_key=True) # 主键
title = db.Column(db.String(32)) # 标题
descs = db.Column(db.String(256)) # 描述

自定义命令

注册命令,便于我们通过CLI对程序进行一些额外的数据处理,比如数据库建表,脚本初始化等

1
2
3
4
5
6
7
8
9
10
import click

@app.cli.command() # 注册为命令
@click.option('--drop', is_flag=True, help='Create after drop.') # 设置选项
def initdb(drop):
"""Initialize the database."""
if drop: # 判断是否输入了选项
db.drop_all()
db.create_all()
click.echo('Initialized database.') # 输出提示信息
1
2
flask initdb # 创建数据库
flask initdb --drop # 清空数据库

CURD

创建

1
2
3
4
5
6
7
8
from app import User, TodoItem  # 导入模型类
>>> user = User(username='wjc') # 创建一个 User 记录
>>> ti1 = TodoItem(title='study') # 创建一个 TodoItem 记录
>>> ti2 = TodoItem(title='game') # 再创建一个 TodoItem 记录
>>> db.session.add(user) # 把新创建的记录添加到数据库会话
>>> db.session.add(ti1)
>>> db.session.add(ti2)
>>> db.session.commit() # 提交数据库会话,只需要在最后调用一次即可

读取

1
<模型类>.query.<过滤方法(可选)>.<查询方法>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
>>> from app import TodoItem  # 导入模型类
>>> todoItem = TodoItem.query.first() # 获取 Movie 模型的第一个记录(返回模型类实例)
>>> todoItem.title # 对返回的模型类实例调用属性即可获取记录的各字段数据
'study'
>>> TodoItem.query.all() # 获取 TodoItem 模型的所有记录,返回包含多个模型类实例的列表
[<TodoItem 1>, <TodoItem 2>]
>>> TodoItem.query.count() # 获取 Movie 模型所有记录的数量
2
>>> TodoItem.query.get(1) # 获取主键值为 1 的记录
<TodoItem 1>
>>> TodoItem.query.filter_by(title='game').first() # 获取 title 字段值为 game 的记录
<TodoItem 2>
>>> TodoItem.query.filter(TodoItem.title=='game').first() # 等同于上面的查询,但使用不同的过滤方法
<TodoItem 2>

sqlalchemy query

更新

1
2
3
4
>>> todoItem = TodoItem.query.get(2)
>>> todoItem.title = 'play ball' # 直接对实例属性赋予新的值即可
>>> todoItem.descs = '玩球'
>>> db.session.commit() # 注意仍然需要调用这一行来提交改动

删除

1
2
3
>>> todoItem = TodoItem.query.get(1)
>>> db.session.delete(todoItem) # 使用 db.session.delete() 方法删除记录,传入模型实例
>>> db.session.commit() # 提交改动

Flask Marshmallow

因为sqlalchemy查询出来的数据不能直接序列化,所以一般要转成dict,这边引入flask-marshmallow来处理

1
2
pip3 install  marshmallow-sqlalchemy
pip3 install flask-marshmallow

引入

1
2
3
4
5
from flask import Flask
from flask_marshmallow import Marshmallow

app = Flask(__name__)
ma = Marshmallow(app)

定义

1
2
3
class TodoItemSchema(ma.SQLAlchemyAutoSchema):
class Meta:
model = TodoItem

使用

1
2
3
4
5
@app.route('/todoItems', methods=['GET'])
def all():
todoItems = TodoItem.query.all()
todoitems_schema = TodoItemSchema()
return jsonify(result = todoitems_schema.dump(todoItems, many=True))

用户认证

密码存储

Flask 的依赖 Werkzeug 内置了用于生成和验证密码散列值的函数,werkzeug.security.generate_password_hash() 用来为给定的密码生成密码散列值,而 werkzeug.security.check_password_hash() 则用来检查给定的散列值和密码是否对应

1
2
3
4
5
6
7
8
9
10
11
12
13
from werkzeug.security import generate_password_hash, check_password_hash

class User(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(20))
username = db.Column(db.String(20)) # 用户名
password_hash = db.Column(db.String(128)) # 密码散列值

def set_password(self, password): # 用来设置密码的方法,接受密码作为参数
self.password_hash = generate_password_hash(password) # 将生成的密码保持到对应字段

def validate_password(self, password): # 用于验证密码的方法,接受密码作为参数
return check_password_hash(self.password_hash, password) # 返回布尔值

flask-jwt-extended

flask-jwt-extended

1
pip3 install flask-jwt-extended
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
from flask_jwt_extended import (
create_access_token,
jwt_required,
get_jwt_identity
)

# 创建toekn
access_token = create_access_token(identity=username)
# 获取当前用户
current_user_name = get_jwt_identity()

# 装饰器拦截接口
@app.route('/todoItem', methods=['POST'])
@jwt_required
def add():
todoItem = TodoItem(title = request.form['title'], descs = request.form['descs'])
db.session.add(todoItem)
db.session.commit()
return utils.result(msg= 'Item added.')

代码结构

代码结构调整,待定。。。

打包部署

待定

相关链接

flask-tutorial

flask 1.1