读《Flask Web 开发》
书籍信息
- 图灵社区:Flask Web 开发:基于 Python 的 Web 应用开发实战(第 2 版)
- 微信读书:Flask Web 开发:基于 Python 的 Web 应用开发实战(第 2 版)-微信读书
- 豆瓣:Flask Web 开发:基于 Python 的 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_json
和accept_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 获取响应中数据;
通过 redirect
和 url_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 表单,只需要:
|
|
在 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)
用于增加数据库事件监听程序。
一对多关系
定义一对多关系:
|
|
一对一关系
与定义一对多关系相似,只需要在调用 db.relationship()
的时候把 uselist 设为 False。
多对多关系
多对多关系需要指定一张关联表,拆分成原表与关联表的两个一对多关系(关联表是多的一侧)。
如果不需要操作关联表,可以这样定义:
|
|
此时使用 secondary
参数设置了关联表,SQLAlchemy 会自动接管这张表。
如果需要操作关联表,可以这样定义:
|
|
这是一个自引用关系,描述了用户的关注和 fans。要点如下:
- 由于 Follow 表上有两个外键都指向 User,因此需要使用
foreign_keys
参数指定外键。 lazy
模式设置为 joined,可以实现立即从联接查询中加载相关对象,在一次数据库查询中完成这些操作。cascade
参数,在关联表中删除记录后应该把指向该记录的实体也删除,因此使用了delete-orphan
,all
表示除了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
一个简单示例:
|
|
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,匿名用户返回 Trueget_id()
返回用户的唯一标识符,使用 Unicode 编码字符串。
Flask-Login 的 UserMixin
类包含了以上的默认实现,用户模型继承该类即可:
|
|
使用 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)。
|
|
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 库可以生成多种类型的虚拟数据
|
|
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 同时也可以在代码中进行控制。
|
|
注意覆盖度指标无法表明项目中的代码多么健康,因为代码有没有缺陷还受其他因素的影响(例如测试的质量)。
Flask 测试客户端
Flask 内建了一个测试客户端,来向 Flask 应用发送请求,得到的结果是一个 Flask response 对象。
获得测试客户端。use_cookies
选项执行测试客户端是否使用 cookie。
|
|
发起 GET 请求:
|
|
发起 POST 请求(FORM):
|
|
发起 POST 请求(JSON),测试客户端不会自动编码 JSON 数据:
|
|
发请求时有以下参数:
follow_redirects
自动重定向请求headers
指定 HTTP header
获得的响应是 response 对象,可以使用 get_data()
方法获取响应主体,默认情况下返回字节数组,传入 as_text=True
参数后返回字符串。
在 with 上下文中使用 client
,可以访问上下文变量,例如 session
:
|
|
Selenium 端到端测试
Selenium 是一个浏览器自动化工具,支持多种主流 Web 浏览器。使用时,除了安装 selenium 外,还需要安装:
- 相应的浏览器驱动,比如 Chome 的 ChromeDriver。
- Selenium 的 Python 接口;
使用 Selenium 进行 Web 测试时,让应用运行在后台线程的开发服务器中,而测试运行在主线程中。通过实现并发送一个 HTTP 请求,来关闭服务器。这里会调用 Werkzeug web 服务器本身的停止选项。
|
|
启动 Chrome,headless 选项指定在无界面 Chrome 实例中运行,并执行所有操作:
|
|
测试过程中,可以禁止服务器的日志或只输出错误日志,保持输出简洁。
请求:
client.get()
寻找元素:
client.find_element_by_link_text()
client.find_element_by_name()
操作:
send_keys()
填写表单;click()
点击
关闭 Chrome:
|
|
日志和性能
日志
应用日志
在应用启动过程中,Flask 会创建一个 logging.Logger 类实例,通过 app.logger
访问。
但在生产模式中,默认情况下没有配置日志的处理程序,如果不添加处理程序,就不会保存日志。
可以通过配置类的 init_app
方法添加处理程序。
|
|
常用 handler 有:
SMTPHandler
发邮件StreamHandler
日志内容输出到 stderrSysLogHandler
日志发送给守护进程 syslog
Werkzeug 日志
获取 Werkzeug 日志对象:
|
|
性能分析
数据库性能分析
多数数据库查询语言都提供了 explain
语句,用于显示数据库执行查询时采取的步骤。
Flask-SQLAlchemy 提供了获取数据库查询的接口。
首先需要设置 SQLALCHEMY_RECORD_QUERIES
为 True,启用记录查询统计数据的功能。
然后在 @after_request
中获取数据库查询:
|
|
查询对象拥有以下属性:
statement
SQL 语句;parameters
SQL 语句使用的参数;duration
查询持续的时间,单位为秒;context
查询在源码中所处的位置;start_time
执行查询时的时间end_time
返回查询结果时的时间
根据 duration
属性筛选出慢查询,将信息写入到日志中,级别 WARNING。
源码分析
应当只在开发环境中分析源码,源码分析器会导致应用的运行速度比常规情况下慢的多。
Werkzeug 提供了一个源码分析器中间件,通过 Flask app 的 wsgi_app
属性依附到应用上。
|
|
应用启动后,控制台会显示每条请求的分析数据。
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
会反映客户端发给反向代理服务器的请求的加密状态,而不是代理服务器到应用的请求的加密状态。
|
|
Web 服务器
常用的 Web 服务器有 Gunicorn, uWSGI 和 Waitress。
Docker
使用 Docker 部署时,应当使用单独的用户运行应用程序,单独的用户在 Dockerfile 文件中新建。
|
|
adduser 命令的 -D
选项禁止命令提示输入密码。
另外程序启动命令我们使用一个 shell 脚本 boot.sh
,因为启动时还需要进行初始化工作:
|
|
使用 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 请求:
|
|
发起 POST 请求(JSON):
|
|
发起 POST 请求(FORM):
|
|
发起带 Http AUTH 的请求:
|
|
发起 PUT 请求:
|
|
设置 header
|
|
Web 应用设计原则
业务逻辑应写入独立与应用上下文的模块中,在视图函数中的代码应该保持简洁,仅发挥粘合剂的作用。
如何优化数据库性能:
- 查找数据库慢查询,优化索引;
- 在应用和数据库之间加入缓存;