Flask框架教程
Flask Course
1.前提准备
Python
版本# python 3.8.0 # 查看Python版本 python --version
安装第三方
Flask
pip install flask # 如果安装失败,可以使用 -i,指定使用国内镜像源 # 清华镜像源:https://pypi.tuna.tsinghua.edu.cn/simple/
检查
Flask
是否安装成功flask --version
Flask官网
# 官网:https://flask.palletsprojects.com # 快速开始:https://flask.palletsprojects.com/en/3.0.x/quickstart/
2.一个简单的Flask程序
创建
Flask
项目目录。mkdir FlaskMarket
创建
app
文件。from flask import Flask app = Flask(__name__) @app.route("/") def hello_world(): return "<p>Hello, World!</p>"
运行
Flask
。flask --app market run
# 设置环境变量,也能够直接运行flask $env:FLASK_APP="market.py" $env:FLASK_APP="market" flask run
查看web页面
Debug 模式
# 运行flask项目时,在最后加--debug,以debug模式启动 $env:FLASK_APP="market.py" flask run --debug
以下是代码产生报错的截图
新增一个路由。
# 路由传参username @app.route("/about/<username>") def about_page(username): return f"<h1>this is about {username} page</h1>"
页面查询结果
3.Template模板文件
可以在Flask项目的目录下创建 templates
目录存放所会用的 html
文件,具体如下:
在Python代码中,直接返回 html
文件即可,不需要携带目录。
@app.route("/")
def hello_world():
return render_template("hello.html")
页面访问如下
4.数据发送到template
Jinjia2
是一个仿照 Django
模板的 Python
模板语句,实现了后端与模板之间的交互。
一个简单的数据交互。
后端
python
这样写:@app.route("/") def hello_world(): return render_template("home.html", item_name="Phone")
对应的前端
html
文件需要使用jiajia2
的语法接收变量,代码如下:<p>{{item_name}}</p>
页面效果如下:
列表数据交互。
后端
python
这样写:@app.route("/") def hello_world(): items = [ {"id": 1, "name": "Phone", "barcode": 123456789, "price": 500}, {"id": 2, "name": "Laptop", "barcode": 123654789, "price": 500}, {"id": 3, "name": "keybord", "barcode": 123456987, "price": 150}, ] return render_template("home.html", items=items)
对应的前端
html
这样接收:<table class="table table-hover table-dark"> <thead> <tr> <th scope="col">ID</th> <th scope="col">Name</th> <th scope="col">Barcode</th> <th scope="col">Price</th> </tr> </thead> <tbody> {% for item in items %} <tr> <td>{{item.id}}</td> <td>{{item.name}}</td> <td>{{item.barcode}}</td> <td>{{item.price}}</td> </tr> {% endfor %} </tbody> </table>
访问页面如下:
5.Template 继承
开发的网站可能涉及多个页面,需要抽取公共的内容,其余的 html
页面继承这些公共内容即可。
引入
base.html
文件。<!doctype html> <html lang="en"> <head> <!-- Required meta tags --> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> <!-- Bootstrap CSS --> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.5.3/dist/css/bootstrap.min.css" integrity="sha384-TX8t27EcRE3e/ihU7zmQxVncDAy5uIKz4rEkgIXeMed4M0jlfIDPvg6uqKI2xXr2" crossorigin="anonymous"> <title>Base Title</title> </head> <body> <!-- Navbar here --> <nav class="navbar navbar-expand-md navbar-dark bg-dark"> <a class="navbar-brand" href="#">EuanSu Coding Market</a> <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="*navbarNav"> <span class="navbar-toggler-icon"></span> </button> <div class="collapse navbar-collapse" id="navbarNav"> <ul class="navbar-nav mr-auto"> <li class="nav-item active"> <a class="nav-link" href="#">Home <span class="sr-only">(current)</span></a> </li> <li class="nav-item"> <a class="nav-link" href="#">Market</a> </li> </ul> <ul class="navbar-nav"> <li class="nav-item"> <a class="nav-link" href="#">Login</a> </li> <li class="nav-item"> <a class="nav-link" href="#">Register</a> </li> </ul> </div> </nav> <!-- Future Content here --> <!-- Optional JavaScript --> <!-- jQuery first, then Popper.js, then Bootstrap JS --> <script src='https://kit.fontawesome.com/a076d05399.js'></script> <script src="https://code.jquery.com/jquery-3.5.1.slim.min.js" integrity="sha384-DfXdz2htPH0lsSSs5nCTpuj/zy4C+OGpamoFVy38MVBnE+IbbVYUew+OrCXaRkfj" crossorigin="anonymous"></script> <script src="https://cdn.jsdelivr.net/npm/popper.js@1.16.1/dist/umd/popper.min.js" integrity="sha384-9/reFTGAW83EW2RDu2S0VKaIzap3H66lZH81PoYlFhbGU+6BZp6G7niu735Sk7lN" crossorigin="anonymous"></script> <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/js/bootstrap.min.js" integrity="sha384-B4gt1jrGC7Jh4AgTPSdUtOBvfO8shuf57BaghqFfPlYxofvL8/KUEfYiJOMMV+rV" crossorigin="anonymous"></script> </body> <style> body { background-color: #212121; color: white } </style> </html>
清空
home.html
原文件,修改为如下内容:{% extends "base.html" %}
访问页面如下:
这里有一个问题就是页面标题显示为
Base Title
,实际上每个页面的标题是不一样,这里可以通过block
语句进行修改,代码如下:修改
base.html
文件head
下的title
标签为如下内容:<head> ... <title> {% block title%} {% endblock %} </title> </head>
修改
home.html
为如下内容:{% extends "base.html" %} {% block title%} Home Page {% endblock %}
再次刷新页面,
title
的内容被替换。替换
html
文件body
下的内容:首先是修改
base.html
中body
的内容,修改如下:{% block content%} {% endblock %}
修改
market.html
为如下内容:{% extends "base.html" %} {% block title%} Market Page {% endblock %} {% block content%} <table class="table table-hover table-dark"> <thead> <tr> <th scope="col">ID</th> <th scope="col">Name</th> <th scope="col">Barcode</th> <th scope="col">Price</th> </tr> </thead> <tbody> {% for item in items %} <tr> <td>{{item.id}}</td> <td>{{item.name}}</td> <td>{{item.barcode}}</td> <td>{{item.price}}</td> <td> <button class="btn btn-outline btn-info">More Info</button> <button class="btn btn-outline btn-success">Purchase this Item</button> </td> </tr> {% endfor %} </tbody> </table> {% endblock %}
访问页面,能够正常对数据进行渲染。
页面跳转
html
文件的href
进行跳转,这里需要使用jinjia2
的语法,而不能直接使用路由。<a class="nav-link" href="{{ url_for('home_page') }}">Home <span class="sr-only">(current)</span></a> <a class="nav-link" href="{{ url_for('market_page') }}">Market</a>
其中的
market_page
是路由关联的函数,如下所示:@app.route("/") def home_page(): items = [ {"id": 1, "name": "Phone", "barcode": 123456789, "price": 500}, {"id": 2, "name": "Laptop", "barcode": 123654789, "price": 500}, {"id": 3, "name": "keybord", "barcode": 123456987, "price": 150}, ] return render_template("home.html", items=items) @app.route("/market") def market_page(): items = [ {"id": 1, "name": "Phone", "barcode": 123456789, "price": 500}, {"id": 2, "name": "Laptop", "barcode": 123654789, "price": 500}, {"id": 3, "name": "keybord", "barcode": 123456987, "price": 150}, ] return render_template("market.html", items=items)
再次点击页面的按钮,能够正常进行路由跳转。
6.数据库模型
6.1 数据库模型的基本使用
安装 flask-sqlalchemy
第三方包。
pip install flask-sqlalchemy
python
文件导入 flask-sqlalchemy
库。
from flask_sqlalchemy import SQLAlchemy
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///market.sqlite'
db = SQLAlchemy(app)
编写模型类。
class Item(db.Model):
id = db.Column(db.Integer(), primary_key=True)
name = db.Column(db.String(length=30),nullable=False, unique=True)
price = db.Column(db.Integer(), nullable=True)
barcode = db.Column(db.String(length=12), nullable=True, unique=True)
description = db.Column(db.String(length=1024), nullable=True, unique=True)
需要在 Flask
的 app
文件中,添加数据库初始化操作。
with app.app_context():
db.create_all()
使用可视化工具查看 SQLite
本地数据库文件,出现初始化的 Item
表。
6.2 SQLAlchemy 的基本使用
新增数据库记录
item1 = Item(name="OPPO Find X6 Pro",price=5000,barcode='123456789',description='OPPO Find X6 Pro')
with app.app_context():
db.session.add(item1)
db.session.commit()
执行如上语句后,数据库中出现一条手机记录。
查询数据库记录
# 全量查询
result = Item.query.all()
print(result)
for item in result:
print(item.name)
# 根据条件过滤
result = Item.query.filter_by(name='OPPO Find X6 Pro')
print(result)
print('=============')
for item in result:
print(item.name)
修改数据库记录
result = Item.query.filter_by(name='OPPO Find X6 Pro')
if result:
item = result[0]
item.price = 5999
db.session.commit()
修改后,数据库中的记录发生了变化。
删除数据库记录
# 查询要删除的记录
record_to_delete = Item.query.filter_by(name="OnePlus 12").first()
# 如果记录存在,则删除
if record_to_delete:
db.session.delete(record_to_delete)
db.session.commit()
这里的数据库查询放到代码中,如下:
@app.route("/market")
def market_page():
items = [
{"id": 1, "name": "Phone", "barcode": 123456789, "price": 500},
{"id": 2, "name": "Laptop", "barcode": 123654789, "price": 500},
{"id": 3, "name": "keybord", "barcode": 123456987, "price": 150},
]
items = Item.query.all()
return render_template("market.html", items=items)
页面就能够直接展示数据库中的记录
7.项目重构
# 这里将项目移动至mark目录下,主目录下仅留项目的启动文件 run.py
D:\CODE\PYTHON\FLASKMARKET
├─instance
├─market
│ ├─templates
│ │ ├─css
│ │ ├─js
│ │ ├─base.html
│ │ ├─home.html
│ │ └─market.html
│ ├─__init__.py
│ ├─models.py
│ ├─routes.py
│ └─__pycache__
├─run.py
└─__pycache__
修改后的各文件一次如下所示:
__init__.py
模块初始化文件:
from flask import Flask, render_template
from flask_sqlalchemy import SQLAlchemy
app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///market.sqlite'
db = SQLAlchemy(app)
from market import routes
models.py
模型文件:
from market import db
class Item(db.Model):
id = db.Column(db.Integer(), primary_key=True)
name = db.Column(db.String(length=30),nullable=False, unique=True)
price = db.Column(db.Integer(), nullable=True)
barcode = db.Column(db.String(length=12), nullable=True, unique=True)
description = db.Column(db.String(length=1024), nullable=True, unique=True)
routes.py
路由文件:
from market import app
from flask import render_template
from market.models import Item
@app.route("/")
@app.route("/home")
def home_page():
items = [
{"id": 1, "name": "Phone", "barcode": 123456789, "price": 500},
{"id": 2, "name": "Laptop", "barcode": 123654789, "price": 500},
{"id": 3, "name": "keybord", "barcode": 123456987, "price": 150},
]
return render_template("home.html", items=items)
@app.route("/market")
def market_page():
items = [
{"id": 1, "name": "Phone", "barcode": 123456789, "price": 500},
{"id": 2, "name": "Laptop", "barcode": 123654789, "price": 500},
{"id": 3, "name": "keybord", "barcode": 123456987, "price": 150},
]
items = Item.query.all()
return render_template("market.html", items=items)
再次启动项目:
页面能够正常访问
8.数据库模型
Flask
中也能够使用类似于 Django
中的 ORM
,通过 Python
的 SQLAlchemy
第三方库实现。
需要安装的第三方包:
# ORM
pip install flask-sqlalchemy
# 数据迁移
pip install flask-migrate
# MySQL驱动
pip install pymysql
# 安装失败,指定如下镜像源即可
# pip install flask-sqlalchemy https://pypi.tuna.tsinghua.edu.cn/simple/
# pip install flask-migrate -i https://pypi.tuna.tsinghua.edu.cn/simple/
# pip install pymysql -i https://pypi.tuna.tsinghua.edu.cn/simple/
数据库配置:
# SQLite连接的URL:
DB_URI = sqlite:///sqlite3.db
# Mysql连接的URL:
mysql+pymysql://USERNAME:PASSWORD@HOSTNAME:POST/DATABASE
# 配置Mysql连接的URL
DB_URI = 'mysql+pymysq://{USERNAME}:{PASSWORD}@{HOST}:{PORT}/{DATABASE}'.format(
USERNAME=USERNAME,
PASSWORD=PASSWORD,
HOST=HOST,
PORT=PORT,
DATABASE=DATABASE
)
在 Flask
中使用 ORM
# 连接数据库需要指定配置
app.config['SQLALCHEMY_DATABASE_URI'] = 'DB_URI # 配置连接路径
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False # 禁止对象追踪修改,限制资源的消耗
# 在Flask项目中使用
db = SQLAlchemy()
执行数据库迁移的命令
flask db init # 创建迁移文件夹migrates,只调用一次
flask db migrate # 生成迁移文件
flask db upgrade # 执行迁移文件中的升级
flask db downgrade # 执行迁移文件中的降级
8.1 数据库模型迁移操作
安装第三方包:
# ORM
pip install flask-sqlalchemy
# 数据迁移
pip install flask-migrate
# MySQL驱动
pip install pymysql
# 安装失败,指定如下镜像源即可
# pip install flask-sqlalchemy https://pypi.tuna.tsinghua.edu.cn/simple/
# pip install flask-migrate -i https://pypi.tuna.tsinghua.edu.cn/simple/
# pip install pymysql -i https://pypi.tuna.tsinghua.edu.cn/simple/
配置数据库:
# 这里先使用sqlite数据库
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///market.sqlite'
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
创建迁移文件夹:
flask db init # 创建迁移文件夹migrates,只调用一次
这一步可能会出现如下的错误,这是因为 Flask
没有找到我们创建的 app
。
执行如下步骤:
# windows环境设置FLASK_APP
$env:FLASK_APP="run.py"
flask db init
执行后,在项目目录下生成了 migrations
目录。
生成迁移文件
flask db migrate
执行这一步,有如下报错产生:
跟踪代码,发现其中一个地方 current_app.extensions['migrate'].db.get_engine()
但是这的 current_app.extensions['migrate'].db
是 None
,因此产生了报错。
stackoverflow
有一个相同的报错:https://stackoverflow.com/questions/58118481/flask-migration-fails,这里是没有给 migrate.init_app()
传入 db
参数,检查后,项目中确实少传了参数,修改项目代码为如下:
在 __init__.py
中写入数据迁移的相关操作。
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from flask_migrate import Migrate
app = Flask(__name__)
# 配置数据库
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///market.sqlite'
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
db = SQLAlchemy()
migrate = Migrate()
# 这里需要导入要初始化的模型文件,否则可能无法生成迁移文件
from market.models import Item
# 初始化插件
db.init_app(app)
migrate.init_app(app, db)
修改后,再次执行迁移文件的操作,在 migrations\version
下生成了迁移文件。
执行迁移文件的升级操作:
flask db upgrade
打开数据库工具,数据库已经迁移成功。
执行数据库降级操作,撤销该次的升级操作。
flask db downgrade
打开数据库,本次迁移创建的数据表已撤销。
总结:
数据库的模型迁移操作涉及的基本包有:
# ORM pip install flask-sqlalchemy # 数据迁移 pip install flask-migrate
数据库的模型迁移需要创建的代码有:
from flask import Flask from flask_sqlalchemy import SQLAlchemy from flask_migrate import Migrate app = Flask(__name__) # 配置数据库 app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///market.sqlite' app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False db = SQLAlchemy() migrate = Migrate() # 这里需要导入要初始化的模型文件,否则可能无法生成迁移文件 from market.models import Item # 初始化插件 db.init_app(app) migrate.init_app(app, db)
数据库模型的迁移操作:
# 设置FLASK_APP环境变量 $env:FLASK_APP="run.py" # 初始化迁移目录,仅需一次操作 flask db init # 生成迁移文件 flask db migrate # 执行迁移操作 flask db upgrade # 撤回迁移操作 flask db downgrade
8.2 数据模型关系
8.2.1 一对多
如上所示,一个作者关联多个文章,暂时认定,一篇文章只能有一个作者。
作者以及文章的类定义如下所示:
class Author(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(128), unique=True)
email = db.Column(db.String(128))
class Article(db.Model):
id = db.Column(db.Integer, primary_key=True)
title = db.Column(db.String(50), index=True)
description = db.Column(db.Text)
8.2.1.1 一对多的建立步骤
现在需要在数据库中,将作者和文章的关系关联成一对多的关系,具体操作如下:
定义外键
外键(
Foreign key
) 用来在B
表存储A
表的主键值,作为与A
表的关系字段。由于外键只能够存储单一数据,所以外键常在 “多” 的一侧定义,一个作者对应多个文章,因此需要在文章模型中添加作者的关系字段,记录作者的主键值,代码如下:
class Article(db.Model): ... author_id = db.Column(db.Integer, db.ForeignKey('author.id'))
定义关系属性
关系属性的定义主要是用来标记该类与那个类建立了关系,常常在 “一” 的一侧进行定义,关系属性能够返回多个记录,也称之为集合关系属性。
在作者和文章的关系中,就需要在作者一侧定义关系属性,代码如下:
class Author(db.Model): ... articles = db.relationship('Article')
创建表
# 我在这里是通过python直接创建表,因此使用了app.app_context()这个方法,这个方法主要是用来引入flask的各种方法,否则操作会产生报错 with app.app_context(): # 将所有的模型文件创建为表 db.create_all() # 删除数据库中所有的表 db.drop_all()
建立关系
建立关系这里指的是,将两张表的数据进行关系,主要有以下两种方式:外键字段赋值、关系属性赋值。
这里我们先准备几组数据,用来操作实现关系的建立。
from market import app,db from market.models import Author,Article author1 = Author(name='余华',email='yuhua@euansu.cn') author2 = Author(name='莫言',email='moyan@euansu.cn') author3 = Author(name='史铁生',email='shitiesheng@euansu.cn') article1 = Article(title='活着',description='活着') article2 = Article(title='许三观卖血记',description='许三观卖血记') article3 = Article(title='我与地坛',description='我与地坛') article4 = Article(title='红高粱',description='红高粱') article5 = Article(title='蛙',description='蛙') with app.app_context(): db.session.add(author1) db.session.add(author2) db.session.add(author3) db.session.add(article1) db.session.add(article2) db.session.add(article3) db.session.add(article4) db.session.add(article5) db.session.commit()
执行如上操作后,查看数据库,正常插入了数据。
这里需要注意,关系属性虽然在作者模型中,但并未实际在表中创建字段,接下来通过如下代码对数据表中的数据建立一对多关系:
# 外键字段赋值 with app.app_context(): author = Author.query.filter_by(name='余华').first() article = Article.query.filter_by(title='活着').first() article.author_id = author.id db.session.commit()
执行完成后,查看数据库,数据表
author_id
的值为关联作者的主键值。# 执行完外键关系赋值后,可以通过如下调用,查询作者余华关联的图书 with app.app_context(): author = Author.query.filter_by(name='余华').first() print(author.articles)
# 操作关系属性 with app.app_context(): author = Author.query.filter_by(name='余华').first() article = Article.query.filter_by(title='许三观卖血记').first() author.articles.append(article) # 提交事务,将变更写入到数据库 db.session.commit()
执行完成后,查看数据库,数据表
author_id
的值为关联作者的主键值。# 执行完外键关系赋值后,可以通过如下调用,查询作者余华关联的图书 with app.app_context(): author = Author.query.filter_by(name='余华').first() print(author.articles)
因此,综上步骤,我们通过Flask建立两个表之间的一对多关系时,需要通过以下三个步骤:
- 定义外键,需要在 “多” 侧表的模型中增加外键字段。
- 定义关系属性,需要在 “一” 侧表的模型中定义关系属性,该属性并不体现在实际的表中。
- 建立关系,通过指定外键字段或操作关系属性,能够建立两个表之间的一对多关系属性。
8.2.1.2 建立双向关系
我们在 Author
类中定义了集合关系属性 articles
,用以获取某个作者的多个作品。在特殊场景下,也有可能希望在 Article
类中定义一个类似的 author
关系属性,当被调用时,返回关联的 Author
记录,这类返回单个值的关系属性被称为标量关系属性。而两侧都添加关系属性获取对方记录的称之为双向关系。
双向关系并不是必须的,只是满足于特殊的场景,可以按照如下方式建立双向关系:
class Author(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(128), unique=True)
email = db.Column(db.String(128))
articles = db.relationship('Article')
class Article(db.Model):
id = db.Column(db.Integer, primary_key=True)
title = db.Column(db.String(50), index=True)
description = db.Column(db.Text)
author_id = db.Column(db.Integer, db.ForeignKey('author.id'))
author = db.relationship('Author')
使用如下 Python
代码查询:
with app.app_context():
author = Author.query.filter_by(name='余华').first()
article = Article.query.filter_by(title='许三观卖血记').first()
print(author.articles)
print(article.author)
8.2.1.3 使用 backref 简化关系定义
backef
参数用来自动为关系另一侧添加关系属性,作为反向引用,赋予的值会作为关系另一侧的关系属性名称。
class Author(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(128), unique=True)
email = db.Column(db.String(128))
articles = db.relationship('Article', backref='author')
class Article(db.Model):
id = db.Column(db.Integer, primary_key=True)
title = db.Column(db.String(50), index=True)
description = db.Column(db.Text)
author_id = db.Column(db.Integer, db.ForeignKey('author.id'))
再次执行关系属性语句:
with app.app_context():
author = Author.query.filter_by(name='余华').first()
article = Article.query.filter_by(title='许三观卖血记').first()
print(author.articles)
print(article.author)
能够正常获取其关系的对象。
使用 backref
非常方便,但通常来说 “显式好过隐式”,所以我们应该尽量使用 back_populates
定义双向关系。
8.2.2 多对一
一对多的关系反过来就是多对一,这两种关系模型分别从不同的视角出发。
在一对多中,我们提交以下两点:
- 定义外键,需要在 “多” 侧表的模型中增加外键字段。
- 定义关系属性,需要在 “一” 侧表的模型中定义关系属性,该属性并不体现在实际的表中。
因此,多对一的关系代码定义如下:
class Author(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(128), unique=True)
email = db.Column(db.String(128))
# 定义外键
author_id = db.Column(db.Integer, db.ForeignKey('author.id'))
# 定义关系属性
articles = db.relationship('Article')
class Article(db.Model):
id = db.Column(db.Integer, primary_key=True)
title = db.Column(db.String(50), index=True)
description = db.Column(db.Text)
8.2.3 一对一
一对一关系是在一对多关系的基础上转化而来,只要确保两侧的关联关系唯一即可保证一对多关系转系转化为了一对一关系,在定义时,设置关系属性的 uselist
为 Flase
,此时的一对多关系转化为一对一关系。
class Person(db.Model):
id = db.Column(db.Integer, primary_key = True)
name = db.Column(db.String(30), unique = True)
idcard = db.relationship('IDCard', uselist = False)
def __repr__(self):
return '<Person %r>' % self.name
class IDCard(db.Model):
id = db.Column(db.Integer, primary_key = True)
idcard = db.Column(db.String(30), unique = True)
person_id = db.Column(db.Integer, db.ForeignKey('person.id'))
person = db.relationship('Person')
def __repr__(self):
return '<IDCard %r>' % self.idcard
这里执行如下操作先写入数据:
with app.app_context():
idcard = IDCard(idcard='123456789')
idcard2 = IDCard(idcard='123456798')
person = Person(name='euansu')
db.session.add(idcard)
db.session.add(idcard2)
db.session.add(person)
db.session.commit()
建立关系:
# 指定外键id
with app.app_context():
idcard = IDCard.query.filter_by(idcard='123456789').first()
person = Person.query.filter_by(name='euansu').first()
idcard.person_id = person.id
db.session.commit()
查看数据库,正常写入:
# 操作关系属性
with app.app_context():
idcard = IDCard.query.filter_by(idcard='123456798').first()
person = Person.query.filter_by(name='euansu').first()
person.idcard.append(idcard)
# 提交事务,将变更写入到数据库
db.session.commit()
8.2.4 多对多
多对多关系中,需要建立一个关联表,关联表并不存在数据,只用来存储两侧模型外键的对应关系。
association_table = db.Table(
'association',
db.Column('student_id', db.Integer, db.ForeignKey('student.id')),
db.Column('teacher_id', db.Integer, db.ForeignKey('teacher.id'))
)
class Student(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(128), unique=True)
grade = db.Column(db.String(20))
teachers = db.relationship('Teacher', secondary=association_table, back_populates='students')
class Teacher(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(128), unique=True)
grade = db.Column(db.String(20))
stutents = db.relationship('Student', secondary=association_table, back_populates='teachers')
创建完模型之后,执行迁移,在数据库中查看到新增的三张表:
flask db migrate
flask db upgrade
9.Flask Form表单
前提条件:
pip install flask-wtf
pip install wtforms
# 如安装失败,使用如下方式:
pip install flask-wtf -i https://pypi.tuna.tsinghua.edu.cn/simple/
pip install wtforms -i https://pypi.tuna.tsinghua.edu.cn/simple/
9.1 表单的编写
编写表单组件的代码,这里新建一个 forms.py
文件,项目结构以及表单组件的代码如下所示:
from flask_wtf import FlaskForm
from wtforms import StringField, PasswordField, SubmitField
class RegisterForm(FlaskForm):
username = StringField(label="User Name:")
email = StringField(label="Email Address:")
password1 = PasswordField(label="Password:")
password2 = PasswordField(label="Confirm Password:")
submit = SubmitField(label="Create Account")
编写路由文件,这里主要是写要应用到表单组件的前端页面以及路由:
@app.route("/register")
def register_page():
form = RegisterForm()
return render_template("register.html", form=form)
前端页面如下所示:
{% extends "base.html" %}
{% block title%}
Register Page
{% endblock %}
{% block content%}
<body>
<div class="container">
<form method="POST" class="form-register" style="color:white">
<img class="mb-4" src="{{ url_for('static', filename='euansu.png') }}" style="width: 100px;height: 100px;margin-top: 30px;"/>
<h1 class="h3 mb-3 font-weight-normal">
Please Create your Account
</h1>
{{ form.username.label() }}
{{ form.username(class="form-control", placeholder="User Name") }}
{{ form.email.label() }}
{{ form.email(class="form-control", placeholder="Email Address") }}
{{ form.password1.label() }}
{{ form.password1(class="form-control", placeholder="Password") }}
{{ form.password2.label() }}
{{ form.password2(class="form-control", placeholder="Confirm Password") }}
<br />
{{form.submit(class="btn btn-lg btn-block btn-primary")}}
</form>
</div>
</body>
{% endblock %}
实现效果如下图所示:
这里的 img
是我本地的图片,也可以替换成你自己的,按照如下步骤操作即可:
在
Flask
项目的__init__py
文件中,配置静态文件的路径:app.static_folder = 'static'
在项目下创建
static
目录,目录结构如下所示。访问路由,查看是否能够使用。
在前端
html
文件中,如果是jinjia2
代码块中,使用如下写法:<img class="mb-4" src="{{ url_for('static', filename='euansu.png') }}" style="width: 100px;height: 100px;margin-top: 30px;"/>
9.2 表单的提交
编写后端代码:
# 这里先只是进行一下模拟,并不实际创建账号
@app.route("/register", methods=["GET", "POST"])
def register_page():
form = RegisterForm()
if form.validate_on_submit():
# 创建用户
print("用户{username}创建成功".format(username=form.username.data))
return redirect(url_for('home_page'))
return render_template("register.html", form=form)
页面输入表单信息进行登录:
输入完成后跳转路由至用户家目录:
9.3 表单的验证
修改 forms.py
为如下内容:
from flask_wtf import FlaskForm
from wtforms import StringField, PasswordField, SubmitField
from wtforms.validators import Length, EqualTo, Email, DataRequired
class RegisterForm(FlaskForm):
username = StringField(label="User Name:", validators=[Length(min=2, max=32), DataRequired()])
email = StringField(label="Email Address:", validators=[Email(), DataRequired()])
password1 = PasswordField(label="Password:", validators=[Length(min=8, max=32), DataRequired()])
password2 = PasswordField(label="Confirm Password:",
validators=[Length(min=8, max=32), EqualTo('password1'), DataRequired()])
submit = SubmitField(label="Create Account")
页面验证:
这里可能有报错,如下所示:
验证邮箱需要单独安装 email_validator
,如下所示:
pip install email_validator
pip install email_validator -i https://pypi.tuna.tsinghua.edu.cn/simple/
再次校验,就能够正常输出不通过校验的信息。
9.4 flash消息
用户执行某些动作后,通常需要在页面显示一个提示消息。Flask
内置了相关的函数 flash()
, flash()
函数用来在视图函数里向模板传递提示消息,get_flashed_messages()
函数则用来在模板中获取提示消息。
使用方法:
from flask import flash
# 视图函数中直接调用即可
flash('Message')
flash()
函数在内部会把消息存储到 Flask 提供的 session
对象里。session
用来在请求间存储数据,它会把数据签名后存储到浏览器的 Cookie 中,所以我们需要设置签名所需的密钥:
app.config['SECRET_KEY'] = '08828e2a1c6e1e35cf020c09'
这个密钥值是个随机值,可以按照如下方式进行设置:
import os
os.urandom(12).hex()
如下是一个 flash()
函数的使用示例:
后端函数:
from market.forms import RegisterForm @app.route("/register", methods=["GET", "POST"]) def register_page(): form = RegisterForm() if form.validate_on_submit(): # 创建用户 print("用户{username}创建成功".format(username=form.username.data)) return redirect(url_for('home_page')) if form.errors != {}: for err_msg in form.errors.values(): print("{}".format(err_msg)) flash(f"There was an error with creating account:{err_msg}", category="danger") return render_template("register.html", form=form) # 这里用到了一个表单,需要事先在forms.py,也即你自己定义的表单中进行创建如下内容 from flask_wtf import FlaskForm from wtforms import StringField, PasswordField, SubmitField from wtforms.validators import Length, EqualTo, Email, DataRequired class RegisterForm(FlaskForm): username = StringField(label="User Name:", validators=[Length(min=2, max=32), DataRequired()]) email = StringField(label="Email Address:", validators=[Email(), DataRequired()]) password1 = PasswordField(label="Password:", validators=[Length(min=8, max=32), DataRequired()]) password2 = PasswordField(label="Confirm Password:", validators=[Length(min=8, max=32), EqualTo('password1'), DataRequired()]) submit = SubmitField(label="Create Account")
前端
html
:{% with messages = get_flashed_messages(with_categories=true) %} {% if messages %} {% for category, message in messages %} <div class="alert alert-{{ category }}"> {{ message }} </div> {% endfor %} {% endif %} {% endwith %}
实现效果:
10.用户认证
前提条件
# flask-bcrypt 用户密码加密存储
pip install flask_bcrypt -i https://pypi.tuna.tsinghua.edu.cn/simple/
# flask提供的用户登录方法
pip install flask_login -i https://pypi.tuna.tsinghua.edu.cn/simple/
10.1 用户注册
后端方法:
@app.route("/register", methods=["GET", "POST"])
def register_page():
form = RegisterForm()
if form.validate_on_submit():
# 创建用户
user_to_create = User(username=form.username.data,
email_address=form.email.data,
password=form.password1.data)
db.session.add(user_to_create)
db.session.commit()
print("用户{username}创建成功".format(username=form.username.data))
return redirect(url_for('login_page'))
if form.errors != {}:
for err_msg in form.errors.values():
print("{}".format(err_msg))
flash(f"There was an error with creating account:{err_msg}", category="danger")
return render_template("register.html", form=form)
模型方法:
# @property 获取用户密码属性
# @password.setter 设置用户密码
from market import bcrypt
class User(db.Model, UserMixin):
id = db.Column(db.Integer(), primary_key=True)
username = db.Column(db.String(length=30), nullable=False, unique=True)
email_address = db.Column(db.String(length=50), nullable=False, unique=True)
password_hash = db.Column(db.String(length=60), nullable=False)
budget = db.Column(db.Integer(), nullable=False, default=1000)
items = db.relationship('Item', backref='owned_user', lazy=True)
@property
def password(self):
return self.password
@password.setter
def password(self, plain_text_password):
self.password_hash = bcrypt.generate_password_hash(plain_text_password).decode('utf-8')
前端页面:
{% extends "base.html" %}
{% block title%}
Register Page
{% endblock %}
{% block content%}
<body>
<div class="container">
<form method="POST" class="form-register" style="color:white">
{{ form.hidden_tag() }}
<img class="mb-4" src="{{ url_for('static', filename='img/euansu.png') }}" style="width: 100px;height: 100px;margin-top: 30px;"/>
<h1 class="h3 mb-3 font-weight-normal">
Please Create your Account
</h1>
{{ form.username.label() }}
{{ form.username(class="form-control", placeholder="User Name") }}
{{ form.email.label() }}
{{ form.email(class="form-control", placeholder="Email Address") }}
{{ form.password1.label() }}
{{ form.password1(class="form-control", placeholder="Password") }}
{{ form.password2.label() }}
{{ form.password2(class="form-control", placeholder="Confirm Password") }}
<br />
{{form.submit(class="btn btn-lg btn-block btn-primary")}}
</form>
</div>
</body>
{% endblock %}
页面实现:
用户注册,成功后跳转至登录页面
10.2 用户登录
后端方法:
from market.forms import LoginForm
from flask_login import login_user, login_required
# 需要登录后才能访问的页面,添加 login_required 装饰器
@app.route("/market")
@login_required
def market_page():
items = Item.query.all()
return render_template("market.html", items=items)
@app.route('/login', methods=['GET', 'POST'])
def login_page():
form = LoginForm()
if form.validate_on_submit():
attempted_user = User.query.filter_by(username=form.username.data).first()
if attempted_user and attempted_user.check_password_correction(attempted_password=form.password.data):
login_user(attempted_user)
flash(f'Success! You are logged in as: {attempted_user.username}', category='success')
return redirect(url_for('market_page'))
else:
flash('Username and password are not match! Please try again', category='danger')
return render_template('login.html', form=form)
# 这里用到了一个表单,需要实现在forms.py,也即你自己的表单文件中进行定义
from flask_wtf import FlaskForm
from wtforms import StringField, PasswordField, SubmitField
from wtforms.validators import Length, EqualTo, Email, DataRequired
class LoginForm(FlaskForm):
username = StringField(label='User Name:', validators=[DataRequired()])
password = PasswordField(label='Password:', validators=[DataRequired()])
submit = SubmitField(label='Sign in')
模型文件也需要队用户对象做一些修改,否则直接使用 flask
自带的 login_user
会报如下的错误:
from market import db,login_manager
from market import bcrypt
from flask_login import UserMixin
# 添加如下内容
@login_manager.user_loader
def load_user(user_id):
return User.query.get(int(user_id))
# User对象需要继承UserMixin类
class User(db.Model,UserMixin):
id = db.Column(db.Integer(), primary_key=True)
username = db.Column(db.String(length=30), nullable=False, unique=True)
email_address = db.Column(db.String(length=50), nullable=False, unique=True)
password_hash = db.Column(db.String(length=60), nullable=False)
budget = db.Column(db.Integer(), nullable=False, default=1000)
items = db.relationship('Item', backref='owned_user', lazy=True)
@property
def password(self):
return self.password
@password.setter
def password(self, plain_text_password):
self.password_hash = bcrypt.generate_password_hash(plain_text_password).decode('utf-8')
# 这里是用来判断用户输入的密码是否正确
def check_password_correction(self, attempted_password):
return bcrypt.check_password_hash(self.password_hash, attempted_password)
前端文件:
{% extends 'base.html' %}
{% block title %}
Login Page
{% endblock %}
{% block content %}
<body>
<div class="container">
<form method="POST" class="form-signin" style="color:white">
{{ form.hidden_tag() }}
<img class="mb-4" src="{{ url_for('static', filename='img/euansu.png') }}" style="width: 100px;height: 100px;margin-top: 30px;"/>
<h1 class="h3 mb-3 font-weight-normal">
Please Login
</h1>
<br>
{{ form.username.label() }}
{{ form.username(class="form-control", placeholder="User Name") }}
{{ form.password.label() }}
{{ form.password(class="form-control", placeholder="Password") }}
<br>
<div class="checkbox mb-3">
<h6>Do not have an account?</h6>
<a class="btn btn-sm btn-secondary" href="{{ url_for('register_page') }}">Register</a>
</div>
{{ form.submit(class="btn btn-lg btn-block btn-primary") }}
</form>
</div>
</body>
{% endblock %}
实现效果:
输入密码错误,页面报错。
输入密码正确,跳转进入 market
页面。
直接访问 market
页面报错,提示缺少所需要的用户信息。
10.3 注销登录
后端方法:
from flask_login import logout_user
@app.route('/logout')
def logout_page():
logout_user()
flash("You have been logged out!", category='info')
return redirect(url_for("home_page"))
前端页面:
原来的 header
这里有 Login
和 Register
两个方法,这里修改一下,如果用户已经登录,这里修改为 Logout
,修改后的代码如下:
{% if current_user.is_authenticated %}
<ul class="navbar-nav">
<li class="nav-item">
<a class="nav-link" style="color: lawngreen; font-weight: bold">
<i class="fas fa-coins"></i>
{{ current_user.prettier_budget }}
</a>
</li>
<li class="nav-item">
<a class="nav-link">Welcome, {{ current_user.username }}</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{{ url_for('logout_page') }}">Logout</a>
</li>
</ul>
{% else %}
<ul class="navbar-nav">
<li class="nav-item">
<a class="nav-link" href="{{ url_for('login_page') }}">Login</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{{ url_for('register_page') }}">Register</a>
</li>
</ul>
{% endif %}
实现效果:
点击 Logout
调用 logout_page
方法,效果如下:
11.项目部署
11.1 Gunicorn
Gunicorn 是一个纯 Python WSGI 服务器,配置简单,多工作者实现,方便 性能调优。
- 它倾向于与主机平台轻松集成。
- 它不支持 Windows (但可以在 WSL 上运行)。
- 它很容易安装,因为不需要额外的依赖或编译。
- 它有内置的异步工作者,支持使用 gevent 或 eventlet。
创建虚拟环境:
python3 -m venv .venv
# 激活虚拟环境
source .venv/bin/activate
# 升级pip工具
python3 -m pip install --upgrade pip
#安装需要的第三方包
pip install -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple/
运行项目:
python run.py
页面访问:
安装 gunicorn
第三方包:
pip install gunicorn
启动项目
python3 -m gunicorn market:app -b 0.0.0.0:5000
# 后台启动
nohup python3 -m gunicorn market:app -b 0.0.0.0:5000 >> gunicorn.log 2>&1 &
页面能够正常访问
11.2 Waitress
Waitress 是一个纯 Python 的 WSGI 服务器。
- 它很容易配置。
- 它直接支持 Windows 。
- 它很容易安装,因为它不需要额外的依赖或编译。
- 它不支持流式请求,完整的请求数据总是被缓冲。
- 它使用一个具有多个线程工作者的单一进程。
虚拟环境已经相关依赖的安装同 Gunicorn
章节,这里就不再重复。
安装 waitress
:
pip3 install waitress -i https://pypi.tuna.tsinghua.edu.cn/simple/
使用 waitress
启动项目:
waitress-serve --host 0.0.0.0 --port 5000 market:app
后台启动:
# 后台启动
nohup waitress-serve --host 0.0.0.0 --port 5000 market:app >> gunicorn.log 2>&1 &
页面能够正常登录访问:
11.3 uWSGI
uWSGI 是一个快速、编译的服务器套件,相较于基本的服务器具有更广泛的 配置和功能。
- 由于是编译过的程序,它具有很好的性能。
- 相较于基本的应用,它的配置很复杂,有很多的选项。因此,对于新手来 来说,上手是有难度的。
- 它不支持 Windows (但可以在 WSL 上运行)。
- 在某些情况下,它需要一个编译器来安装。
虚拟环境已经相关依赖的安装同 Gunicorn
章节,这里就不再重复。
安装 uwsgi
:
pip3 install pyuwsgi -i https://pypi.tuna.tsinghua.edu.cn/simple/
使用 uwsgi
启动项目:
uwsgi --http 0.0.0.0:5000 --master -p 4 -w market:app
后台启动:
nohup uwsgi --http 0.0.0.0:5000 --master -p 4 -w market:app >> gunicorn.log 2>&1 &
前台能够正常访问页面: