Contents

Flask——黑马教程

参考资料

概述

Web 与 Web 框架基础

World Wide Web 诞生的最初目的:利用互联网交流工作文档。其基本工作模式为:客户端向服务器发送请求,服务器返回响应。

Web 框架是协助开发者快速开发 Web 应用程序的一套功能代码。成熟稳健的 Web 框架省去了程序员在安全性、数据流控制等方面的处理,使开发人员可以把精力放在具体的业务逻辑上,从而降低开发难度,提高开发效率。

Flask 核心特性

Flask 的核心包含两个关键组件:

  • WSGI(Web Server Gateway Interface)工具箱采用 Werkzeug,提供路由、调试和 Web 服务器网关接口功能
  • 模板引擎使用 Jinja2

常用扩展包

扩展包 用途
Flask-SQLalchemy 操作数据库
Flask-migrate 管理数据库迁移
Flask-Session 指定 Session 存储方式
Flask-WTF 表单处理
Flask-Mail 邮件功能
Flask-Login 认证用户状态
Flask-RESTful 开发 REST API

环境搭建

创建虚拟环境

使用 venv 或 virtualenv 创建独立的 Python 运行环境:

1
python -m venv testyun

激活虚拟环境后即可在其中安装 Flask 及相关依赖包。

视图与路由

第一个 Flask 程序

(基础示例:hello world)

Flask 应用配置

初始化参数

创建 Flask 应用实例时,可传入以下参数:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
app = Flask(
    # Flask 程序所在的包(模块),传 __name__ 即可
    # 决定 Flask 在访问静态文件时查找的路径
    import_name=__name__,

    # 静态文件访问路径,默认值为:/ + static_folder
    static_url_path=None,

    # 静态文件存储的文件夹,默认为 'static'
    static_folder='static',

    # 模板文件存储的文件夹,默认为 'templates'
    template_folder='templates'
)

配置加载方式

在 Flask 程序运行的时候,可以通过以下方式设置配置:

方式一:从配置对象中加载(常用)

1
2
3
4
5
6
# 定义配置类
class Config:
    DEBUG = True

app = Flask(__name__)
app.config.from_object(Config)

方式二:从配置文件中加载

首先创建配置文件 config.ini

1
DEBUG = True

然后加载配置:

1
2
app = Flask(__name__)
app.config.from_pyfile('config.ini')

方式三:从环境变量中加载(了解)

1
2
# 环境变量 FLASKCONFIG 指向配置文件路径
app.config.from_envvar('FLASKCONFIG')

DEBUG(调试模式)特性:

  • 代码修改后自动重启服务器
  • 服务器出错时将错误信息抛出示

配置读取

通过 app.config.get() 方法读取配置值:

1
app.config.get('DEBUG')

路由基础

基本路由定义

1
2
3
@app.route('/demo1')
def hello():
    return 'hello world'

路由传参

将同一类 URL 映射到同一个视图函数处理。例如向路由添加用户 ID,从而使用同一个视图函数来显示不同用户的个人信息:

1
2
3
@app.route('/user/<int:user_id>')
def user_info(user_id):
    return f'hello {user_id}'

路由传递的参数默认当作字符串处理,也可以自行指定类型。上述代码中指定了参数 user_id 的类型为整数。

请求方式

默认请求方式是 GET。如果需要处理多种请求方式,使用 methods 参数:

1
2
3
4
@app.route('/demo2', methods=['GET', 'POST'])
def demo2():
    tmp = request.method
    return tmp

API 测试工具

PostMan 等工具用于网页测试和发送 HTTP 请求,便于对后端所写的路由和视图函数进行调试。

视图函数响应

返回 JSON

使用 jsonify 生成符合 HTTP 协议规范的 JSON 响应:

1
2
3
4
5
6
7
@app.route('/demo4')
def demo4():
    json_dict = {
        "user_id": 10,
        "user_name": "Tom"
    }
    return jsonify(json_dict)

不推荐使用 json.dumps 直接返回,因为 jsonify 会自动设置 content-type: application/json,确保返回数据符合 HTTP 协议规范。

url_for 反向路由

url_for 根据传入的视图函数名,返回该路由对应的 URL:

1
url_for('hello')  # 返回 /demo1

重定向

1
2
3
@app.route('/demo5')
def demo5():
    return redirect('http://www.baidu.com')

也可以重定向到其他视图函数:

1
redirect(url_for('user_info', user_id=100))

自定义状态码

返回元组形式可以自定义状态码:

1
2
3
@app.route('/demo6')
def demo6():
    return '状态码为 666', 666

正则匹配路由

通过正则表达式限定请求参数,用于限制用户访问规则的场景:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
from werkzeug.routing import BaseConverter

# 自定义正则转换器
class RegexConverter(BaseConverter):
    def __init__(self, url_map, *args):
        super(RegexConverter, self).__init__(url_map)
        self.regex = args[0]

app = Flask(__name__)
app.url_map.converters['re'] = RegexConverter

# 使用转换器实现自定义匹配规则:3 位数字
@app.route('/user/<re("[0-9]{3}"):user_id>')
def user_info(user_id):
    return f'hello {user_id}'

测试结果:

  • http://127.0.0.1:5000/user/123 访问成功
  • http://127.0.0.1:5000/user/12345 提示找不到页面

Flask 内置的转换器:

1
2
3
4
5
6
7
8
9
DEFAULT_CONVERTERS = {
    'int': IntegerConverter,
    'default': UnicodeConverter,
    'string': UnicodeConverter,
    'any': AnyConverter,
    'path': PathConverter,
    'float': FloatConverter,
    'uuid': UUIDConverter,
}

异常处理

abort 主动抛出异常

使用 abort 方法主动抛出 HTTP 异常,但只能抛出 HTTP 协议的错误状态码:

1
abort(404)  # 页面未找到

捕获异常

通过装饰器捕获 HTTP 状态码或指定异常类型:

1
2
3
4
5
6
7
8
9
# 捕获 500 状态码
@app.errorhandler(500)
def internal_server_error(e):
    return '服务器搬家了'

# 捕获 Python 异常
@app.errorhandler(ZeroDivisionError)
def zero_division_error(e):
    return '除数不能为0'

请求钩子

在请求开始前或结束后执行一些通用操作,如建立数据库连接、进行权限校验、指定数据交互格式等。Flask 提供了四个请求钩子:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
from flask import Flask, abort

app = Flask(__name__)

# 在第一次请求之前调用,可在此进行初始化操作
@app.before_first_request
def before_first_request():
    print("before_first_request")

# 在每一次请求之前调用,可在此进行请求校验
# 如果校验失败,直接 return 则不会执行视图函数
@app.before_request
def before_request():
    print("before_request")

# 在执行完视图函数之后调用,可对响应做统一处理
@app.after_request
def after_request(response):
    print("after_request")
    response.headers["Content-Type"] = "application/json"
    return response

# 在每一次请求之后调用,会接收服务器的错误信息参数
@app.teardown_request
def teardown_request(e):
    print("teardown_request")

request 对象

request 对象代表了当前的请求,包含以下常用属性:

属性 说明
request.data 记录请求的数据,转换为字符串
request.form 请求的表单数据
request.args 请求的查询参数
request.cookies 请求的 cookie 信息
request.headers 请求的报文头
request.method 请求使用的 HTTP 方法
request.url 请求的 URL 地址
request.files 请求上传的文件
request.get_json() 获取 JSON 格式的请求数据

状态保持

无状态协议与状态保持

HTTP 是一种无状态协议,浏览器请求服务器是无状态的。这意味着用户请求时,浏览器和服务器无法知道该用户之前做过什么,每次请求对服务器来说都是一次新的请求。

无状态的原因在于:浏览器与服务器使用 socket 套接字进行通信,服务器返回响应后会关闭当前 socket 连接,且服务器处理完页面后会销毁页面对象。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
有状态示例:
C:你今天中午吃的啥?
S:吃的大盘鸡。
C:味道怎么样呀?
S:还不错,挺好吃的。

无状态示例:
C:你今天中午吃的啥?
S:吃的大盘鸡。
C:味道怎么样呀?
S:啊?啥味道怎么样?

使用 Cookie 实现状态保持:
C:你今天中午吃的啥?
S:吃的大盘鸡。
C:你今天中午吃的大盘鸡味道怎么样呀?
S:还不错,挺好吃的。

实现状态保持主要有两种方式:

  • 在客户端使用 Cookie 存储信息
  • 在服务器使用 Session 端存储信息

Cookie 最早是网景公司的前雇员 Lou Montulli 在 1993 年 3 月发明的。

Cookie 由服务器端生成,发送给客户端浏览器(通常经过加密)。它是一段纯文本信息,浏览器会将 Cookie 的 key/value 保存,下次请求同一网站时发送该 Cookie 给服务器。

Cookie 基于域名安全,不同域名的 Cookie 不能相互访问。例如访问淘宝时写入的 Cookie,访问百度时无法获取。

Cookie 的典型应用场景:

  1. 用户身份识别:保留用户信息,简化登录手续
  2. 广告推送:保留浏览过的商品信息,推送类似商品
  3. 购物车:保存商品信息到 Cookie,付款时提取

设置 Cookie:

1
2
response.set_cookie('username', 'Tom', max_age=3600)
# 在 3600 秒后过期,可在浏览器开发者工具的网络面板中查看 set-Cookie 响应头

读取 Cookie:

1
request.cookies.get('username')

Session

对于敏感重要的信息(如用户余额、验证码等),建议存储在服务器端。Session 依赖于 Cookie:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
from flask import session

app.secret_key = 'your secret'  # 作为加密算法的参数

@app.route('/index1')
def index1():
    session['username'] = 'itcast'
    return redirect(url_for('index'))

@app.route('/')
def index():
    return session.get('username')

app.secret_key 是通用密钥,用于对 Session 数据进行签名加密。一般存储在系统环境变量中,不暴露在程序代码里。

上下文

上下文相当于一个容器,保存了 Flask 程序运行过程中的一些信息。

请求上下文

requestsession 称为请求上下文对象,保存了客户端和服务器交互的数据:

  • request:保存当前请求的相关数据,包括请求地址、请求方式、Cookie 等
  • session:记录请求会话中的用户信息

应用上下文

应用上下文会随着 request 出现和消失,主要用于帮助 request 获取当前的应用信息:

  • current_app:获取启动参数、配置、数据库连接、运行机器 IP 等信息
    1
    2
    
    current_app.name
    current_app.test_value = 'value'
    
  • g:保存当前请求的全局变量,不同请求有不同的全局变量

模板

Jinja2 模板引擎

视图函数的主要用途是处理业务逻辑和返回响应内容。为了将二者分离、减少代码复杂度,使用模板来承担返回响应内容的任务。

模板是一个文件,其中的变量通过占位符表示。模板引擎使用视图函数的值来替换占位符,得到最终字符串,这个过程称为渲染。Flask 中的模板引擎是 Jinja2,通过 render_template 函数进行封装。

模板语法

变量引用

双括号 {{ }} 表示变量名:

1
2
3
4
<h1>{{ post.title }}</h1>

{{ your_dict['key'] }} {{ your_list[0] }}
<!-- 只要双括号中的元素可以被 str() 转换即可 -->

控制代码块

使用 {% %} 包裹控制逻辑:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
<ul>
  {% for index in indexs %}
  <li>{{ index }}</li>
  {% endfor %}
</ul>

{% if user %}
<p>{{ user }}</p>
{% else %}
<p>hello!</p>
{% endif %}

注释

使用 {# #} 包裹注释内容,注释不会被渲染:

1
{# {{ name }} #}

模板使用流程

项目目录下创建 templates 文件夹存放模板文件:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
<!-- templates/temp_demo1.html -->
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <title>Title</title>
  </head>
  <body>
    我的模板 html 内容
    <br />{{ my_str }} <br />{{ my_int + 10 }} <br />{{ my_array[1] }} <br />{{
    my_dict.age }}
  </body>
</html>

在视图函数中传递数据:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
@app.route('/')
def index():
    my_str = 'Hello'
    my_int = 10
    my_array = [3, 4, 2, 1, 7, 9]
    my_dict = {
        'name': 'Tom',
        'age': 18
    }
    return render_template('temp_demo1.html',
                           my_str=my_str,
                           my_int=my_int,
                           my_array=my_array,
                           my_dict=my_dict)

过滤器

在模板中不能直接调用 Python 中的方法。如果需要对变量进行修改(如格式化或运算),需要使用过滤器:

1
2
<!-- 链式调用:先反转字符串再转大写 -->
{{ "hello world" | reverse | upper }}

常用内置过滤器

用途 过滤器 示例
禁用转义 safe {{ '<em>hello</em>' | safe }}
渲染前删除 HTML 标签 striptags {{ '<em>hello</em>' | striptags }}
首字母大写,其余小写 capitalize
转小写 lower
转大写 upper
每个单词首字母大写 title
字符串反转 reverse
格式化输出 format {{ '%s is %d' | format('name', 17) }}
取列表第一个元素 first {{ [1,2,3,4,5,6] | first }}
取列表最后一个元素 last
获取列表长度 length
列表求和 sum
列表排序 sort

自定义过滤器

方式一:调用 add_template_filter 方法

1
2
3
4
5
6
def do_listreverse(li):
    temp_li = list(li)
    temp_li.reverse()
    return temp_li

app.add_template_filter(do_listreverse, 'lireverse')

方式二:使用装饰器

1
2
3
4
5
@app.template_filter('lireverse')
def do_listreverse(li):
    temp_li = list(li)
    temp_li.reverse()
    return temp_li

在模板中使用自定义过滤器:

1
{{ my_array | lireverse }}

控制代码块进阶

条件判断配合过滤器

1
2
{% if comments | length > 0 %} There are {{ comments | length }} comments {%
else %} There are no comments {% endif %}

for 循环配合 if 语句

1
2
3
4
5
6
7
<!-- 只渲染 post.text 不为空的数据 -->
{% for post in posts if post.text %}
<div>
  <h1>{{ post.title }}</h1>
  <p>{{ post.text | safe }}</p>
</div>
{% endfor %}

模板复用

当多个模板之间有相同内容时,为了避免代码冗余,可以采用以下复用方式。

将重复部分写成函数形式以供重用:

1
2
3
4
<!-- mymacro.html -->
{% macro function(name, value='', type='text') %}
<input type="{{type}}" name="{{name}}" value="{{value}}" class="form-control" />
{% endmacro %}

先导入再调用宏:

1
2
3
4
{% import 'mymacro.html' as func %} {{ func.function('username', value='tom') }}

<!-- 输出结果 -->
<input type="text" name="username" value="tom" class="form-control" />

继承

子模板继承父模板后,可以在父模板的占位区域(block)中填充自定义内容。一般用于网站的顶部菜单和底部公共内容。

注意:Flask 模板不支持多继承。

父模板 base.html

1
2
{% block top %} 顶部菜单 {% endblock top %} {% block content %} {% endblock
content %} {% block bottom %} 底部 {% endblock bottom %}

子模板继承 base.html

1
2
{% extends 'base.html' %} {% block content %} 顶部菜单和底部与父模板一致 content
区域的内容进行自定义 {% endblock content %}

包含

通过 include 将另一个模板整个加载到当前模板中并直接渲染:

1
{% include 'hello.html' ignore missing %}

如果包含的模板文件不存在,使用 ignore missing 会忽略这条 include 语句。

模板中的特殊变量和函数

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
<!-- 访问 Flask 当前的 config 对象,如数据库 URI -->
{{ config.SQLALCHEMY_DATABASE_URI }}

<!-- 获取当前请求的 request 对象信息 -->
{{ request.url }}

<!-- 获取 session 对象 -->
{{ session.new }}

<!-- 获取视图函数中设置的 g 变量的属性 -->
{{ g.name }}

<!-- 使用 url_for 生成 URL,避免模板中硬编码链接 -->
{{ url_for('article', post_id=1) }}

<!-- 获取闪现消息 -->
{% for message in get_flashed_messages() %} {{ message }} {% endfor %}

Web 表单

表单基础

Web 表单是 HTML 页面中负责数据采集的部件,允许用户输入数据并提交给服务器。

Flask-WTF 扩展

Flask-WTF 封装了 WTForms,提供了验证表单数据的功能。使用前需要配置 SECRET_KEY

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
from flask import Flask, render_template, flash
from flask_wtf import FlaskForm
from wtforms import SubmitField, StringField, PasswordField
from wtforms.validators import DataRequired, EqualTo

app = Flask(__name__)
app.config['SECRET_KEY'] = 'SECRET_KEY'

# 自定义注册表单类
class RegisterForm(FlaskForm):
    username = StringField(
        "用户名:",
        validators=[DataRequired("请输入用户名")],
        render_kw={"placeholder": "请输入用户名"}
    )
    password = PasswordField(
        "密码:",
        validators=[DataRequired("请输入密码")]
    )
    password2 = PasswordField(
        "确认密码:",
        validators=[DataRequired("请输入确认密码"),
                    EqualTo("password", "两次密码不一致")]
    )
    submit = SubmitField("注册")

@app.route('/demo2', methods=["get", "post"])
def demo2():
    register_form = RegisterForm()
    if register_form.validate_on_submit():
        username = request.form.get("username")
        password = request.form.get("password")
        password2 = request.form.get("password2")
        # 执行注册操作
        return "success"
    else:
        if request.method == "POST":
            flash("参数有误或者不完整")
    return render_template('temp_register.html', form=register_form)

if __name__ == '__main__':
    app.run(debug=True)

对应的模板文件:

1
2
3
4
5
6
<form method="post">
  {{ form.csrf_token() }} {{ form.username.label }} {{ form.username }}<br />
  {{ form.password.label }} {{ form.password }}<br />
  {{ form.password2.label }} {{ form.password2 }}<br />
  {{ form.submit }}
</form>

常用验证器说明:

  • DataRequired:确保字段中有数据
  • EqualTo:常用于比较两次密码输入

CSRF 防护

什么是 CSRF

跨站请求伪造(Cross Site Request Forgery)是指攻击者盗用用户身份,以用户名义发送恶意请求,包括发送邮件、发消息、盗取账号、购买商品、虚拟货币转账等,造成个人隐私泄露和财产安全威胁。

CSRF 攻击示例:

  1. 用户 C 在购物网站 A 登录,服务器在浏览器生成 Cookie
  2. 用户 C 在未登出 A 的情况下访问伪装网站 B
  3. B 中有诱导点击(如领取优惠券),触发向 A 发送请求
  4. 浏览器携带 A 的 Cookie 访问 A
  5. A 的服务器无法区分请求来源,可能成功执行恶意操作

Flask 的 CSRF 防护机制

设置 app.secret_key 用于加密生成 csrf_token 值,然后在表单中添加 {{ form.csrf_token() }} 即可:

1
2
3
4
5
6
<form method="post">
  {{ form.csrf_token() }} {{ form.username.label }} {{ form.username }}<br />
  {{ form.password.label }} {{ form.password }}<br />
  {{ form.password2.label }} {{ form.password2 }}<br />
  {{ form.submit }}
</form>

csrf_token 校验流程

后端在渲染页面前:

  1. 生成 csrf_token 值,传入模板:response = make_response(render_template('???.html', csrf_token=csrf_token))
  2. 设置到 Cookie 中:response.set_cookie('csrf_token', csrf_token)

前端表单中添加隐藏字段:

1
<input type="hidden" name="csrf_token" value="{{ csrf_token }}" />

后端响应请求前比较:

  • 前端传来的 csrf_token
  • 浏览器 Cookie 中的 csrf_token

若二者一致,说明是正常请求;若不一致,说明请求来自别处。原理是利用浏览器的同源策略,网站之间不能获取对方的 Cookie。

数据库

ORM 概念

ORM(Object-Relation Mapping,对象-关系映射)主要实现模型对象到关系数据库的数据映射。

优点:

  • 面向对象操作:将数据库操作转换为类属性和方法调用,不需要编写 SQL 语句
  • 数据模型解耦:更换数据库只需简单配置

缺点:

  • 对象操作到 SQL 语句的转换有性能损失

SQLAlchemy 是一个关系型数据库框架,通过 Python 对象操作数据库,以一定的性能开销换取开发效率的提升。

建表时可以直接对数据库操作而不通过 Flask Model,因为 ORM 只是提供抽象层,底层仍是 SQL 语句。但在需要优化性能或处理复杂查询时,仍推荐直接操作数据库。

数据库模型定义

一对多关系

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
from sqlalchemy.orm import declarative_base, relationship
from sqlalchemy import Column, Integer, String, ForeignKey

class Base:
    __allow_unmapped__ = True
Base = declarative_base(cls=Base)

class Role(Base):
    __tablename__ = 'roles'

    id = Column(Integer, primary_key=True, autoincrement=True)
    name = Column(String(64), unique=True)
    us = relationship('User', backref='role')

    def __repr__(self):
        return f"Role: {self.name}"

class User(Base):
    __tablename__ = 'users'

    id = Column(Integer, primary_key=True, autoincrement=True)
    name = Column(String(64), unique=True, index=True)
    email = Column(String(64), unique=True)
    password = Column(String(64))
    role_id = Column(Integer, ForeignKey('roles.id'))

    def __repr__(self):
        return f"User: {self.name}"

角色和用户是一对多关系:一个角色可以有多个用户,一个用户只能属于一个角色。

多对多关系

典型场景:学生与选修课程、讲师与班级、用户与收藏内容等。需要添加一张关联表记录两张表之间的对应关系:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
from sqlalchemy.orm import declarative_base, relationship
from sqlalchemy import Column, Integer, String, Table, ForeignKey

class Base:
    __allow_unmapped__ = True
Base = declarative_base(cls=Base)

# 创建数据关联关系表
tb_student_course = Table(
    'tb_student_course',
    Column('student_id', Integer, ForeignKey('students.id')),
    Column('course_id', Integer, ForeignKey('courses.id'))
)

class Student(Base):
    __tablename__ = "students"
    id = Column(Integer, primary_key=True)
    name = Column(String(64), unique=True)
    courses = relationship('Course', secondary=tb_student_course,
                          backref='students', lazy='dynamic')

class Course(Base):
    __tablename__ = "courses"
    id = Column(Integer, primary_key=True)
    name = Column(String(64), unique=True)

# 使用示例
stu1 = Student(name='张三')
stu2 = Student(name='李四')
cou1 = Course(name='物理')
cou2 = Course(name='化学')
cou3 = Course(name='生物')

stu1.courses = [cou2, cou3]
stu2.courses = [cou1, cou2, cou3]

自关联一对多

典型场景:评论与其子评论(回复)的关系:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
class Comment(Base):
    """评论"""
    __tablename__ = "comments"

    id = Column(Integer, primary_key=True)
    content = Column(Text, nullable=False)
    parent_id = Column(Integer, ForeignKey("comments.id"))
    parent = relationship("Comment", remote_side=[id],
                          backref='children', lazy='dynamic')

# 使用示例
com1 = Comment(content='我是主评论1')
com11 = Comment(content='我是回复主评论1的子评论1')
com11.parent = com1
com12 = Comment(content='我是回复主评论1的子评论2')
com12.parent = com1

自关联多对多

典型场景:用户与关注用户的关系(微博关注功能):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
tb_user_follows = Table(
    "tb_user_follows",
    Column('follower_id', Integer, ForeignKey('info_user.id'), primary_key=True),
    Column('followed_id', Integer, ForeignKey('info_user.id'), primary_key=True)
)

class User(Base):
    __tablename__ = "info_user"

    id = Column(Integer, primary_key=True)
    name = Column(String(32), unique=True, nullable=False)
    followers = relationship('User',
                            secondary=tb_user_follows,
                            primaryjoin=id == tb_user_follows.c.followed_id,
                            secondaryjoin=id == tb_user_follows.c.follower_id,
                            backref='followed', lazy='dynamic')

数据库迁移

开发过程中修改数据库模型时,直接删除旧表会丢失数据。使用数据库迁移框架可以追踪数据库模式的变化并安全地应用更新。

Flask 中使用 Flask-Migrate 扩展实现数据迁移:

1
2
3
4
5
6
from flask_migrate import Migrate, MigrateCommand
from flask_script import Shell, Manager

migrate = Migrate(app, db)
manager = Manager(app)
manager.add_command('db', MigrateCommand)

常用迁移命令:

  • python manage.py db init:初始化迁移目录
  • python manage.py db migrate:生成迁移文件
  • python manage.py db upgrade:执行迁移

蓝图

蓝图用于对程序进行模块化处理,避免在一个 .py 文件中写入过多路由。

在 Flask 中,传统的模块化方式行不通,而其内置了 Blueprint 类用于模块化处理。Blueprint 是一个存储操作方法的容器,被注册到应用后即可被调用。它不是一个完整的应用,不能独立于应用运行。

注:如果在应用根目录的 templates 文件夹中存在与蓝图 templates 文件夹同名的文件,系统会优先使用根目录的文件。

单元测试

测试基础

Web 程序开发过程一般包括:需求分析、设计阶段、实现阶段、测试阶段。

测试阶段通过人工或自动化方式测试系统功能,目的是检验是否满足需求,弄清预期结果和实际结果之间的差别。

测试分类:

  • 单元测试:对单独的代码块(如函数)分别进行测试,保证其正确性
  • 集成测试:对大量程序单元的协同工作情况进行测试
  • 系统测试:对整个系统的正确性进行检查

单元测试由开发人员编写,其他测试由专业测试人员完成。

断言与测试框架

单元测试实际上就是一些"断言"代码,断言用于判断函数或对象方法产生的结果是否符合期望:

1
2
3
4
5
6
7
8
a = [1, 2, 3, 4, 5]
b = 3

# 表达式为真,无返回值
assert b in a

# 表达式为假,抛出 AssertionError
assert b not in a, '发生异常'

常用断言方法:

方法 说明
assertEqual 判断两个值是否相等
assertNotEqual 判断两个值是否不相等
assertTrue 判断 bool 值是否为 True
assertFalse 判断 bool 值是否为 False
assertIsNone 判断是否不存在
assertIsNotNone 判断是否存在

基础测试写法

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
# demo1.py
@app.route('/login', methods=['POST'])
def login():
    username = request.form.get('username')
    password = request.form.get('password')

    if not all([username, password]):
        return jsonify({"errcode": -2, "errmsg": "params error"})

    if username == 'itheima' and password == 'python':
        return jsonify({"errcode": 0, "errmsg": "success"})
    else:
        return jsonify({"errcode": -1, "errmsg": "wrong username or password"})
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
# test_demo1.py
import unittest
import json
from demo1 import app

class LoginTestClass(unittest.TestCase):

    def setUp(self):
        """测试开始前首先执行"""
        app.testing = True
        self.client = app.test_client()

    def tearDown(self):
        """单元测试结束后执行,可做扫尾工作"""
        pass

    def test_empty_username_password(self):
        """测试用户名与密码均为空的情况"""
        response = app.test_client().post('/login', data={})
        json_dict = json.loads(response.data)
        self.assertIn('errcode', json_dict, '数据格式返回错误')
        self.assertEqual(json_dict['errcode'], -2, '状态码返回错误')

    def test_error_username_password(self):
        """测试用户名和密码错误的情况"""
        response = app.test_client().post('/login',
                                          data={"username": "aaaaa", "password": "12343"})
        json_dict = json.loads(response.data)
        self.assertIn('errcode', json_dict, '数据格式返回错误')
        self.assertEqual(json_dict['errcode'], -1, '状态码返回错误')

if __name__ == '__main__':
    unittest.main()

注意:setUptearDown 是固定写法,测试函数名必须以 test_ 开头。

设置 app.testing=Trueapp.config['TESTING']=True 可以开启测试模式,也常用于数据库测试场景。

拓展与后续学习建议

延伸阅读方向:

  • Flask 高级特性:深入学习 Flask 扩展生态,如 Flask-RESTful、Flask-JWT 用于 API 开发与身份认证
  • 数据库进阶:学习 SQLAlchemy 的更多特性,包括查询优化、事务管理、连接池配置等
  • 前端框架集成:探索 Flask 与 React、Vue 等现代前端框架的结合使用

高阶应用场景:

  • RESTful API 设计:学习 API 架构风格,理解资源抽象、HTTP 动词、状态码规范等最佳实践
  • 微服务架构:了解 Flask 在微服务架构中的应用,结合 Docker 容器化部署
  • 性能优化:学习使用 Gunicorn、uWSGI 等 WSGI 服务器,以及缓存策略(Redis)的应用
  • 安全加固:深入学习 Web 安全,包括 SQL 注入防护、XSS 防护、HTTPS 部署、限流策略等