📙安装
$ pip install flask
1 | 依赖项
- Werkzeug 实现 WSGI
- Jinja 模板引擎
- MarkupSafe Jinja 自带,用于防止 XSS 攻击
- ItsDangerous 签名数据确保安全性,用于保证 flask 的会话安全
- Click 编写命令行应用的框架,为 flask 指令提供支持
2 | 可选依赖项
以下依赖包不会自动安装,但安装后会被自动识别采用
- Blinker object-to-object 的信号广播功能
- python-dotenv 可从
.env
文件读取键值对并设为环境变量 - Watchdog 监视系统活动,方便开发环境快速高效重载
Successfully installed Flask-2.2.2 Jinja2-3.1.2 MarkupSafe-2.1.1
Werkzeug-2.2.2 click-8.1.3 colorama-0.4.5 itsdangerous-2.1.2
📙Quickstart
1 | 最简应用
-
写入文件
hello.py
,不要起名flask.py
, 否则与框架自身冲突# hello.py from flask import Flask # 实例是 WSGI 应用 # __name__是当前模块或包的名称的简便写法 app = Flask(__name__) @app.route("/") # 将路径与其对应函数绑定 def hello_world(): return "<p>Hello, World!</p>"
-
设定环境变量
$ set FLASK_APP=hello
-
在虚拟环境里运行
$ flask run
命令行直接运行 flask --app hello run
📋文件命名为app.py
或 wsgi.py
, 就不用设环境变量FLASK_APP
或使用命令行时加上 --app
参数 了
2 | 调试模式
生产环境下勿用❗
$ set FLASK_ENV=development
$ flask run
或命令行加参数直接运行 flask --app hello --debug run
运行后访问 http://127.0.0.1:5000
📋通过参数定义端口 flask --app=hello run --host=0.0.0.0
3 | HTML 脱字符
🚷Jinja 模板引擎会自动脱字符确保安全,也可手动调用脱字符功能
from markupsafe import escape
@app.route("/<name>")
def hello(name):
return f"Hello, {escape(name)}!"
4 | 路由
定义网站 url 使用装饰器 @app.route('/')
定义 URL 参数
@app.route('/<post_id>') # 定义参数
@app.route('/<int:post_id>') # 自动转换参数类型
post_id
将以关键字参数形式在方法中获取
🛠️可供转换类型:
Type | Info |
---|---|
string | 默认, 接收任意字符串除了 / |
int | 接收正整数 |
float | 接收正数 |
path | 与string 相同但接收 / |
uuid | 接收 UUID 字符串 |
重定向行为
下列定义会将请求从 /projects
重定向至 /projects/
@app.route('/projects/')
下列定义请求 /about/
时会抛出404
@app.route('/about')
💡概括:Flask 按最长的定义匹配 URL,不会自动增补 /
反向查询 URL
url_for
根据函数名反向输出其对应的url,类似 django 的 reverse()
from flask import url_for
@app.route('/')
def index():
return 'index'
with app.test_request_context():
print(url_for('index')) # /
如果传入定义时没有的参数,生成的 url 会将其自动转为 GET 参数
url_for('login', next='/') # /login?next=/
定义请求方式
路由默认只响应 GET 方式
-
使用同一个函数时,可通过参数
methods
自定义其他方式@app.route('/login', methods=['GET', 'POST'])
-
使用不同函数可调用不同方法定义
@app.get('/login') def login_get(): ... @app.post('/login') def login_post(): ...
5 | 静态文件
在模块同级或模块目录下创建一个文件夹 static
,flask 会将其识别为静态资源文件夹
以 /static/
作为 url,访问其中文件时直接加文件名
如在 static
下存放 head.jpg
可访问路径 /static/head.jpg
生成文件路径可使用 url_for()
url_for('static', filename='head.jpg')
6 | 渲染模板
Rendering Templates
Flask 会自动寻找与应用文件相邻的 templates
文件夹,并作为模板文件夹
-
单个文件结构
/hello.py /templates /hello.html
-
包结构
/application /__init__.py /templates /hello.html
💻调用方法 render_template()
传入模板名称、填充模板的数据 即可渲染模板
from flask import render_template
@app.route('/hi/<name>')
def hi(name=None):
return render_template('hello.html', name=name)
<!doctype html>
<title>Hello from Flask</title>
{% if name %}
<h1>Hello {{ name }}!</h1>
{% else %}
<h1>Hello, World!</h1>
{% endif %}
模板内部可以使用变量 config
, request
, session
,g
对象,
也可调用方法 url_for()
,get_flashed_messages()
模板内字符串可通过 Markup()
或 |safe
标记为安全的文字,不被 escape
>>> from markupsafe import Markup
>>> Markup('<strong>Hello %s!</strong>') % '<blink>hacker</blink>'
Markup('<strong>Hello <blink>hacker</blink>!</strong>')
7 | 获取请求数据
Accessing Request Data
Flask 内置方法 test_request_context()
可用于简单的单元测试,通过 with
语句设定上下文
from flask import request
with app.test_request_context('/hello', method='POST'):
# simple tests
assert request.path == '/hello'
assert request.method == 'POST'
还可以传字典代表 WSGI 环境给内置方法 request_context()
with app.request_context({}):
assert request.method == 'POST'
Request 对象
The Request Object
通过变量 request
获取,
完整对象属性参考🌏https://flask.palletsprojects.com/en/2.1.x/api/#flask.Request
from flask import request
request.method # 获取请求方式
request.form['username'] # 获取表单提交数据
request.args.get('key', '') # 获取 GET 参数
上传的文件
File Uploads
😊别忘给 <form>
标签设定属性 enctype="multipart/form-data"
上传的文件在内存中或是文件系统的临时存放处
f = request.files['the_file']
f.save('/var/www/uploads/uploaded_file.txt')
⚠️获取上传文件的文件名时注意安全,可用 secure_filename()
from werkzeug.utils import secure_filename
file = request.files['the_file']
file.save(f"/var/www/uploads/{secure_filename(file.filename)}")
Cookies
获取可直接使用属性
username = request.cookies.get('name')
设定 cookie
from flask import make_response
resp = make_response(render_template(...))
resp.set_cookie('name', 'the username')
响应还未生成时设定cookie 可参考 🌏Deferred Request Callbacks
8 | 重定向与错误
Redirects and Errors
-
生成重定向响应可用
redirect()
from flask import redirect, url_for @app.route('/') def index(): return redirect(url_for('login'))
-
响应错误中止代码执行可用
abort()
from flask import abort @app.route('/login') def login(): abort(401) this_is_never_executed()
💻M1. 自定义错误处理器可以使用装饰器
@app.errorhandler()
@app.errorhandler(404) def page_not_found(error): return render_template('page_not_found.html'), 404 # 结尾的404会显式指定状态码,否则 Flask 返回 200
💻M2. 使用 APP 工厂时直接在 create_app 调用时登记:
def create_app(config_filename): app = Flask(__name__) app.register_error_handler(404, page_not_found) return app
蓝图对象也可直接调用
errorhandler()
和register_error_handler()
9 | 响应
About Responses
Flask 会将视图函数的返回值自动转换为一个响应对象 response object:
-
返回的就是响应对象,则直接返回
-
返回值是字符串会则放在响应体中,状态码为 200,mimetype 为
text/html
-
返回值是
dict
或list
,Flask 会调用jsonify()
生成一个 Json Response -
如果返回生成字符类型的迭代器或生成器,则作为 streeaming reponse 处理
-
返回元组,则有三种形式
(response, status)
,(response, headers)
, or(response, status, headers)
-
其他情况会返回响应对象
手动创建 response object 可以调用 make_response()
生成响应对象可自定义响应头
@app.errorhandler(404)
def not_found(error):
resp = make_response(render_template('error.html'), 404)
resp.headers['X-Something'] = 'A value'
return resp
10 | 响应 JSON
APIs with JSON
编写返回 JSON 格式数据的 API, 可将返回值类型定为 dict
或 list
Flask 会自动调用内置方法 jsonify()
生成一个 Json Response 无需手动调用
⚠️数据必须支持序列化
11 | Sessions
使用前必须⚠️先设定一个 secrect key, Flask 会使用该值自动加密
from flask import session
# secret key 的值设为随机比特并且不要泄露!!!
app.secret_key = b'_5#y2L"F4Q8z\n\xec]/'
@app.route('/')
def index():
if 'username' in session:
return f'Logged in as {session["username"]}'
return 'You are not logged in'
@app.route('/login', methods=['GET', 'POST'])
def login():
if request.method == 'POST':
session['username'] = request.form['username']
return redirect(url_for('index'))
return '''
<form method="post">
<p><input type=text name=username>
<p><input type=submit value=Login>
</form>
'''
@app.route('/logout')
def logout():
# remove the username from the session if it's there
session.pop('username', None)
return redirect(url_for('index'))
💡secret key 越随机越好,Python 内置库可以解决
>>> import secrets
>>> secrets.token_hex()
Out[3]: '1860a2eb7361c50d3f0c4890a0cf5591cb0892ec378aeb8457cf044ab94e92db'
🚸Flask 会将存入 session 的值序列化存入 cookie,如果请求过程中其值发生变化,cookie 可能被截断,检查响应中 cookie 的长度,以及浏览器支持的长度
12 | 传递消息
Message Flashing
Step1. 在视图中使用内置方法 flash()
@app.route('/')
def index():
flash('hahah')
return render_template("hello.html")
Step2. 在模板中使用 get_flashed_messages()
{% with messages = get_flashed_messages() %}
{% if messages %}
<ul class=flashes>
{% for message in messages %}
<li>{{ message }}</li>
{% endfor %}
</ul>
{% endif %}
{% endwith %}
13 | 日志
Logging
Flask 使用 Python 标准库 logger 记录日志
开发模式下直接调用则在控制台中打印
app.logger.debug('A value for debugging')
app.logger.warning('A warning occurred (%d apples)', 42)
app.logger.error('An error occurred')
14 | WSGI 中间件
Hooking in WSGI Middleware
通过包裹属性 app.wsgi_app
定义,如
from werkzeug.middleware.proxy_fix import ProxyFix
app.wsgi_app = ProxyFix(app.wsgi_app)
📙Tutorial
🔗https://flask.palletsprojects.com/en/2.2.x/tutorial/
1 | 应用搭建
Application Setup
一个 flask 应用是类 Flask
的实例,其也称为应用工厂
创建一个空文件夹 flaskr
,在该目录下创建文件 __init__.py
, 写入代码
import os
from flask import Flask
def create_app(test_config=None):
# instance_relative_config 参数表示部分项目文件在 flaskr 文件夹外层
# 这些文件存有本地环境的数据,不需要提交至 VCS
app = Flask(__name__, instance_relative_config=True)
app.config.from_mapping(
SECRET_KEY='dev', # 方便开发,生产环境勿用
DATABASE=os.path.join(app.instance_path, 'flaskr.sqlite'),
)
if test_config is None:
# 从该文件读取配置
# silent=True 表示未找到文件时静默处理,默认值 False
app.config.from_pyfile('config.py', silent=True)
else:
# 读取传入的 mapping 类型配置
app.config.from_mapping(test_config)
# ensure the instance folder exists
try:
os.makedirs(app.instance_path)
except OSError:
pass
# 测试用视图函数
@app.route('/hello')
def hello():
return 'Hello, World!'
return app
2 | 数据库交互
Define and Access the Database
用 SQLite 作为数据库,可以直接用 Python 内置的标准库,大量写入请求会导致性能下降建议更换其他数据库:
import sqlite3
import click
from flask import current_app, g
def get_db():
if 'db' not in g:
g.db = sqlite3.connect(
current_app.config['DATABASE'],
detect_types=sqlite3.PARSE_DECLTYPES
)
g.db.row_factory = sqlite3.Row
return g.db
def close_db(e=None):
db = g.pop('db', None)
if db is not None:
db.close()
📋 g
是一个特殊对象,对每个请求都保证唯一性,处理请求过程中,可存储公共数据被多个函数共享
📋 .sqlite
文件可以稍后用的时候再创建
创建数据表
Create the Tables
创建指令方便执行:
def init_db():
db = get_db()
with current_app.open_resource('schema.sql') as f:
db.executescript(f.read().decode('utf8'))
@click.command('init-db')
def init_db_cmd():
"""Clear the existing data and create new tables."""
init_db()
click.echo("Initialized the database.")
📋open_resource()
按照相对路径寻找并打开资源,无需知道文件的确切位置
绑定应用
Register with the Application
close_db()
和 init_db_cmd()
定义之后,需要绑定在 APP 的上才能被调用
def init_app(app):
app.teardown_appcontext(close_db) # 返回响应后清理内存时调用 close_db
app.cli.add_command(init_db_cmd) # 注册自定义指令
💻在命令行中执行指令 flask --app flaskr init-db
即可
3 | 蓝图视图
Blueprints and Views
FLask 将 url 与存放对应业务逻辑的视图函数绑定,其处理后返回响应数据
创建蓝图
Create a Blueprint
蓝图是一种将相关视图函数及其相关代码放在一起的组织方式,注册蓝图而不注册函数:
关系:view function —> blueprint —> App
💻创建一个名为 auth 的蓝图:
from flask import Blueprint
bp = Blueprint('auth', __name__, url_prefix='/auth')
# 第二个参数是文件路径
# 第三个参数是 url 命名空间
💻注册蓝图调用方法 app.register_blueprint(auth.bp)
:
from . import auth
app.register_blueprint(auth.bp)
💻定义蓝图下的视图函数
@bp.route('/login', methods=('GET', 'POST'))
def login():
...
💻定义方法执行前的逻辑用 before_app_request
@bp.before_app_request
def load_logged_in_user():
...
💻定义一个装饰器
import functools
def login_required(view):
@functools.wraps(view)
def wrapped_view(**kwargs):
if g.user is None:
return redirect(url_for('auth.login'))
return view(**kwargs)
return wrapped_view
4 | 模板
Templates
默认模板引擎为 Jinja,主要语法:
{{ }}
将其中变量的值输出{% %}
用于if
语句等结构,通过{% end.. %}
结尾,end后加上对应语句
💻定义需要继承填充的块,用 {% block name %}{% endblock%}
💻继承模板用 {% extends 'template.html' %}
5 | 使应用可安装
Make the Project Installable
将编写好的应用实现在其他环境中安装运行
描述应用
在应用同级目录下创建文件 setup.py
描述应用及其内部的文件
# setup.py
from setuptools import find_packages, setup
setup(
name='flaskr',
version='1.0.0',
packages==find_packages()(),
include_package_data=True,
install_requires=[
'flask',
],
)
-
参数
packages
定义应用的文件路径,find_packages()
自动寻找文件,无需开发者逐个手写 -
参数
include_package_data
表示是否包含其他文件,如模板、静态资源文件等
如果需要包括其他文件,还要创建 MANIFEST.in
告知 Python 其他文件是什么
include flaskr/schema.sql
graft flaskr/static
graft flaskr/templates
global-exclude *.pyc
📋复制 static 和 templates 下的所有内容
📋全局排除 .pyc
文件
安装应用
将当前应用安装在当前的虚拟环境中:pip install -e .
该指令使 pip 寻找文件 setup.py
并在 editable development 两种模式下安装
📋editable 模式可以在本地修改应用代码,但修改应用的元信息时需要重装,如依赖项
使用 pip list
可以查看已安装的应用
$ pip list
Package Version Editable project location
------------ ------- --------------------------
click 8.1.3
colorama 0.4.5
Flask 2.2.2
flaskr 1.0.0 c:\projects\flask-tutorial
🎯安装到虚拟环境后,可在任意目录下运行应用
6 | 测试覆盖
Test Coverage
Flask 提供了一个测试客户端,可模拟对应用程序的请求并返回响应数据
测试覆盖率越接近 100% 越好,但不代表万无一失
以下使用第三方库 pytest
测试 coverage
检测覆盖率
设置测试环境
Setup and Fixtures
创建目录 tests
并创建以 test_ 开头的 .py 文件,每个蓝图对应一个测试文件
创建测试初始化测试的文件:
import tempfile
import pytest
from flaskr import create_app
from flaskr.db import get_db, init_db
@pytest.fixture
def app():
db_fd, db_path = tempfile.mkstemp()
app = create_app({
'TESTING': True,
'DATABASE': db_path,
})
with app.app_context():
init_db()
# _data_sql 是一个写有测试用 SQL 的文件
get_db().executescript(_data_sql)
yield app
os.close(db_fd)
os.unlink(db_path)
@pytest.fixture
def client(app):
return app.test_client()
@pytest.fixture
def runner(app):
return app.test_cli_runner()
📋tempfile.mkstemp()
创建并打开一个临时文件,返回文件描述符及其路径
📋 'TESTING': True
让 Flask 改变一些内部行为更容易测试
📋app.test_client()
测试将使用客户端向应用程序发出请求,而无需运行服务器
📋 app.test_cli_runner()
用于测试应用程序注册的 Click 命令
运行测试
setup.cfg
可定义一些额外的配置,这些配置可选,但可以使带覆盖率测试不那么冗长
[tool:pytest]
testpaths = tests
[coverage:run]
branch = True
source =
flaskr
💻使用指令 pytest
运行测试,加上参数 -v
显示被测试的函数名称
$ coverage run -m pytest
======================= test session starts ===========================
platform win32 -- Python 3.10.7, pytest-7.1.3, pluggy-1.0.0
rootdir: C:\projects\flask-tutorial, configfile: setup.cfg, testpaths: tests
collected 24 items
tests\test_auth.py ........ [ 33%]
tests\test_blog.py ............ [ 83%]
tests\test_db.py .. [ 91%]
tests\test_factory.py .. [100%]
==================== 24 passed in 1.15s =============================
💻使用指令 coverage run -m pytest
计算测试覆盖率,也可以查看覆盖报告 coverage report
$ coverage report
Name Stmts Miss Branch BrPart Cover
------------------------------------------------------
flaskr\__init__.py 18 0 2 0 100%
flaskr\auth.py 60 0 20 0 100%
flaskr\blog.py 58 0 16 0 100%
flaskr\db.py 23 0 6 0 100%
------------------------------------------------------
TOTAL 159 0 44 0 100%
生成 html 文件查看覆盖报告 coverage html
,该指令自动生成文件 htmlcov/index.html
7 | 部署生产环境
Deploy to Production
构建和安装
Build and Install
在其他位置部署应用时,首先创建一个分发文件,python 目前采用 .whl 文件
-
先安装库
pip install wheel
-
生成分发文件
python setup.py bdist_wheel
该指令自动生成文件
dist/flaskr-1.0.0-py3-none-any.whl
其格式为
{project name}-{version}-{python tag} -{abi tag}-{platform tag}
-
将该文件复制到生产环境,创建一个新的虚拟环境,并执行
pip install flaskr-1.0.0-py3-none-any.whl
-
安装后初始化数据库
flask --app flaskr init-db
配置密钥
Configure the Secret Key
推荐用 python 内置库生成
$ python -c 'import secrets; print(secrets.token_hex())'
d36f1e7608600c97b773911ee002a21fddd465bf359f529c8ce0966e0f8ddd30
将其复制到 config.py
文件中
SECRET_KEY = d36f1e7608600c97b773911ee002a21fddd465bf359f529c8ce0966e0f8ddd30
运行服务
Run with a Production Server
Flask 内置的指令 run
是开发时使用的便捷服务器,生产环境下可使用:
$ pip install waitress
安装后执行指令:
$ waitress-serve --call 'flaskr:create_app'
📙模板
Template
Flask 依赖安装 Jinja2,也可使用其他模板引擎
1 | 默认设定
Standard Context
- 使用方法
render_template()
时对于.html
,.htm
,.xml
.xhtml
其中的文字自动转义 - 使用方法
render_template_string()
时,所有字符串自动转义 - 模板中可以用
{% autoescape %}
控制自动转义 - Flask 自动插入一些全局方法与变量到模板上下文中:
config
request
session
g
url_for()
get_flashed_messages()
⚠️其中的变量并非全局,它们不会再被引入的模板上下文中出现,如果需要可以
{% from '_helpers.html' import my_macro with context %}
2 | 控制自动转义
Controlling Autoescaping
三种禁用自动转义的方法
-
将文字传入一个Markup 对象, 再传入模板
-
输出文字时,加上
|safe
, 如{{ myvariable|safe }}
-
暂时完全禁用自动转义
{% autoescape false %} <p>autoescaping is disabled here <p>{{ will_not_be_escaped }} {% endautoescape %}
3 | 自定义过滤器
Registering Filters
两种注册过滤器方法
-
方法一:
@app.template_filter()
@app.template_filter('reverse') def reverse_filter(s): return s[::-1]
-
方法二:
jinja_env
def reverse_filter(s): return s[::-1] app.jinja_env.filters['reverse'] = reverse_filter
使用自定义过滤器:
{% for x in mylist | reverse %}
{% endfor %}
4 | 上下文处理器
Context Processors
可以注入变量在所有模板上下文中使用:
@app.context_processor
def inject_user():
return dict(user=g.user)
还可以插入自定义方法:
@app.context_processor
def utility_processor():
def format_price(amount, currency="€"):
return f"{amount:.2f}{currency}"
return dict(format_price=format_price)
使用时 {{ format_price(0.33) }}
5 | 流式传输
Streaming
将模板以流的形式按需传输,而不是作为一个字符串一次性传输,渲染大模板时可节省内存
Flask 自带方法 stream_template()
stream_template_string()
可以实现
from flask import stream_template
@app.get("/timeline")
def timeline():
return stream_template("timeline.html")
📙测试
Testing Flask Applications
使用第三方库 pytest
进行测试,需手动安装
1 | 识别测试用例
Identifying Tests
测试文件通常放在 tests
文件夹中,测试文件以 test_ 开头,
测试用例以 test_
开头,可以将测试用例放在 Test
开头的类中进行分组
2 | 夹具
Fixtures
pytest 中的 fixture 可用于编写测试中通用的代码片段,一个夹具返回一个值,使用后可释放销毁,通常放在 tests/conftest.py
中
使用 APP 工厂 create_app()
,可以定义一些配置,销毁释放的业务逻辑代码
import pytest
from my_project import create_app
@pytest.fixture()
def app():
app = create_app()
app.config.update({
"TESTING": True,
})
# other setup can go here
yield app
# clean up / reset resources here
也可以引入已有的 app 对象,并定义夹具
@pytest.fixture()
def client(app):
return app.test_client()
@pytest.fixture()
def runner(app):
return app.test_cli_runner()
3 | 使用测试客户端发送请求
Sending Requests with the Test Client
测试客户端可以在不运行服务的情况下发送请求进行测试,其扩展自 Werkzeug
该测试客户端可以发送常见的 HTTP 请求,如 GET POST,还可以自定义请求参数,
通常使用 path
, query_string
, headers
, data
,json
def test_request_example(client):
response = client.get("/posts")
assert b"<h2>Hello, World!</h2>" in response.data
📋返回的 response
是一个 TestResponse 对象,其具有普通响应对象的属性
📋 response.data
中的数据通常是 bytes 类型,以文本形式查看可使用 response.text
或 response.get_data(as_text=True)
4 | 表单数据
Form Data
将数据以 dict 类型传给参数 data
即可, Content-Type
会自动设成 multipart/form-data
或 application/x-www-form-urlencoded
如果数据是以 rb 模式打开的文件,将视为上传的文件
文件对象会在请求生成后关闭,因此不用写 with
语句打开文件资源
可以将文件存放在 tests/resources
下,通过 pathlib.Path
按相对路径获取文件
from pathlib import Path
# get the resources folder in the tests folder
resources = Path(__file__).parent / "resources"
def test_edit_user(client):
response = client.post("/user/2/edit", data={
"name": "Flask",
"theme": "dark",
"picture": (resources / "picture.png").open("rb"),
})
assert response.status_code == 200
避免自动检测文件元信息,可以按照格式 (file, filename, content_type)
传元组
5 | Json 数据
JSON Data
将数据传参数 json
, Content-Type
将自动设置为application/json
响应中的 json 数据可以通过属性 response.json
查看
def test_json_data(client):
response = client.post("/graphql", json={
"query": "",
variables={"id": 2},
})
assert response.json["data"]["user"]["name"] == "Flask"
6 | 跟踪重定向
Following Redirects
传入参数follow_redirects=True
给请求方法,测试客户端将继续发出请求,直到返回非重定向响应
def test_logout_redirect(client):
response = client.get("/logout")
# Check that there was one redirect response.
assert len(response.history) == 1
# Check that the second request was to the index page.
assert response.request.path == "/index"
📋每个响应都有一个 request 属性
7 | 编辑 Session
Accessing and Modifying the Session
获取 Flask 上下文中的变量(session),需要在 with 语句下使用测试客户端,生成请求后上下问会保持 active 状态,知道 with 语句结束
from flask import session
def test_access_session(client):
with client:
client.post("/auth/login", data={"username": "flask"})
# session is still accessible
assert session["user_id"] == 1
# session is no longer accessible
在生成请求前就获取或设定session中的值,需要调用方法 session_transaction()
其返回session 对象
from flask import session
def test_modify_session(client):
with client.session_transaction() as session:
# set a user id without going through the login route
session["user_id"] = 1
# session is saved now
response = client.get("/users/me")
assert response.json["username"] == "flask"
8 | 使用 CLI Runner 运行命令
Running Commands with the CLI Runner
Flask 的 runner 扩展自 Click 库,使用 invoke()
方法可以模拟真实环境下执行 flask 指令
import click
@app.cli.command("hello")
@click.option("--name", default="World")
def hello_command(name):
click.echo(f"Hello, {name}!")
def test_hello_command(runner):
result = runner.invoke(args="hello")
assert "World" in result.output
result = runner.invoke(args=["hello", "--name", "Flask"])
assert "Flask" in result.output
9 | 依赖于动态上下文的测试
Tests that depend on an Active Context
某些方法调用时需要获取 request, session, current_app 等,使用 with app.app_context()
压入 app 上下文
def test_db_post_model(app):
with app.app_context():
post = db.session.query(Post).get(1)
使用 with app.test_request_context()
压入 request 上下文
def test_validate_user_edit(app):
with app.test_request_context(
"/user/2/edit", method="POST", data={"name": ""}
):
# call a function that accesses `request`
messages = validate_edit_user()
assert messages["name"][0] == "Name cannot be empty."
⚠️创建测试请求上下文不会运行任何 Flask 调度代码,因此before_request
不会被调用,可以手动调用
def test_auth_token(app):
with app.test_request_context("/user/2/edit", headers={"X-Auth-Token": "1"}):
app.preprocess_request()
assert g.user.name == "Flask"
📙处理错误异常
Handling Application Errors
生产环境下报错时,Flask 会展示一个简单的错误页面,并通过 logger
记录日志
1 | 错误记录工具 Sentry
Error Logging Tools
当海量用户请求触发同一错误时会导致错误邮件大量发送,推荐使用 Sentry
(Freemium)🔗https://sentry.io/welcome/
它可以聚合重复错误,捕获完整的堆栈跟踪和局部变量以进行调试,并根据新的错误或频率阈值发送报错邮件
🛠️务必安装带 Flask 支持的版本
$ pip install sentry-sdk[flask]
Flask 版本文档 🔗 https://docs.sentry.io/platforms/python/guides/flask/
安装后,服务器错误会自动发给 Sentry,其管理员发送错误通知
2 | 错误处理程序
Error Handlers
两种方式设置错误处理的方法,别忘定义状态码:
# 装饰器
@app.errorhandler(werkzeug.exceptions.BadRequest)
def handle_bad_request(e):
return 'bad request!', 400
# 直接注册
app.register_error_handler(400, handle_bad_request)
非标准的状态码错误需要特殊处理:
class InsufficientStorage(werkzeug.exceptions.HTTPException):
code = 507
description = 'Not enough storage space.'
def handle_507():
...
app.register_error_handler(InsufficientStorage, handle_507)
raise InsufficientStorage() # 使用时直接 raise
蓝图中定义的错误处理方法会优先于其他全局的错误处理方法
3 | 通用异常处理程
Generic Exception Handlers
不建议通过 HTTPException
来判断异常,其捕捉范围太广🤣 无异于 except Exception
:
# ❌不建议:
@app.errorhandler(HTTPException)
def handle_exception(e):
...
# ⭕可改写为:
@app.errorhandler(Exception)
def handle_exception(e):
if isinstance(e, HTTPException):
return e
# 对于非 HTTP 错误返回500页面
return render_template("500_generic.html", e=e), 500
⚠️如果同时注册 HTTPException
和 Exception
的错误处理,出现 HTTP 错误时会触发HTTPException
的错误处理因为其更具体
如果错误没有注册对应处理逻辑,Flask 返回 500 Internal Server Error,用 InternalServerError
处理
4 | 自定义错误页面
Custom Error Pages
💡QuickStart - 8 | 重定向与错误
如果针对不同蓝图设定不同的错误处理,需要在 app 层面定义,判断不同 url:
@app.errorhandler(404)
def page_not_found(e):
# if a request is in our blog URL space
if request.path.startswith('/blog/'):
return render_template("blog/404.html"), 404
else:
return render_template("404.html"), 404
5 | Json 格式错误
Returning API Errors as JSON
编写 API 时返回 Json 格式的错误,需要使用方法 jsonify()
from flask import abort, jsonify
@app.errorhandler(404)
def resource_not_found(e):
return jsonify(error=str(e)), 404
@app.route("/cheese")
def get_one_cheese():
resource = get_resource()
if resource is None:
abort(404, description="Resource not found")
return jsonify(resource)
💡自定义 API 用的错误处理方法
from flask import jsonify, request
# 1. 自定义一个异常类
class InvalidAPIUsage(Exception):
status_code = 400
def __init__(self, message, status_code=None, payload=None):
super().__init__()
self.message = message
if status_code is not None:
self.status_code = status_code
self.payload = payload
def to_dict(self):
rv = dict(self.payload or ())
rv['message'] = self.message
return rv
# 2.注册自定义异常的处理方法
@app.errorhandler(InvalidAPIUsage)
def invalid_api_usage(e):
return jsonify(e.to_dict()), e.status_code
# 3.通过 raise 触发自定义异常
raise InvalidAPIUsage("No user id provided!")
raise InvalidAPIUsage("No such user!", status_code=404)
📙排查错误
Debugging Application Errors
⚠️生产环境下不要开debug 模式,或运行开发服务器
📋使用外部调试器时禁用一些内置的设定会更方便
$ flask --app hello --debug run --no-debugger --no-reload
等效于
app.run(debug=True, use_debugger=False, use_reloader=False)
📙日志
Logging
💻Flask 使用 Python 标准库 logging
记录日志,调用app.logger
的方法
app.logger.info('%s logged in successfully', user.username)
app.logger.info('%s failed to log in', user.username)
日志级别默认为 warning ,其下级的日志不会显示
1 | 基础配置
Basic Configuration
💻通过 dictConfig()
定义日志配置
from logging.config import dictConfig
dictConfig({
'version': 1,
'formatters': {'default': {
'format': '[%(asctime)s] %(levelname)s in %(module)s: %(message)s',
}},
'handlers': {'wsgi': {
'class': 'logging.StreamHandler',
'stream': 'ext://flask.logging.wsgi_errors_stream',
'formatter': 'default'
}},
'root': {
'level': 'INFO',
'handlers': ['wsgi']
}
})
app = Flask(__name__)
-
默认配置
如果没配置就使用,Flask 默认添加一个 StreamHandler,日志输出到
sys.stderr
💻可以手动删除默认配置
from flask.logging import default_handler app.logger.removeHandler(default_handler)
⭕最好在记录日之前定义配置
2 | 发送错误邮件
Email Errors to Admins
💻配置logging.handlers.SMTPHandler
可发送报错邮件给开发者
📋需要 SMTP 服务器才能发送
import logging
from logging.handlers import SMTPHandler
mail_handler = SMTPHandler(
mailhost='127.0.0.1',
fromaddr='server-error@example.com',
toaddrs=['admin@example.com'],
subject='Application Error'
)
mail_handler.setLevel(logging.ERROR)
mail_handler.setFormatter(logging.Formatter(
'[%(asctime)s] %(levelname)s in %(module)s: %(message)s'
))
if not app.debug:
app.logger.addHandler(mail_handler)
3 | 注入请求信息
Injecting Request Information
记录将有助于调试的请求相关信息,可修改 logging.Formatter
from flask import has_request_context, request
from flask.logging import default_handler
class RequestFormatter(logging.Formatter):
def format(self, record):
if has_request_context():
record.url = request.url
record.remote_addr = request.remote_addr
else:
record.url = None
record.remote_addr = None
return super().format(record)
formatter = RequestFormatter(
'[%(asctime)s] %(remote_addr)s requested %(url)s\n'
'%(levelname)s in %(module)s: %(message)s'
)
default_handler.setFormatter(formatter)
mail_handler.setFormatter(formatter)
4 | 其他库
使用其他日志功能的第三方库时,可以将需要执行的日志逻辑绑定到根 logger 上
💻手动添加 handler 可调用 addHandler()
from flask.logging import default_handler
root = logging.getLogger()
root.addHandler(default_handler)
root.addHandler(mail_handler)
📙设置
Configuration Handling
1 | 基本配置
Configuration Basics
app = Flask(__name__)
app.config['TESTING'] = True # config 是 dict 子类
app.testing = True # 也可从对象获取或修改配置
# 同时修改多个配置可以直接update()
app.config.update(
TESTING=True,
SECRET_KEY='192b9bdd22a...'
)
2 | python 配置文件
Configuring from Python Files
根据不同环境加载不同配置,读取配置:
app = Flask(__name__)
app.config.from_object('yourapplication.default_settings') # 默认设置
app.config.from_envvar('YOURAPPLICATION_SETTINGS') # 不同环境的特殊设置
YOURAPPLICATION_SETTINGS
可以在启动服务前通过 shell 设置
$ export YOURAPPLICATION_SETTINGS=/path/to/settings.cfg
$ flask run
* Running on http://127.0.0.1:5000/
配置文件中,保证变量采用全大写命名,如 SECRET_KEY
3 | 数据配置文件
Configuring from Data Files
可以从 .toml
和 .json
文件中读取配置
import toml
app.config.from_file("config.toml", load=toml.load)
import json
app.config.from_file("config.json", load=json.load)
4 | 环境变量配置
Configuring from Environment Variables
$ export FLASK_SECRET_KEY="5f352379324c22463451387a0aec5d2f"
$ export FLASK_MAIL_ENABLED=false
$ flask run
* Running on http://127.0.0.1:5000/
Flask 可通过方法 from_prefixed_env(prefix='FLASK')
获取这些 FLASK_
开头的环境变量
app.config.from_prefixed_env()
app.config["SECRET_KEY"] # Is "5f352379324c22463451387a0aec5d2f"
Flask 还以识别嵌套格式的环境变量
$ export FLASK_MYAPI__credentials__username=user123
会转化为:
app.config["MYAPI"]["credentials"]["username"] # Is "user123"
5 | 测试 / 正式
Development / Production
正式环境下,创建 config.py
并设定环境变量 YOURAPPLICATION_SETTINGS=/path/to/config.py
也可以创建配置的类:
class Config(object):
TESTING = False
class ProductionConfig(Config):
DATABASE_URI = 'mysql://user@localhost/foo'
class DevelopmentConfig(Config):
DATABASE_URI = "sqlite:tmp/foo.db"
class TestingConfig(Config):
DATABASE_URI = 'sqlite:///:memory:'
TESTING = True
# 载入配置
app.config.from_object('configmodule.ProductionConfig')
⚠️ from_object()
方法不会实例化对象,如果后期需获取配置,必须手动实例化
from configmodule import ProductionConfig
app.config.from_object(ProductionConfig())
# 也可以:
from werkzeug.utils import import_string
cfg = import_string('configmodule.ProductionConfig')()
app.config.from_object(cfg)
📋一些建议:
- 版本库中始终维护一个默认配置文件
- 使用环境变量区分环境
- 使用类似 fabric 的工具提交代码并单独设置生产服务器
6 | 实例文件夹
Instance Folders
自 Flask 0.8 有属性 Flask.instance_path
,其引入一个 instance folder 的概念,其中可以放配置文件、业务代码,可以显式提供该文件夹路径,也可以让 Flask 自动检测:
app = Flask(__name__, instance_path='/path/to/instance/folder')
# 路径必须为绝对路径
如果没传 instance_path
则以下路径默认使用
-
单个文件:
/myapp.py /instance
-
单个模块
/myapp /__init__.py /instance
-
已安装的文件或包:
$PREFIX/lib/pythonX.Y/site-packages/myapp $PREFIX/var/myapp-instance
Flask 提供了便捷方法可以读取 instance folder 下的文件
filename = os.path.join(app.instance_path, 'application.cfg')
with open(filename) as f:
config = f.read()
# 使用 open_instance_resource:
with app.open_instance_resource('application.cfg') as f:
config = f.read()
📙信号
Signals
🔗https://blinker.readthedocs.io/en/stable/
自 Flask 0.6 开始支持信号机制,其代码来自第三方库 blinker
,即使不可用也能回退🤣
💡信号机制旨在通知订阅者,而不鼓励订阅者修改数据
signals.signals_available=True
信号机制可用
1 | 订阅信号
下面代码是一个上下文管理器,帮助单元测试时选择哪个模板进行渲染,以及传给模板的数据
from flask import template_rendered
from contextlib import contextmanager
@contextmanager
def captured_templates(app):
recorded = []
def record(sender, template, context, **extra): # 额外加一个**extra
recorded.append((template, context))
template_rendered.connect(record, app)
try:
yield recorded
finally:
template_rendered.disconnect(record, app)
# 对应的测试代码
with captured_templates(app) as templates:
rv = app.test_client().get('/')
assert rv.status_code == 200
assert len(templates) == 1
template, context = templates[0]
assert template.name == 'index.html'
assert len(context['items']) == 10
-
订阅用
connect(receiver, sender=ANY, weak=True)
第一个参数发出信号时调用函数,第二个参数式发送者,通常式发出信号的应用,不提供发送者则监听来自所有应用发出的信号
-
取关用
disconnect()
🆕还可以使用 Blinker 1.1 版本新增的 connect_to(receiver, sender=ANY)
临时订阅某信号
from flask import template_rendered
def captured_templates(app, recorded, **extra):
def record(sender, template, context):
recorded.append((template, context))
return template_rendered.connected_to(record, app)
# 对应测试代码
templates = []
with captured_templates(app, templates, **extra):
...
template, context = templates[0]
2 | 创建信号
Creating Signals
可以通过 blinker 直接自定义信号,最普遍且推荐的用法是在 Namespace 中创建
from blinker import Namespace
my_signals = Namespace()
model_saved = my_signals.signal('model-saved') # 自定义信号
💡信号名称全局唯一可以简化排查问题的过程,属性 name
可以直接获取名称
3 | 发出信号
Sending Signals
💻调用方法 send(*sender, **kwargs)
class Model(object):
...
def save(self):
model_saved.send(self)
💡模型发出信号时可传入 self
,如果是普通函数,可传入 current_app._get_current_object()
4 | 请求过程信号
Signals and Flask’s Request Context
请求过程中也可使用信号,如 request_started
request_finished
且上下文可用, 如 flask.g
5 | 信号装饰器
Decorator Based Signal Subscriptions
🆕 Blinker 1.1 版本开始引入的快捷方法 connect_via(sender, weak=False)
亦可订阅信号
from flask import template_rendered
@template_rendered.connect_via(app)
def when_template_rendered(sender, template, context, **extra):
print(f'Template {template.name} is rendered with {context}')
6 | 所有信号
Core Signals
官方文档:🔗https://flask.palletsprojects.com/en/2.2.x/api/#core-signals-list
📙类视图
Class-based Views
🔗https://flask.palletsprojects.com/en/2.2.x/views/
充当视图函数的类,通过不同参数创建不同实例,以改变视图行为
1 | 基础示例
Basic Reusable View
用户列表的 视图函数 ----> 类视图:
@app.route("/users/")
def user_list():
users = User.query.all()
return render_template("users.html", users=users)
from flask.views import View
class UserList(View):
# 相当于视图函数:
def dispatch_request(self):
users = User.query.all()
return render_template("users.html", objects=users)
# 定义路由时用 as_view() + add_url_rule()
# as_view(name, *class_args, **class_kwargs) 可创建一个使用视图函数用于登记 URL:
app.add_url_rule("/users/", view_func=UserList.as_view("user_list"))
⚠️ @app.route()
不能给类视图定义 URL !!
🆕 Flask 2.2 的 as_view()
新增参数 init_every_request
可设定每次请求时是否创建实例,默认 True
🔗https://flask.palletsprojects.com/en/2.2.x/api/#flask.views.View.as_view
2 | URL 参数
URL Variables
路由中定义的参数以关键字参数形式传给 dispatch_request
class DetailView(View):
def __init__(self, model):
...
def dispatch_request(self, id): # 这里获取参数 id
item = self.model.query.get_or_404(id)
return render_template(self.template, item=item)
app.add_url_rule(
"/users/<int:id>", view_func=DetailView.as_view("user_detail", User)
)
3 | init_every_request
View Lifetime and self
类视图默认每次请求都会实例化 init_every_request=True
,用 self
获取属性并修改时,数据相互独立
💡也可设为 False
以避免复杂的实例化过程提高效率,此时存储数据务必用 g
对象
4 | 视图装饰器
View Decorators
⚠️类视图的视图函数加装饰器需要装在 as_view() 方法返回的视图函数上,
因此不能用普通的写法
-
类中定义:
class UserList(View): decorators = [cache(minutes=2), login_required] ...
-
手动调用
view = UserList.as_view("users_list") view = cache(minutes=2)(view) view = login_required(view) app.add_url_rule('/users/', view_func=view) # 相当于普通函数 @app.route("/users/") @login_required @cache(minutes=2) def user_list(): ...
⚠️注意装饰顺序不要搞错 !!! ⚠️
5 | 请求方法
Method Hints
定义类视图支持的请求方法
-
类中定义:
class MyView(View): methods = ["GET", "POST"] def dispatch_request(self): if request.method == "POST": # 这里判断方法类型写逻辑 ... app.add_url_rule('/my-view', view_func=MyView.as_view('my-view'))
-
URL 中定义:
app.add_url_rule( "/my-view", view_func=MyView.as_view("my-view"), methods=["GET", "POST"], )
📙应用上下文
The Application Context
🔗https://flask.palletsprojects.com/en/2.2.x/appcontext/
应用上下文在请求、CLI 命令或其他活动期间跟踪 app 级别的数据,通过 current_app
与 g
1 | 上下文的作用
Purpose of the Context
在模块的业务逻辑中引入 app
获取 config
等数据时可能出现循环引用❌
⭕Flask 用上下文解决该问题,通过 current_app
代理 app ,而不是直接访问 app
-
处理请求时自动压入应用上下文,无论是 视图函数、错误处理器、普通函数都能使用
current_app
-
执行 CLI 指令时自动压入上下文
2 | 生命周期
Lifetime of the Context
通常应用上下文的生命周期从请求开始到请求结束,详见 The Request Context
3 | 手塞上下文
Manually Push a Context
在应用上下文以外的地方调用 current_app
会报错 RuntimeError: Working outside of application context
如果是初始化应用时出现该错误,可用 with app.app_context()
解决:
def create_app():
app = Flask(__name__)
with app.app_context():
init_db()
...
其他地方出现时,应将出错代码移入视图函数或 CLI 指令的逻辑中
4 | 存储数据
Storing Data
g
对象可用于存储请求期间或 CLI 指令的公共数据,
与应用上下文的生命周期相⚠️,因此不要存储不同请求间的数据,应用 session
替代
💻在 g
中创建一个公用的数据库链接:
from flask import g
def get_db():
if 'db' not in g:
g.db = connect_to_database()
return g.db
# 请求结束时自动销毁
@app.teardown_appcontext
def teardown_db(exception):
db = g.pop('db', None)
if db is not None:
db.close()
💻LocalProxy(local, name=None, *, unbound_message=None)
代理使用对象并创建新的本地上下文
from werkzeug.local import LocalProxy
db = LocalProxy(get_db)
📙请求上下文
The Request Context
🔗 https://flask.palletsprojects.com/en/2.2.x/reqcontext/
在请求过程中跟踪请求及数据,主要通过 request
与session
代理操作
1 | 上下文的作用
Purpose of the Context
Flask 处理请求时,会根据 WSGI 服务环境创建一个 Request 对象,自动压入上下文
视图函数、错误处理器、其他函数都能在请求期间获取当前的 request 对象
2 | 生命周期
Lifetime of the Context
与 应用上下文相同,贯穿整个请求过程,每个线程的请求上下文相互独立
上下文中的(局部)变量,通过 Python 的 contextvars
与 Werkzeug 的 LocalProxy
实现底层逻辑
3 | 手塞上下文
Manually Push a Context
请求上下文外访问 request 会出错 RuntimeError: Working outside of request context
通常该错误发生在测试代码未能获取动态 request 时,应使用 with app.test_request_context()
解决
with app.test_request_context(
'/make_report/2017', data={'format': 'short'}):
f = request.args.get('format')
其他地方出现时,应将出错代码放在视图函数中
4 | 工作原理
How the Context Works
- 每次请求开始前,Flask 调用
before_request()
注册的函数 , 如果其中由函数返回值,则其他函数被忽略,请求的视图函数也不会被调用 - 如果
before_request()
没有返回响应,请求的视图函数会被调用 - 视图的返回值被转为一个 Response 对象,并传入
after_request()
其中每个函数可一次加工响应对象 - 响应返回后,Flask 调用
teardown_request()
与teardown_appcontext()
销毁上下文,即使出现异常错误这些销毁函数会被调用
5 | 销毁回调
Teardown Callbacks
上下文弹出时调用销毁程序,测试时用 with app.test_client()
保留上下文,从而在测试后访问其数据
# with 语句结束后会执行销毁环节
with app.test_client() as client:
client.get('/')
# 尽管请求结束了上下文依旧可以访问
print(request.path)
6 | 信号
Signals
启用信号机制时,会发送以下信号:
-
request_startedbefore_request()
在调用函数之前发送 -
request_finishedafter_request()
在调用函数后发送 -
got_request_exception
在开始处理异常时发送,但在寻找errorhandler()
或调用之前 -
request_tearing_downteardown_request()
在调用函数后发送
7 | 代理提示
Notes On Proxies
Flask 中的一些对象是其他对象的代理
- 代理对象无法具有与真实对象的一模一样的数据类型,如果需要类型对比,必须与真实对象对比
- 在某些场景下会使用被代理的真实对象,如发送信号
如果需要访问被代理的对象,可以使用 _get_current_object()
app = current_app._get_current_object()
my_signal.send(app)
📙蓝图
Modular Applications with Blueprints
🔗https://flask.palletsprojects.com/en/2.2.x/blueprints/
1 | 为什么使用蓝图
Why Blueprints?
- 蓝图可以将应用分解
- 不同蓝图可以注册不同的 url 前缀
- 同一蓝图可以注册多个不同的 url
- 蓝图支持模板过滤器,静态文件,及其他便捷功能
2 | 创建并注册蓝图
My First Blueprint
from flask import Blueprint
simple_page = Blueprint('simple_page', __name__,
template_folder='templates')
@simple_page.route('/<page>')
def show(page):
...
💻调用 app.register_blueprint()
from flask import Flask
from yourapplication.simple_page import simple_page
app = Flask(__name__)
app.register_blueprint(simple_page)
💻 app.url_map
可输出所有路由
3 | 嵌套蓝图
蓝图可以相互嵌套
parent = Blueprint('parent', __name__, url_prefix='/parent')
child = Blueprint('child', __name__, url_prefix='/child')
parent.register_blueprint(child)
app.register_blueprint(parent)
📋子蓝图的路由将扩展在父路由的后面
url_for('parent.child.create')
/parent/child/create
📋子蓝图没有定义错误处理器时,会在父蓝图中寻找
4 | 蓝图资源文件
Blueprint Resources
💻查看蓝图所在的应用的文件夹路径 blueprint.root_path
simple_page.root_path
'/Users/username/TestProject/yourapplication'
💻 快速打开资源文件可以使用 open_resource()
-
静态资源文件
蓝图可自定义资源文件夹,路径要么绝对要么相对:
admin = Blueprint('admin', __name__, static_folder='static')
该蓝图对应的静态文件 url 是
/admin/static
📋可以通过参数
static_url_path
定义 url在模板中生成 url 应写为
url_for('admin.static', filename='style.css')
-
模板
admin = Blueprint('admin', __name__, template_folder='templates')
目录结构:
yourpackage/ blueprints/ admin/ templates/ admin/ index.html __init__.py
📙扩展
Extensions
Flask 的定制扩展库名称通常采用 Flask-
或者 -Flask
PyPI 上可以按标签找到:
https://pypi.org/search/?c=Framework+%3A%3A+Flask
扩展通常在初始化 app 时加入一些独有的配置
from flask_foo import Foo
foo = Foo()
app = Flask(__name__)
app.config.update(
FOO_BAR='baz',
FOO_SPAM='eggs',
)
foo.init_app(app)
📙命令行接口
Command Line Interface
https://flask.palletsprojects.com/en/2.2.x/cli/
1 | 找到应用
flask -app <appname> run
可运行服务,其中 --app 有几种设定方式
--app src/hello
将当前工作目录改为 src 并引入 hello--app hello.wb
引入路径 hello.web--app hello:app2
使用 hello 下的应用 app2--app hello:create_app('dev')
调用 hello 中的 create_app 时传入参数dev
2 | 运行开发服务器
Run the Development Server
直接运行 flask --app hello run
-
Debug 模式
可以启用交互式 debugger 工具并自动重载如果文件内容发生变化,推荐开发时使用
flask --app hello --debug run * Serving Flask app "hello" * Debug mode: on * Running on http://127.0.0.1:5000/ (Press CTRL+C to quit) * Restarting with inotify reloader * Debugger is active! * Debugger PIN: 223-456-919
📋自动重载机制还可以监测一些额外文件,可通过
--extra-files
定义,windows 用分号分隔$ flask run --extra-files file1:dirA/file2:dirB/ * Running on http://127.0.0.1:8000/ * Detected change in '/path/to/file1', reloading
📋确保默认的 5000 端口没有被占用
3 | Shell
Open a Shell
flask 也有交互式命令行,应用上下文与 app 对象会被引入
$ flask --app flaskr shell
Python 3.10.7 (tags/v3.10.7:6cc6b13, Sep 5 2022, 14:08:36) [MSC v.1933 64 bit (AMD64)] on win32
App: flaskr
Instance: C:\projects\flask-tutorial\instance
>>>
-
创建请求上下文
ctx = app.test_request_context() ctx.push() # 压入上下文 ctx.pop() # 不用时弹出上下文
-
执行请求前行为
preprocess_request()
ctx = app.test_request_context() ctx.push() app.preprocess_request()
preprocess_request()
有可能返回一个响应对象 -
关闭请求
app.process_response(app.response_class()) <Response 0 bytes [200 OK]> ctx.pop()
pop() 时会自动执行
teardown_request()
📋可以将 shell 里重复执行的代码放进一个模块中,并通过 *
引入调用,更加方便
4 | 使用环境变量
Environment Variables From dotenv
Flask 的指令选项支持名称以 FLASK_
开头的环境变量作为参数值
-
python-dotenv
Flask 支持自动设定环境变量,无需手写
安装
python-dotenv
执行指令会将环境变量记录在文件.env
和.flaskenv
中也可以用参数
--env-file
设定文件这些文件只会在 flask 指令执行时加载,生产环境下需要手动调用
load_dotenv()
忽略自动加载环境变量机制
FLASK_SKIP_DOTENV=1
也可以在虚拟环境中设置环境量,使用设置的命令前激活环境即可
5 | 自定义指令
Custom Commands
定义:
import click
@app.cli.command("create-user")
@click.argument("name")
def create_user(name):
...
使用 $ flask create-user admin
-
指令组
使用
$ flask user create admin
方便将多个相关的指令组织到一起import click from flask import Flask from flask.cli import AppGroup app = Flask(__name__) user_cli = AppGroup('user') # 定义指令组的名称 @user_cli.command('create') @click.argument('name') def create_user(name): ... app.cli.add_command(user_cli)
-
蓝图指令
指令可绑定给蓝图对象,写法与绑定 app 相同,即
@bp.cli.command('..')
默认情况下指令的第二个参数是蓝图名称,使用时
$ flask students create alice
该名称可以自定义
cli_group
:bp = Blueprint('students', __name__, cli_group='other') # or app.register_blueprint(bp, cli_group='other')
使用时
$ flask other create alice
如果 cli_group=None
则相当于去掉了指令的第二个参数
-
应用上下文
指令的逻辑代码中需要应用上下文时,引入装饰器
from flask.cli import with_appcontext
即可
6 | 插件指令
Plugins
第三方库中方法也可被定义为指令,在 setup.py
中可以自定义这些指令:
from setuptools import setup
setup(
name='flask-my-extension',
...,
entry_points={
'flask.commands': [
# flask_my_extension/commands.py 下
# 带有装饰器 @click.command() 的函数 cli
'my-command=flask_my_extension.commands:cli'
],
},
)
7 | 自定义脚本
Custom Scripts
import click
from flask import Flask
from flask.cli import FlaskGroup
def create_app():
app = Flask('wiki')
return app
@click.group(cls=FlaskGroup, create_app=create_app)
def cli():
...
在 setup.py
中定义脚本:
from setuptools import setup
setup(
name='flask-my-extension',
...,
entry_points={
'console_scripts': [
'wiki=wiki:cli'
],
},
)
安装应用 $ pip install -e .
即可使用自定义脚本:
$ wiki run
📙安全
Security Considerations
🔗https://flask.palletsprojects.com/en/2.2.x/security/
1 | 跨站脚本攻击
Cross-Site Scripting (XSS)
Flask 默认的模板引擎 Jinja2 会自动脱字符,理论上已排除 XSS 攻击风险,但仍有几种情况需要注意:
-
不通过 Jinja 生成 HTML 代码
-
将用户提交的数据,在渲染模板时标记 Markup
-
不要分发外部上传文件中的 HTML ,务必使用响应头
Content-Disposition: attachment
让其在浏览器中以附件形式展示,否则默认inline
会将文件展示在网页中 -
不要分发外部上传的文本文件,因为某些浏览器会根据文件中的 bytes 猜文件类型,因此攻击者可以让浏览器执行文本文件中的 HTML 攻击代码
-
渲染模板的变量值一定用引号括起来, Jinja 无法对其脱字符,如果值是攻击代码可能会被执行!
<input value="{{ value }}">
-
链接地址可能渲染攻击代码,Jinja 无法提供保护:
<a href="{{ value }}">click here</a> <a href="javascript:alert('unsafe');">click here</a>
也可设定响应头 CSP 予以避免
2 | 跨站请求伪造
Cross-Site Request Forgery (CSRF)
Flask 没有 form
3 | Json 安全
用 Flask 内置的 jsonify() 即可保证安全
4 | 响应头安全
Security Headers
-
HTTP Strict Transport Security HSTS
告知浏览器强制使用 https 协议,防止中间人攻击 MITM
response.headers['Strict-Transport-Security'] = 'max-age=31536000; includeSubDomains'
-
Content Security Policy (CSP)
response.headers['Content-Security-Policy'] = "default-src 'self'"
设定比较耗时费力,且需要维护,但能有效防止攻击脚本的运行
🔗https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy
-
X-Content-Type-Options
防止浏览器对类型未知的文件资源嗅探
response.headers['X-Content-Type-Options'] = 'nosniff'
-
X-Frame-Options
防止攻击者利用
<iframe>
标签将我方网站嵌入攻击网站response.headers['X-Frame-Options'] = 'SAMEORIGIN'
-
HTTP Public Key Pinning HPKP
让浏览器向服务器核实某个具体的认证的 key 以防止 MITM
一旦开启,不正确更新或设置 key 将很难撤销
🔗https://developer.mozilla.org/en-US/docs/Web/HTTP/Public_Key_Pinning
-
cookie 安全
为响应头
Set-Cookie
添加选项以增加安全性:app.config.update( SESSION_COOKIE_SECURE=True, # cookie 仅可在 https 协议下使用 SESSION_COOKIE_HTTPONLY=True, # 保护 cookie 内容不被 Js 代码读取 # 限制外站 cookie 如何发送,推荐 lax 或 strict # lax 防止发送外站可能是 CSRF 的请求的 cookie SESSION_COOKIE_SAMESITE='Lax', ) response.set_cookie('username', 'flask', secure=True, httponly=True, samesite='Lax')
还可以设定过期时间,否则浏览器关闭时删除:
response.set_cookie('snakes', '3', max_age=600)
对于 session cookie ,如果
session.permannet=True
则PERMANENT_SESSION_LIFETIME
配置项将被用作过期时间app.config.update( PERMANENT_SESSION_LIFETIME=600 ) @app.route('/login', methods=['POST']) def login(): ... session.clear() session['user_id'] = user.id session.permanent = True ...
💡用
itsdangerous.TimedSerializer
给 cookie 值签名及验证,或者其他需要签名的数据
📙正式环境服务
Deploying to Production
Flask 时 WSGI 应用,需要 WSGI 服务器将收到的 HTTP 请求转换到标准的 WSGI 环境,并吧发送的 WSGI 响应转换为 HTTP 响应
常用的服务器:
WSGI 服务器内置 HTTP 服务器,但一个专门的 HTTP 服务器更加安全,高效,好用
💡也可以将 HTTP 服务器挡在 WSGI 服务器前作为反向代理
主流的 HOST 服务平台:
📙异步执行
Using async and await
如果安装了第三方库 flask[async]
,各个功能可协程运作,可使用关键字 async
与 await
@app.route("/get-data")
async def get_data():
data = await async_db_query(...)
return jsonify(data)
类视图也可支持协同运作
-
性能表现
当请求到达 async 视图时,Flask 会创建一个线程,在该线程中执行视图函数,并返回结果
每个请求对应一个负责处理的 worker,
⭕好处是可以在视图中异步执行代码,如请求外部API
❌但是一次性处理请求的数量不会改变
异步本身并不比同步快 ,异步在执行并发的 IO 任务时会有用,
-
后台任务
异步函数会执行一个活动循环 event loop, 完成时循环停止,这意味着任何额外衍生的未完成 task 会在异步函数完成时终止,因此不能通过
asyncio.create_task
衍生后台任务如果需要使用后台任务,最好选择任务队列,而不是在视图函数执行时衍生,
-
Quart
由于实现方式,Flask 的异步性能不如异步框架,如果业务代码以异步为基础设计的,可使用第三方库 Quart
Quart 是基于 ASGI 标准的 Flask 实现
-
扩展
在 Flask 支持异步功能之前就开发的第三方库不兼容异步,如果其提供了装饰器,可能不会异步执行
第三方库的作者可以利用
flask.Flask.ensure_sync()
以支持异步,如修改装饰器逻辑def extension(func): @wraps(func) def wrapper(*args, **kwargs): ... # Extension logic return current_app.ensure_sync(func)(*args, **kwargs) return wrapper
📋 ensure_sync(func): async def
functions are wrapped to run and wait for the response. Override this method to change how the app runs async views.
其他活动循环
目前 Flask 仅支持 asyncio
, 可以覆写 flask.Flask.ensure_sync()
以改变异步函数的包裹方式从而使用其他库
END