Flask 官方文档笔记(简明)

在这里插入图片描述

📙安装

$ 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 | 最简应用

  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>"
    
  2. 设定环境变量

    $ set FLASK_APP=hello
    
  3. 在虚拟环境里运行

    $ flask run
    

​ 命令行直接运行 flask --app hello run

📋文件命名为app.pywsgi.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 将以关键字参数形式在方法中获取

🛠️可供转换类型:

TypeInfo
string默认, 接收任意字符串除了 /
int接收正整数
float接收正数
pathstring 相同但接收 /
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, sessiong 对象,

也可调用方法 url_for()get_flashed_messages()

模板内字符串可通过 Markup()|safe 标记为安全的文字,不被 escape

>>> from markupsafe import Markup
>>> Markup('<strong>Hello %s!</strong>') % '<blink>hacker</blink>'
Markup('<strong>Hello &lt;blink&gt;hacker&lt;/blink&gt;!</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

  • 返回值是 dictlist ,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, 可将返回值类型定为 dictlist

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 文件

  1. 先安装库 pip install wheel

  2. 生成分发文件 python setup.py bdist_wheel

    该指令自动生成文件 dist/flaskr-1.0.0-py3-none-any.whl

    其格式为 {project name}-{version}-{python tag} -{abi tag}-{platform tag}

  3. 将该文件复制到生产环境,创建一个新的虚拟环境,并执行 pip install flaskr-1.0.0-py3-none-any.whl

  4. 安装后初始化数据库 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.textresponse.get_data(as_text=True)

4 | 表单数据

Form Data

将数据以 dict 类型传给参数 data 即可, Content-Type 会自动设成 multipart/form-dataapplication/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

将数据传参数 jsonContent-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

⚠️如果同时注册 HTTPExceptionException 的错误处理,出现 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_appg

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/

在请求过程中跟踪请求及数据,主要通过 requestsession 代理操作

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

  1. 每次请求开始前,Flask 调用 before_request() 注册的函数 , 如果其中由函数返回值,则其他函数被忽略,请求的视图函数也不会被调用
  2. 如果 before_request() 没有返回响应,请求的视图函数会被调用
  3. 视图的返回值被转为一个 Response 对象,并传入 after_request() 其中每个函数可一次加工响应对象
  4. 响应返回后,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

启用信号机制时,会发送以下信号:

  1. request_startedbefore_request() 在调用函数之前发送

  2. request_finishedafter_request()在调用函数后发送

  3. got_request_exception在开始处理异常时发送,但在寻找errorhandler()或调用之前

  4. 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=TruePERMANENT_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],各个功能可协程运作,可使用关键字 asyncawait

@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

  • 6
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值