参考资料
概述
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 运行环境:
激活虚拟环境后即可在其中安装 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
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 协议的错误状态码:
捕获异常
通过装饰器捕获 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
Cookie 最早是网景公司的前雇员 Lou Montulli 在 1993 年 3 月发明的。
Cookie 由服务器端生成,发送给客户端浏览器(通常经过加密)。它是一段纯文本信息,浏览器会将 Cookie 的 key/value 保存,下次请求同一网站时发送该 Cookie 给服务器。
Cookie 基于域名安全,不同域名的 Cookie 不能相互访问。例如访问淘宝时写入的 Cookie,访问百度时无法获取。
Cookie 的典型应用场景:
- 用户身份识别:保留用户信息,简化登录手续
- 广告推送:保留浏览过的商品信息,推送类似商品
- 购物车:保存商品信息到 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 程序运行过程中的一些信息。
请求上下文
request 和 session 称为请求上下文对象,保存了客户端和服务器交互的数据:
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 %}
|
注释
使用 {# #} 包裹注释内容,注释不会被渲染:
模板使用流程
项目目录下创建 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 攻击示例:
- 用户 C 在购物网站 A 登录,服务器在浏览器生成 Cookie
- 用户 C 在未登出 A 的情况下访问伪装网站 B
- B 中有诱导点击(如领取优惠券),触发向 A 发送请求
- 浏览器携带 A 的 Cookie 访问 A
- 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 校验流程
后端在渲染页面前:
- 生成
csrf_token 值,传入模板:response = make_response(render_template('???.html', csrf_token=csrf_token))
- 设置到 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 语句
- 数据模型解耦:更换数据库只需简单配置
缺点:
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()
|
注意:setUp 和 tearDown 是固定写法,测试函数名必须以 test_ 开头。
设置 app.testing=True 或 app.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 部署、限流策略等