Skip to content

Flask框架教程

7732字约26分钟

PythonFlask

2024-03-26

Flask Course

1.前提准备

  • Python版本

    # python 3.8.0
    # 查看Python版本
    python --version

    image-20240320135732074

  • 安装第三方 Flask

    pip install flask
    # 如果安装失败,可以使用 -i,指定使用国内镜像源
    # 清华镜像源:https://pypi.tuna.tsinghua.edu.cn/simple/
  • 检查 Flask 是否安装成功

    flask --version

    image-20240320134300965

  • Flask官网

    # 官网:https://flask.palletsprojects.com
    # 快速开始:https://flask.palletsprojects.com/en/3.0.x/quickstart/

2.一个简单的Flask程序

  1. 创建 Flask 项目目录。

    mkdir FlaskMarket
  2. 创建 app 文件。

    from flask import Flask
    
    app = Flask(__name__)
    
    @app.route("/")
    def hello_world():
        return "<p>Hello, World!</p>"
  3. 运行 Flask

    flask --app market run

    image-20240320135533967

    # 设置环境变量,也能够直接运行flask
    $env:FLASK_APP="market.py"
    $env:FLASK_APP="market"
    flask run

    image-20240320141117984

    查看web页面

    image-20240320135445474

    Debug 模式

    # 运行flask项目时,在最后加--debug,以debug模式启动
    $env:FLASK_APP="market.py"
    flask run --debug

    image-20240320141945417

    以下是代码产生报错的截图

    image-20240320141909237

  4. 新增一个路由。

    # 路由传参username
    @app.route("/about/<username>")
    def about_page(username):
        return f"<h1>this is about {username} page</h1>"

    页面查询结果

    image-20240320171713170

3.Template模板文件

可以在Flask项目的目录下创建 templates 目录存放所会用的 html 文件,具体如下:

image-20240321162208069

在Python代码中,直接返回 html 文件即可,不需要携带目录。

@app.route("/")
def hello_world():
    return render_template("hello.html")

页面访问如下

image-20240321162633131

4.数据发送到template

Jinjia2 是一个仿照 Django 模板的 Python 模板语句,实现了后端与模板之间的交互。

  1. 一个简单的数据交互。

    后端 python 这样写:

    @app.route("/")
    def hello_world():
        return render_template("home.html", item_name="Phone")

    对应的前端 html 文件需要使用 jiajia2 的语法接收变量,代码如下:

    <p>{{item_name}}</p>

    页面效果如下:

    image-20240321164324347

  2. 列表数据交互。

    后端 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>

    访问页面如下:

    image-20240321165344155

5.Template 继承

开发的网站可能涉及多个页面,需要抽取公共的内容,其余的 html 页面继承这些公共内容即可。

  1. 引入 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>
  2. 清空 home.html 原文件,修改为如下内容:

    {% extends "base.html" %}
  3. 访问页面如下:

    image-20240321171827660

    这里有一个问题就是页面标题显示为 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 的内容被替换。

    image-20240321172151144

  4. 替换 html 文件 body 下的内容:

    首先是修改 base.htmlbody 的内容,修改如下:

    {% 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 %}

    访问页面,能够正常对数据进行渲染。

    image-20240321173411271

  5. 页面跳转

    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)

    再次点击页面的按钮,能够正常进行路由跳转。

    image-20240321174224280

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)

需要在 Flaskapp 文件中,添加数据库初始化操作。

with app.app_context():
    db.create_all()

使用可视化工具查看 SQLite 本地数据库文件,出现初始化的 Item 表。

image-20240321230051440

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()

image-20240321231756582

执行如上语句后,数据库中出现一条手机记录。

image-20240321231830519

查询数据库记录

# 全量查询
result = Item.query.all()
print(result)
for item in result:
    print(item.name)

image-20240321232721451

# 根据条件过滤
result = Item.query.filter_by(name='OPPO Find X6 Pro')
print(result)
print('=============')
for item in result:
    print(item.name)

image-20240321233251288

修改数据库记录

result = Item.query.filter_by(name='OPPO Find X6 Pro')
if result:
    item = result[0]
    item.price = 5999
    db.session.commit()

修改后,数据库中的记录发生了变化。

image-20240321233607423

删除数据库记录

# 查询要删除的记录
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)

页面就能够直接展示数据库中的记录

image-20240321233938615

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)

再次启动项目:

image-20240322002147804

页面能够正常访问

image-20240322002211964

8.数据库模型

Flask 中也能够使用类似于 Django 中的 ORM,通过 PythonSQLAlchemy 第三方库实现。

需要安装的第三方包:

# 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

image-20240323213144762

执行如下步骤:

# windows环境设置FLASK_APP
$env:FLASK_APP="run.py"
flask db init

image-20240323213719678

执行后,在项目目录下生成了 migrations 目录。

image-20240323215002293

生成迁移文件

flask db migrate

执行这一步,有如下报错产生:

image-20240323215652809

跟踪代码,发现其中一个地方 current_app.extensions['migrate'].db.get_engine() 但是这的 current_app.extensions['migrate'].dbNone,因此产生了报错。

image-20240323220307968

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 下生成了迁移文件。

image-20240323225247336

执行迁移文件的升级操作:

flask db upgrade

image-20240323225531928

打开数据库工具,数据库已经迁移成功。

image-20240323225517533

执行数据库降级操作,撤销该次的升级操作。

flask db downgrade

image-20240323225706473

打开数据库,本次迁移创建的数据表已撤销。

image-20240323225721435

总结:

  1. 数据库的模型迁移操作涉及的基本包有:

    # ORM
    pip install flask-sqlalchemy
    # 数据迁移
    pip install flask-migrate
  2. 数据库的模型迁移需要创建的代码有:

    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)
  3. 数据库模型的迁移操作:

    # 设置FLASK_APP环境变量
    $env:FLASK_APP="run.py"
    # 初始化迁移目录,仅需一次操作
    flask db init
    # 生成迁移文件
    flask db migrate
    # 执行迁移操作
    flask db upgrade
    # 撤回迁移操作
    flask db downgrade

8.2 数据模型关系

8.2.1 一对多

image-20240322220941329

如上所示,一个作者关联多个文章,暂时认定,一篇文章只能有一个作者。

作者以及文章的类定义如下所示:

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 一对多的建立步骤

现在需要在数据库中,将作者和文章的关系关联成一对多的关系,具体操作如下:

  1. 定义外键

    外键(Foreign key) 用来在 B 表存储 A 表的主键值,作为与 A 表的关系字段。

    由于外键只能够存储单一数据,所以外键常在 “多” 的一侧定义,一个作者对应多个文章,因此需要在文章模型中添加作者的关系字段,记录作者的主键值,代码如下:

    class Article(db.Model):
        ...
        author_id = db.Column(db.Integer, db.ForeignKey('author.id'))
  2. 定义关系属性

    关系属性的定义主要是用来标记该类与那个类建立了关系,常常在 “一” 的一侧进行定义,关系属性能够返回多个记录,也称之为集合关系属性。

    在作者和文章的关系中,就需要在作者一侧定义关系属性,代码如下:

    class Author(db.Model):
        ...
        articles = db.relationship('Article')
  3. 创建表

    # 我在这里是通过python直接创建表,因此使用了app.app_context()这个方法,这个方法主要是用来引入flask的各种方法,否则操作会产生报错
    with app.app_context():
    	# 将所有的模型文件创建为表
    	db.create_all()
    	# 删除数据库中所有的表
    	db.drop_all()

    image-20240322220410572

  4. 建立关系

    建立关系这里指的是,将两张表的数据进行关系,主要有以下两种方式:外键字段赋值、关系属性赋值。

    这里我们先准备几组数据,用来操作实现关系的建立。

    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()

    image-20240322223608157

    执行如上操作后,查看数据库,正常插入了数据。

    image-20240322223636703

    image-20240322223658415

    这里需要注意,关系属性虽然在作者模型中,但并未实际在表中创建字段,接下来通过如下代码对数据表中的数据建立一对多关系:

    # 外键字段赋值
    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()

    image-20240322224417268

    执行完成后,查看数据库,数据表 author_id 的值为关联作者的主键值。

    image-20240322224511610

    # 执行完外键关系赋值后,可以通过如下调用,查询作者余华关联的图书
    with app.app_context():                              
    	author = Author.query.filter_by(name='余华').first() 
        print(author.articles)

    image-20240322224801968

    # 操作关系属性
    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()

    image-20240322225505285

    执行完成后,查看数据库,数据表 author_id 的值为关联作者的主键值。

    image-20240322225612156

    # 执行完外键关系赋值后,可以通过如下调用,查询作者余华关联的图书
    with app.app_context():                              
    	author = Author.query.filter_by(name='余华').first() 
        print(author.articles)

    image-20240322225709098

因此,综上步骤,我们通过Flask建立两个表之间的一对多关系时,需要通过以下三个步骤:

  1. 定义外键,需要在 “多” 侧表的模型中增加外键字段。
  2. 定义关系属性,需要在 “一” 侧表的模型中定义关系属性,该属性并不体现在实际的表中。
  3. 建立关系,通过指定外键字段或操作关系属性,能够建立两个表之间的一对多关系属性。
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)

image-20240322231221735

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)

能够正常获取其关系的对象。

image-20240322231806740

使用 backref 非常方便,但通常来说 “显式好过隐式”,所以我们应该尽量使用 back_populates 定义双向关系。

8.2.2 多对一

image-20240322232219116

一对多的关系反过来就是多对一,这两种关系模型分别从不同的视角出发。

在一对多中,我们提交以下两点:

  1. 定义外键,需要在 “多” 侧表的模型中增加外键字段。
  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 一对一

image-20240322232902788

一对一关系是在一对多关系的基础上转化而来,只要确保两侧的关联关系唯一即可保证一对多关系转系转化为了一对一关系,在定义时,设置关系属性的 uselistFlase ,此时的一对多关系转化为一对一关系。

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()

image-20240323235556768

查看数据库,正常写入:

image-20240323235721670

# 操作关系属性
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 多对多

image-20240322234639848

多对多关系中,需要建立一个关联表,关联表并不存在数据,只用来存储两侧模型外键的对应关系。

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

image-20240324000617731

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 文件,项目结构以及表单组件的代码如下所示:

image-20240324231531596

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 %}

实现效果如下图所示:

image-20240324231803071

这里的 img 是我本地的图片,也可以替换成你自己的,按照如下步骤操作即可:

  1. Flask 项目的 __init__py 文件中,配置静态文件的路径:

    app.static_folder = 'static'
  2. 在项目下创建 static 目录,目录结构如下所示。

    image-20240324232027659

  3. 访问路由,查看是否能够使用。

    image-20240324232133091

  4. 在前端 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)

页面输入表单信息进行登录:

image-20240324234308134

输入完成后跳转路由至用户家目录:

image-20240324234236394

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")

页面验证:

image-20240324235125077

这里可能有报错,如下所示:

image-20240324235551445

验证邮箱需要单独安装 email_validator,如下所示:

pip install email_validator
pip install email_validator -i https://pypi.tuna.tsinghua.edu.cn/simple/

image-20240324235717796

再次校验,就能够正常输出不通过校验的信息。

image-20240325100016041

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()

image-20240325132911766

如下是一个 flash() 函数的使用示例:

  1. 后端函数:

    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")
  2. 前端 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 %}
  3. 实现效果:

    image-20240325133312915

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 %}

页面实现:

image-20240326133613385

用户注册,成功后跳转至登录页面

image-20240326133656722

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 会报如下的错误:

image-20240326134633109

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 %}

实现效果:

输入密码错误,页面报错。

image-20240326134904534

输入密码正确,跳转进入 market 页面。

image-20240326134943193

直接访问 market 页面报错,提示缺少所需要的用户信息。

image-20240326135434170

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"))

前端页面:

image-20240326135852862

原来的 header 这里有 LoginRegister 两个方法,这里修改一下,如果用户已经登录,这里修改为 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 %}

实现效果:

image-20240326140053491

点击 Logout 调用 logout_page 方法,效果如下:

image-20240326140129945

11.项目部署

11.1 Gunicorn

Gunicorn 是一个纯 Python WSGI 服务器,配置简单,多工作者实现,方便 性能调优。

  • 它倾向于与主机平台轻松集成。
  • 它不支持 Windows (但可以在 WSL 上运行)。
  • 它很容易安装,因为不需要额外的依赖或编译。
  • 它有内置的异步工作者,支持使用 gevent 或 eventlet。

创建虚拟环境:

python3 -m venv .venv

image-20240326142910049

# 激活虚拟环境
source .venv/bin/activate

image-20240326155857690

# 升级pip工具
python3 -m pip install --upgrade pip

image-20240326161223609

#安装需要的第三方包
pip install -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple/

image-20240326161033294

运行项目:

python run.py

image-20240326162726032

页面访问:

image-20240326162812677

安装 gunicorn 第三方包:

pip install gunicorn

image-20240326170704605

启动项目

python3 -m gunicorn market:app -b 0.0.0.0:5000

image-20240326170613650

# 后台启动
nohup python3 -m gunicorn market:app -b 0.0.0.0:5000 >> gunicorn.log 2>&1 &

页面能够正常访问

image-20240326170954665

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 &

页面能够正常登录访问:

image-20240326171928601

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

image-20240326172359596

后台启动:

nohup uwsgi --http 0.0.0.0:5000 --master -p 4 -w market:app >> gunicorn.log 2>&1 &

image-20240326172539420

前台能够正常访问页面:

image-20240326172603243