目录

读《Flask Web 开发》

书籍信息


Flask 基础

上下文

current_app 应用上下文,当前应用实例。

g 应用上下文,处理请求时的临时存储对象,每次请求会重设对象。

request 请求对象,封装请求,常用的属性和方法有:

  • form 获取表单数据;
  • args 获取 URL 查询参数,参数值都是 string,args.get(key, default=None, type=None) 的 type 参数指定的函数用于类型强制转换,如果转换失败就会返回默认值;
  • cookies 获取请求 cookie;
  • headers 获取 HTTP header;
  • files 获取请求上传的文件;
  • get_json() 获取 body 中的 JSON 数据;
  • is_secure() 是否通过安全连接发送的请求;
  • host 请求定义的主机名;
  • blueprint 处理请求的 Flask 蓝本;
  • endpoint 处理请求的端点名称;
  • accept_mimetypes 获取请求客户端接受的响应格式,子属性 accept_jsonaccept_html,可用于内容协商;

session 请求上下文,用户会话。

@app.shell_context_processor 装饰器可以用来添加 shell 上下文。

@app.app_context_processor 装饰器装饰的函数的返回内容在所有模版中都可访问。

with app.app_context() 可以用来在应用上下文中执行代码。

route

请求钩子,使用 g 在请求钩子和视图函数之间共享数据。

  • before_request
  • before_app_request 用于 blueprint 定义 app 的 before_request
  • before_first_request
  • after_request
  • teardown_request

添加路由的方式:

  • @app.route 装饰器,endpoint 即为视图函数名称;
  • app.add_url() 方法,可以自定义 endpoint;

Flask 支持动态路由,内置动态路由的类型有 string、int、float 和 path。

查看路由表通过 app.url_map 属性读取。Flask 会自动为静态文件目录添加 static 路由,默认静态文件目录为 static

url_for('blueprint.endpoint', _external=True),第二个参数控制生成绝对链接,绝对链接主机名取决于 request。目标是动态路由时,通过关键字参数传入动态路由参数,也可以通过关键字参数传入 URL 查询参数。

如果没有定义 /api 但定义了 /api/,Flask 会自动将 /api 的请求重定向到 /api/,反之则不会重定向。

view 和 blueprint

view 函数处理逻辑并生成响应。

blueprint 是 view 的集合,可以设置 url 前缀,通过 app.register_blueprint() 注册到 Flask app。

blueprint 包中应该使用相对导入,以便解耦。

在 blueprint 中使用 url_for 指向同一 blueprint 的路由时, blueprint 名可以省略,仅使用 .endpoint 的形式。

response

通过 [响应内容,状态码,header] 的格式,在视图函数中直接返回,后两个参数可以省略,默认 200 状态码。

通过make_response 生成响应对象,响应对象常用属性和方法如下:

  • status_code 状态码;
  • headers HTTP 头部;
  • set_cookie() 设置 cookie;
  • delete_cookie() 删除 cookie;
  • set_data() 使用字符串或字节值设定响应;
  • get_data() 获取响应主体,常用于 Flask test_client 获取响应中数据;

通过 redirecturl_for 重定向;

通过 abort(xxx) 进行 xxx 状态码的错误处理。注意是通过抛出异常的方式,也就是说 abort 之后视图函数就返回了。abort 还可以传第二个参数,作为错误的描述。

根据客户端请求的格式改写响应类型,称为内容协商。浏览器一般不限制响应的格式。

error handler

  • @app.errorhandler(500) 为 app 定义错误处理函数
  • @blueprint.errorhandler(500) 为 blueprint 定义错误处理函数;
  • @blueprint.app_errorhandler(500) 为 app 定义错误处理函数;

除了使用状态码,也可以使用 Exception 作为参数,例如 @bp.errorhandler(ValidationError)

管理

@app.cli.command() 用于添加 flask 自定义命令。修饰的函数的 docstring 会成为 help 消息。

安装 python-dotenv 包后,flask 可以自动从 .env 文件中导入环境变量。

模板

Flask 使用 Jinja2 模板引擎。

常用变量过滤器有:

  • safe 不要在不可信的文本上使用 safe 过滤器。
  • capitalize
  • lower
  • upper
  • title
  • trim
  • striptags

常用控制结构有

  • import ... macro 宏定义和宏导入,宏的参数列表中不需要显式指定 **kwargs*args 即可接受关键字参数和可变参数;
  • include 'xx.html' 引用模板片段;
  • extends 'xx.html' 模板继承;

在模板继承中,常常使用 block 区块。如果要重新定义父模板的区块并保留父模板区块中的内容,可以在区块中使用 super() 函数。

跟模板有关的两个扩展是

  • Flask-Bootstrap 集成 Bootstrap 框架;
  • Flask-Moment 集成 Moment.js 处理时间展示。在引入 Moment.js 之后,立即使用 locale('es') 函数设置时间的语言偏好。

模版的默认目录为 templates,也可以配置 blueprint 使用专门的目录保存模版。搜索时会先搜索 app 的模版目录,再搜索 blueprint 的模版目录。

在模版的 for 循环结构内,可以访问一个特殊的 loop 变量,提供了 for 循环状态的一些信息。

  • loop.index 当前循环计数,从 1 开始;
  • loop.first 是否是循环的第一项;
  • loop.last 是否是循环的最后一项;

具体可见 Jinjia 文档

表单

Flask 使用 Flask-WTF 扩展快速实现表单。该扩展不需要在应用层初始化,但要配置密钥 SECRET_KEY

表单类继承自 FlaskForm 类,每个表单对象可以有多个多种类型的字段,每个字段可以绑定多个 validator,validator 也可以使用自定义函数。表单类构造函数可以接受一些参数,比如用户对象,并保存在环境变量中,供自定义的验证方法使用。

表单字段可以通过 coerce 参数指定函数进行强制类型转换。

将表单类实例化后可以在视图函数中使用,form.validate_on_submit() 用于判断表单提交且通过验证。

将表单对象传递给模版,在模版中构造表单,使用 form.hidden_tag() 可以为表单添加防止 CSRF 攻击的隐藏字段。也可以使用 Flask-Bootstrap 渲染整个 Flask-WTF 表单,只需要:

1
2
{% import 'bootstrap/wtf.html' %}
{{ wtf.quick_form(form) }}

在 POST 请求中,常使用 Post/重定向/Get 的模式提升用户体验,但不是所有场景都需要这样做,比如前后端分离模式下,以及 API 模式下就不应该返回 302。

Flask 调用 flash(msg, category="INFO") 函数可以向模版中传递消息,在模版中调用 get_flashed_messages(with_categories=False) 获取消息列表。category 是消息类别。

数据库

数据库分为关系型数据库和非关系型数据库。

  • 关系性数据库遵循 ACID 范式,存储数据高效且避免了重复,但是结构复杂。
  • 非关系型数据库又有键值对数据库、文档数据库等,查询操作简单,查询速度快,但增加了数据重复量。

Flask 使用的关系型数据库 ORM 框架主要是 Flask-SQLAlchemy,依赖 SQLAlchemy。除此之外,还需要安装相应数据库的驱动模块,例如 MySQL 数据库需要安装 pymysql,Postgres 需要安装 psycopg2。

Flask-SQLAlchemy

模型通过 __tablename__ 类变量指定数据库表名,一般使用复数命名。

常用的查询过滤器有:

  • filter()
  • filter_by()
  • limit()
  • offset()
  • order_by()
  • group_by()

常用的查询执行方法有:

  • all()
  • first()
  • first_or_404() Flask-SQLAlchemy 实现
  • get()
  • get_or_404() Flask-SQLAlchemy 实现
  • count()
  • paginate() Flask-SQLAlchemy 实现

分页对象属性和方法(分页对象是 Flask-SQLAlchemy 特有的):

  • items 记录集合
  • prev_num 上一页的页数
  • next_num 下一页的页数
  • has_prev 是否有上一页
  • has_next 是否有下一页
  • total
  • pages 总页数
  • page 当前页码
  • per_page
  • query 分页的源查询
  • iter_pages(left_edge=2, left_current=2, right_current=5, right_edge=2) 用户构建分页导航条数据的迭代器;
  • prev() 上一页的分页对象
  • next() 下一页的分页对象

常用的关系选项有:

  • backref 在关系的另一侧添加反向引用;
  • lazy 如何加载相关记录,最常用的是 dynamic(不加载记录,但提供加载记录的查询);
  • uselist 设置为 false 时,不使用列表,而使用标量值;
  • order_by 指定关系中记录的排序方式
  • secondary 指定多对多关系中关联表的名称;
  • primaryjoin 明确指定两个模型之间使用的联接条件,只在模糊的关系中需要指定;
  • secondaryjoin 指定多对多关系中的二级联结条件

涉及到外键定义和关系时,可以使用模型名,或者表名的字符串格式。

数据库字段的 default 参数可以接受函数作为默认值。

db.event.listen(Post.body, 'set', Post.on_changed_body) 用于增加数据库事件监听程序。

一对多关系

定义一对多关系:

1
2
3
4
5
6
7
8
9
# 在一的一侧使用 relationship
class Role(db.Model):
    # ...
    users = db.relationship('User', backref='role', lazy='dynamic')

# 在多的一侧使用 ForeignKey
class User(db.Model):
    # ...
    role_id = db.Column(db.Integer, db.ForeignKey(Role.id))

一对一关系

与定义一对多关系相似,只需要在调用 db.relationship() 的时候把 uselist 设为 False。

多对多关系

多对多关系需要指定一张关联表,拆分成原表与关联表的两个一对多关系(关联表是多的一侧)。

如果不需要操作关联表,可以这样定义:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
registrations = db.Table(
    'registrations',
    db.Column('student_id', db.Integer, db.ForeignKey('students.id')),
    db.Column('class_id', db.Integer, db.ForeignKey('classes.id')),
)

class Class(db.Model):
    __tablename__ = 'classes'
    # ...

class Student(db.Model):
    __tablename__ = 'students'
    # ...
    classes = db.relationship(
        Class,
        secondary=registrations,
        backref=db.backref('students', lazy='dynamic'),
        lazy='dynamic',
    )

此时使用 secondary 参数设置了关联表,SQLAlchemy 会自动接管这张表。

如果需要操作关联表,可以这样定义:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Follow(db.Model):
    __tablename__ = 'follows'
    follower_id = db.Column(db.Integer, db.ForeignKey('users.id'), primary_key=True)
    followed_id = db.Column(db.Integer, db.ForeignKey('users.id'), primary_key=True)
    timestamp = db.Column(db.DateTime, default=datetime.utcnow)

class User(UserMixin, db.Model):
    __tablename__ = 'users'
    followers = db.relationship(
        Follow,
        foreign_keys = [Follow.followed_id],
        backref=db.backref('followed', lazy='joined'),
        lazy='dynamic',
        cascade='all, delete-orphan',
    )
    followed = db.relationship(
        Follow,
        foreign_keys = [Follow.follower_id],
        backref=db.backref('followed', lazy='joined'),
        lazy='dynamic',
        cascade='all, delete-orphan',
    )

这是一个自引用关系,描述了用户的关注和 fans。要点如下:

  • 由于 Follow 表上有两个外键都指向 User,因此需要使用 foreign_keys 参数指定外键。
  • lazy 模式设置为 joined,可以实现立即从联接查询中加载相关对象,在一次数据库查询中完成这些操作。
  • cascade 参数,在关联表中删除记录后应该把指向该记录的实体也删除,因此使用了 delete-orphanall 表示除了 delete-orphan 之外的所有选项

数据库迁移框架

数据库迁移框架能够跟踪数据库模式的变化,例如 Alembic 和 Flask-Migrate,后者是前者的轻量级包装,与 Flask 命令 flask db 做了集成。常用命令有:

  • revision 手动创建迁移,需要分别实现 upgrade()downgrade() 函数;
  • migrate 自动创建迁移;
  • upgrade 把改动应用到数据库;
  • downgrade 还原前一个脚本对数据库的改动;
  • stamp 把数据库标记为已更新。

数据库模型在开发过程中可能时有变动,在提交到版本控制系统之前,可以合并迁移,避免产生大量无意义的小迁移脚本。

数据库迁移框架并不完美,在一些情况下是无法迁移的。例如在表中添加了一个非 null 而且没有默认值的字段就无法迁移,因为框架不知道该如何填充这个字段的数据。

电子邮件

Flask 发送邮件可以使用 Flask-Mail 扩展库,它实现了对标准库 smtplib 的包装,能更好的与 Flask 集成。

使用时需要添加几个 SMTP 服务器的配置项:

  • MAIL_SERVER
  • MAIL_PORT
  • MAIL_USE_TLS
  • MAIL_USE_SSL
  • MAIL_USERNAME
  • MAIL_PASSWORD

一个简单示例:

1
2
3
4
5
6
7
8
from flask_mail import Message

msg = Message('this is title', sender='[email protected]', recipients=['[email protected]'])
msg.body = 'This is the body'
msg.html = 'This is the <b>HTML</b> body'

with app.app_context():
    mail.send(msg)

Flask-Mail 的 send() 函数使用 current_app,因此要在激活的应用上下文中执行。

身份认证

身份认证主要使用到以下几个库:

  • Flask-Login 管理登录、登出用户,管理用户会话;
  • itsdangerous 生成并核对加密安全令牌;
  • Werkzeug 计算密码 hash 值并进行核对。

密码 hash 计算

Werkzeug 的 security 模块实现了密码 hash 的计算。提供了两个常用的函数:

  • generate_password_hash(password, method='pbkdf2:sha256', salt_length=8) 用于生成密码,生成的密码是加了 salt 的,即使两个用户使用相同的密码,hash 值也完全不一致。;
  • check_password_hash(hash, password) 用于验证密码;

实际开发中,可以通过 @property 特性,在模型层面,让数据库中的密码字段只读。

Flask-Login

Flask-Login 要求实现的用户模型的属性和方法:

  • is_authenticated 用户提供的登录凭据是否有效
  • is_active 是否允许用户登录
  • is_anonymous 普通用户返回 False,匿名用户返回 True
  • get_id() 返回用户的唯一标识符,使用 Unicode 编码字符串。

Flask-Login 的 UserMixin 类包含了以上的默认实现,用户模型继承该类即可:

1
2
class User(UserMixin, db.Model):
    # ...

使用 Flask-Login,需要实例化 LoginManager 类,并设置:

  • login_manager.login_view 属性指定登录页面的端点,用于匿名用户访问时重定向到该页面;
  • login_manager.anonymous_user 属性指定自定义的匿名用户类,定义匿名用户类的目的是,给用户类添加的自定义方法,在匿名类中也定义,就不需要判断用户是否登录即可调用;
  • @login_manager.user_loader 装饰器,指定加载用户的函数,函数参数是用户标识符(字符串),返回用户对象或 None。用于给上下文变量 current_user 赋值;
  • @login_required 装饰器,装饰受保护的路由,该装饰器要在 @app.route() 之后;
  • current_user 是由 Flask-Login 提供的上下文变量,可以在模版和视图函数中使用,是用户对象的轻度包装,获取真正的用户对象需要使用 current_user._get_current_object()

用户登录,调用 login_user(user, remember=False, duration=None, force=False, fresh=True) 函数,duration 用于设置 cookie 的有效期,或者通过 REMEMBER_COOKIE_DURATION 配置选项设置;

用户登出,调用 logout_user() 函数

安全令牌

使用 itsdangerous 库生成包含用户信息的安全令牌,可用于重置密码,电子邮件确认等场景;

itsdangerous 提供了多种生成令牌的方法。TimedJSONWebSignatureSerializer 类生成具有过期时间的 JSON Web 签名(JWS)。

1
2
3
4
5
from itsdangerous import TimedJSONWebSignatureSerializer

s = TimedJSONWebSignatureSerializer('secret_key', expires_in=3600)
token = s.dumps({'user_id': 12})
data = s.loads(token)

expires_in 参数指定令牌过期时间,单位为秒。如果令牌过期或者无效,会抛出异常。这种令牌在有效时间内,无法判断是否已经使用过。

Flask-HTTPAuth

Flask-HTTPAuth 常用于 API 请求认证,因为 API 是无状态的,不能使用 cookie 认证的方式。

使用 Flask-HTTPAuth 需要实例化 HTTPBasicAuth 类,并设置:

  • @auth.verify_password 装饰器,指定验证函数,函数只返回 True/False,获取到的用户需要存入 g 中;
  • @auth.login_required 装饰器,修饰需要登录保护的路由;

使用令牌的身份认证,用户名即令牌,密码为空。

  • 避免了总是发送敏感信息;
  • 令牌具有短暂有效期,降低泄漏后的安全隐患;
  • 必须使用登录凭据签发新令牌,不能使用旧令牌签发新令牌;

验证失败时,返回 401 状态码。

REST API

REST API 架构

RESTful 架构的特征:

  • 客户端-服务端有明确的界线;
  • 无状态;
  • 服务器响应可以标记为缓存或者不缓存;
  • 接口统一
  • 系统分层
  • 按需编程

资源 是 REST 架构风格的核心:

  • 使用唯一的 URL 表示每个资源;
  • 使用请求方法表示期望的操作;

常用请求方法:

  • GET 目标 URL 是单个资源或者资源集合,获取目标资源,响应状态码 200;
  • POST 目标 URL 是资源集合,用于新建资源,响应状态 201,并在 header 的 Location 中返回新资源 URL,也可以在响应的主体中包含该资源;
  • PUT 目标 URL 是单个资源,用于修改资源(也可以用来创建),响应状态码 200 或者 204;
  • DELETE 目标 URL 是单个资源或资源集合,用于删除资源,响应状态码 200 或者 204。

相关状态码说明:

  • HTTP 201,Created,请求成功并且创建了一个新资源;
  • HTTP 204,No Content,请求成功处理,但是返回的响应没有数据,比如资源被删除了。

设计良好的 RESTful API:

  • 在返回数据中包含完整的其他资源 URL,可以由客户端自己发掘新资源;
  • 使用版本区分 Web 服务所处理的 URL。

数据验证

客户端提供的数据可能无效、错误或者多余,要进行数据验证。

数据验证时,可以通过抛出异常的方式,将错误交给上层调用函数处理。同时,可以注册该种错误的异常处理函数,来返回给客户端错误消息。

测试

faker

使用 faker 库可以生成多种类型的虚拟数据

1
2
from faker import Faker
fake = Faker()
  • fake.email()
  • fake.user_name()
  • fake.name()
  • fake.city()
  • fake.past_date()
  • fake.text()

coverage

coverage 是一个代码覆盖度工具,用于统计单元测试检查了应用多少功能,并提供一份详细的报告。

coverage 提供了 CLI 命令 coverage。主要功能有:

  • coverage run cmd args 启动覆盖度检测引擎并运行,cmd args 是运行单元测试的命令;
  • coverage report 生成文本格式报告并输出到 stdout;
  • coverage html 生成 html 格式报告;
  • coverage erase 删除 coverage 缓存目录。

coverage 同时也可以在代码中进行控制。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
import coverage

# 创建覆盖度检测引擎
# branch 选项是否开启分支覆盖度分析,开启后会检查条件语句的 True 和 False 分支是否都执行了
# include 选项限制检测的文件在应用包内
COV = coverage.coverage(branch=True, include='app/*')

COV.start()

COV.stop()
COV.save()

# 会在 stdout 输出文本格式的报告
COV.report()

# directory 指定 html 报告保存路径
COV.html_report(directory=covdir)

COV.erase()

注意覆盖度指标无法表明项目中的代码多么健康,因为代码有没有缺陷还受其他因素的影响(例如测试的质量)。

Flask 测试客户端

Flask 内建了一个测试客户端,来向 Flask 应用发送请求,得到的结果是一个 Flask response 对象。

获得测试客户端。use_cookies 选项执行测试客户端是否使用 cookie。

1
2
app = create_app('testing')
client = app.test_client(use_cookies=True)

发起 GET 请求:

1
client.get('/')

发起 POST 请求(FORM):

1
client.post('/', data=data)

发起 POST 请求(JSON),测试客户端不会自动编码 JSON 数据:

1
2
3
4
5
client.post(
    '/',
    headers={'Content-Type': 'application/json'},
    data=json.dumps(data),
)

发请求时有以下参数:

  • follow_redirects 自动重定向请求
  • headers 指定 HTTP header

获得的响应是 response 对象,可以使用 get_data() 方法获取响应主体,默认情况下返回字节数组,传入 as_text=True 参数后返回字符串。

在 with 上下文中使用 client,可以访问上下文变量,例如 session

1
2
3
4
from flask_login import current_user

with client:
    assert current_user.id == 1

Selenium 端到端测试

Selenium 是一个浏览器自动化工具,支持多种主流 Web 浏览器。使用时,除了安装 selenium 外,还需要安装:

  • 相应的浏览器驱动,比如 Chome 的 ChromeDriver。
  • Selenium 的 Python 接口;

使用 Selenium 进行 Web 测试时,让应用运行在后台线程的开发服务器中,而测试运行在主线程中。通过实现并发送一个 HTTP 请求,来关闭服务器。这里会调用 Werkzeug web 服务器本身的停止选项。

1
2
shutdown = request.environ.get('werkzeug.server.shutdown')
shutdown()

启动 Chrome,headless 选项指定在无界面 Chrome 实例中运行,并执行所有操作:

1
2
3
4
5
from selenium import webdriver

options = webdriver.ChromeOptions()
options.add_argument('headless')
client = webdriver.Chrome(chrome_options=options)

测试过程中,可以禁止服务器的日志或只输出错误日志,保持输出简洁。

请求:

  • client.get()

寻找元素:

  • client.find_element_by_link_text()
  • client.find_element_by_name()

操作:

  • send_keys() 填写表单;
  • click() 点击

关闭 Chrome:

1
client.quit()

日志和性能

日志

应用日志

在应用启动过程中,Flask 会创建一个 logging.Logger 类实例,通过 app.logger 访问。

但在生产模式中,默认情况下没有配置日志的处理程序,如果不添加处理程序,就不会保存日志。

可以通过配置类的 init_app 方法添加处理程序。

1
2
3
4
5
6
@classmethod
def init_app(cls, app):
    from logging.handlers import SMTPHandler
    mail_handler = SMTPHandler(...)
    mail_handler.setLevel(logging.ERROR)
    app.logger.addHandler(mail_handler)

常用 handler 有:

  • SMTPHandler 发邮件
  • StreamHandler 日志内容输出到 stderr
  • SysLogHandler 日志发送给守护进程 syslog

Werkzeug 日志

获取 Werkzeug 日志对象:

1
2
import logging
logger = logging.getLogger('werkzeug')

性能分析

数据库性能分析

多数数据库查询语言都提供了 explain 语句,用于显示数据库执行查询时采取的步骤。

Flask-SQLAlchemy 提供了获取数据库查询的接口。

首先需要设置 SQLALCHEMY_RECORD_QUERIES 为 True,启用记录查询统计数据的功能。

然后在 @after_request 中获取数据库查询:

1
2
3
4
from flask_sqlalchemy import get_debug_queries

for query in get_debug_queries():
    # ...

查询对象拥有以下属性:

  • statement SQL 语句;
  • parameters SQL 语句使用的参数;
  • duration 查询持续的时间,单位为秒;
  • context 查询在源码中所处的位置;
  • start_time 执行查询时的时间
  • end_time 返回查询结果时的时间

根据 duration 属性筛选出慢查询,将信息写入到日志中,级别 WARNING。

源码分析

应当只在开发环境中分析源码,源码分析器会导致应用的运行速度比常规情况下慢的多。

Werkzeug 提供了一个源码分析器中间件,通过 Flask app 的 wsgi_app 属性依附到应用上。

1
2
3
4
5
6
7
from werkzeug.contrib.profiler import ProfilerMiddleware
app.wsgi_app = ProfilerMiddleware(
    app.wsgi_app,
    restrictions=[func_count],
    profile_dir=profile_dir,
)
app.run(debug=False)

应用启动后,控制台会显示每条请求的分析数据。

  • restrictions 参数的 func_count 指定了展示执行最慢的函数的数量;
  • profile_dir 参数指定了保存分析数据的路径,分析器输出的数据文件可以用于生成更详细的报告,如调用图等;

部署

Flask-SSLify

Flask-SSLify 扩展能够拦截发给 http:// 的请求,将其重定向到 https://,需要在应用层初始化。但是如果应用部署在 Nginx 等反向代理服务器后面时,就没有必要使用该库了,完全可以在 Nginx 配置 HTTPS 并进行重定向,同时应用使用 HTTP 与 Nginx 通信。

ProxyFix

使用反向代理服务器时,代理会设定一些自定义的 HTTP 头部,以用来标识请求的真实协议、地址等。

Werkzeug 提供的一个 WSGI 中间件 ProxyFix 能够检查代理服务器设定的这些自定义 HTTP 头部,并更新 request 对象。举例来说,request.is_secure 会反映客户端发给反向代理服务器的请求的加密状态,而不是代理服务器到应用的请求的加密状态。

1
2
from werkzeug.contrib.fixers import ProxyFix
app.wsgi_app = ProxyFix(app.wsgi_app)

Web 服务器

常用的 Web 服务器有 Gunicorn, uWSGI 和 Waitress。

Docker

使用 Docker 部署时,应当使用单独的用户运行应用程序,单独的用户在 Dockerfile 文件中新建。

1
2
RUN adduser -D flasky
USER flasky

adduser 命令的 -D 选项禁止命令提示输入密码。

另外程序启动命令我们使用一个 shell 脚本 boot.sh,因为启动时还需要进行初始化工作:

1
2
3
4
#!/bin/sh
source venv/bin/activate
flask deploy
exec gunicorn -b 0.0.0.0:5000 --access-logfile - --error-logfile - flasky:app

使用 exec 命令启动 Gunicorn 后,Gunicorn 的进程便取代了运行 boot.sh 文件的进程,成为主进程。

排查容器问题的常用策略是,创建一个特殊的容器,加载一些辅助工具,然后在 shell 会话中调用。

执行 docker run 命令时有 --link target:alias 选项,该选项把这个新容器与一个现有的容器连接起来。其值是以冒号分隔的两个名称,一个是目标容器的名称或 ID target,另一个是在当前容器中访问目标容器所用的别名 alias

使用容器编排时,尽可能让每个容器使用单独的 env 文件,隔离机密配置信息。

docker-compose 可以按照依赖顺序启动容器,但如 mysql 可能需要几秒钟才能启动,而 docker-compose 不会等待。因此连接外部服务器时要有重试机制。

docker-compose up -d --build 命令,--build 选项指明,应该在启动应用之前构建镜像。

docker system prune --volumes 命令,会删除所有不再使用的 image 或 volume,以及不在运行的容器。

在生产环境中使用 docker,需要考虑的安全问题:

  • 监控和提醒
  • 日志
  • 机密信息管理
  • 可靠性和伸缩性

其他

权限设计

书中把不同权限的权限值设置为 2 的幂,有三个好处:

  • 每种不同的权限组合对应的值都是唯一的,方便在数据库中存储;
  • 为权限组合增加权限只需要 self.permissions += perm,反之则 self.permissions -= perm
  • 检查是否拥有某项权限,只需要按位与即可,self.permissions & perm == perm

Bleach

Bleach 是使用 Python 实现的 HTML 清理程序。

  • bleach.clean(content, tags, strip=True) 删除不在白名单中的标签;
  • bleach.linkify(content) 把纯文本中的 URL 转换为合适的 <a> 链接;

markdown 相关库

  • Markdown:使用 Python 实现的服务端 Markdown 到 HTML 转换程序;
  • PageDown:使用 JavaScript 实现的客户端 Markdown 到 HTML 转换程序;
  • Flask-PageDown:为 Flask 包装的 PageDown,把 PageDown 集成到 Flask-WTF 中;

URL 片段

URL 片段 用于指定加载页面后滚动条所在的初始位置。格式为在 URL 查询参数之后,以 # 开头的锚点名。

例如:http://example.com/users/?page=1#tom

HTTPie

HTTPie 是一个 Python 编写的命令行 HTTP 请求工具,安装后会添加 CLI 命令 http。与 cURL 相比,其语法更加简洁,可读性更高,进行 API 请求也更便捷

发起 GET 请求:

1
http "httpbin.org/get?a=1&b=2"

发起 POST 请求(JSON):

1
http httpbin.org/post a=1 b=2

发起 POST 请求(FORM):

1
http --form httpbin.org/post a=1 b=2

发起带 Http AUTH 的请求:

1
http --auth username:password httpbin.org/get

发起 PUT 请求:

1
http --auth username:password PUT httpbin.org/put a=1 b=2

设置 header

1
http httpbin.org/headers User-Agent:asdf

Web 应用设计原则

业务逻辑应写入独立与应用上下文的模块中,在视图函数中的代码应该保持简洁,仅发挥粘合剂的作用。

如何优化数据库性能:

  • 查找数据库慢查询,优化索引;
  • 在应用和数据库之间加入缓存;