Contents
23.2.1. Flask 简介¶
1.安装¶
1.1创建应用目录¶
首先,要新建一个目录,存放从 GitHub 仓库中下载的示例代码。前言的“如何使用示例 代码”部分已经说过,最简单的方法是使用 Git 客户端直接从 GitHub 仓库中拉取代码。下 述命令从 GitHub 中下载示例代码,并检出应用的 1a 版本。我们将从这一版开始。
$ git clone https://github.com/miguelgrinberg/flasky.git
$ cd flasky
$ git checkout 1a
如果你不想使用 Git,打算自己动手输入或复制代码,像下面这样新建一个空应用目录即可:
$ mkdir flasky
$ cd flasky
1.2 虚拟环境¶
创建好应用目录之后,接下来该安装 Flask 了。安装 Flask 最便捷的方法是使用虚拟环境。 虚拟环境是 Python 解释器的一个私有副本,在这个环境中你可以安装私有包,而且不会影响系统中安装的全局 Python 解释器。
虚拟环境非常有用,可以避免你安装的 Python 版本和包与系统预装的发生冲突。为每个项目单独创建虚拟环境,可以保证应用只能访问所在虚拟环境中的包,从而保持全局解释器的干净整洁,使其只作为创建更多虚拟环境的源。
与直接使用系统全局的 Python 解释器相比,使用虚拟环境还有个好处,那就是不需要管理员权限。
1.3 在 Python 3 中创建虚拟环境¶
Python 3 和 Python 2 解释器创建虚拟环境的方法有所不同。
在 Python 3 中,虚拟环境由Python 标准库中的 venv 包原生支持。
如果你使用的是 Ubuntu Linux 系统预装的 Python 3,那么标准库中没有 venv包。请执行下述命令安装 python3-venv 包:
$ sudo apt-get install python3-venv
创建虚拟环境的命令格式如下:
$ python3 -m venv virtual-environment-name -m venv
选项的作用是以独立的脚本运行标准库中的 venv 包,后面的参数为虚拟环境的名称。
下面我们在 flasky 目录中创建一个虚拟环境。通常,虚拟环境的名称为 venv,不过你也可以使用其他名称。确保当前目录是 flasky,然后执行这个命令:
$ python3 -m venv venv
这个命令执行完毕后,flasky 目录中会出现一个名为 venv 的子目录,这里就是一个全新的虚拟环境,包含这个项目专用的 Python 解释器。
1.4 在 Python 2 中创建虚拟环境¶
Python 2 没有集成 venv 包。这一版 Python 解释器要使用第三方工具 virtualenv 创建虚拟环境。
确保当前目录是 flasky,然后根据自己使用的操作系统,执行下面两个命令中的一个。如果使用的是 Linux 或 macOS,执行的命令是:
$ sudo pip install virtualenv
如果使用的是微软 Windows 系统,打开命令提示符时要选择“以管理员身份运行”,然后执行这个命令:
$ pip install virtualenv
virtualenv 命令的参数是虚拟环境的名称。确保当前目录是 flasky,然后执行下述命令创建名为 venv 的虚拟环境:
$ virtualenv venv
New python executable in venv/bin/python2.7
Also creating executable in venv/bin/python
Installing setuptools, pip, wheel...done.
这个命令在当前目录中创建一个名为 venv 的子目录,虚拟环境相关的文件都在这个子目录中。
1.5 使用虚拟环境¶
若想使用虚拟环境,要先将其“激活”。如果你使用的是 Linux 或 macOS,可以通过下面的命令激活虚拟环境:
$ source venv/bin/activate
如果使用微软 Windows 系统,激活命令是:
$ venv\Scripts\activate
虚拟环境被激活后,里面的 Python 解释器的路径会添加到当前命令会话的 PATH 环境变量中,指明在什么位置寻找一众可执行文件。
为了提醒你已经激活了虚拟环境,激活虚拟环境的命令会修改命令提示符,加入环境名:
(venv) $
激活虚拟环境后,在命令提示符中输入 python ,将调用虚拟环境中的解释器,而不是系统全局解释器。
如果你打开了多个命令提示符窗口,在每个窗口中都要激活虚拟环境。
虽然多数情况下,为了方便,应该激活虚拟环境,但是不激活也能使用虚拟环境。
例如,为了启动 venv 虚拟环境中的 Python 控制台,
在 Linux 或macOS 中 可 以 执 行
venv/bin/python命 令,在 微 软 Windows 中 可 以 执 行
venv\Scripts\python命令。
虚拟环境中的工作结束后,在命令提示符中输入 deactivate ,还原当前终端会话的 PATH 环境变量,把命令提示符重置为最初的状态。
1.6 使用 pip 安装 Python 包¶
Python 包使用包管理器 pip 安装,所有虚拟环境中都有这个工具。与 python 命令类似,在命令提示符会话中输入 pip 将调用当前激活的虚拟环境中的 pip 工具。
若想在虚拟环境中安装 Flask,要确保 venv 虚拟环境已经激活,然后执行下述命令:
(venv) $ pip install flask
执行这个命令后,pip 不仅安装 Flask 自身,还会安装它的所有依赖。任何时候都可以使用pip freeze 命令查看虚拟环境中安装了哪些包:
(venv) $ pip freeze
click==8.1.3
Flask==2.2.2
importlib-metadata==4.12.0
itsdangerous==2.1.2
Jinja2==3.1.2
MarkupSafe==2.1.1
Werkzeug==2.2.2
zipp==3.8.1
pip freeze 命令的输出包含各个包的具体版本号。你安装的版本号可能与这里给出的不同。 要想验证 Flask 是否正确安装,可以启动 Python 解释器,尝试导入 Flask:
(venv) $ python
>>> import flask
>>>
vs code 格式化Python代码的快捷键如下:
On Windows Shift + Alt + F
On Mac Shift + Option + F
On Ubuntu Ctrl + Shift + I
2.应用的基本结构¶
2.1 初始化¶
所有 Flask 应用都必须创建一个应用实例。
Web 服务器使用一种名为 Web 服务器网关接口(WSGI,Web server gateway interface,读作“wiz-ghee”)的协议,把接收自客户端的所有 请求都转交给这个对象处理。
应用实例是 Flask 类的对象,通常由下述代码创建:
from flask import Flask
app = Flask(__name__)
Flask 类的构造函数只有一个必须指定的参数,即应用主模块或包的名称。在大多数应用中,Python 的 name 变量就是所需的值。
2.2 路由和视图函数¶
在 Flask 应用中定义路由的最简便方式,是使用应用实例提供的 app.route 装饰器。下面的例子说明了如何使用这个装饰器声明路由:
@app.route('/')
def index():
return '<h1>Hello World!</h1>'
Flask 还支持一种更传统的方式:使用 app.add_url_rule() 方法。
这个方法最简单的形式接受 3 个参数:URL、端点名和视图函数。
下述示例使用 app.add_url_rule() 方法注册 index() 函数,其作用与前例相同:
def index():
return '<h1>Hello World!</h1>'
app.add_url_rule('/', 'index', index)
index() 这样处理入站请求的函数称为视图函数。如果应用部署在域名为 www.example.com 的服务器上,在浏览器中访问 http://www.example.com 后,会触发服务器执行 index()函数。
这个函数的返回值称为响应,是客户端接收到的内容。如果客户端是 Web 浏览器,响应就是显示给用户查看的文档。视图函数返回的响应可以是包含 HTML 的简单字符串,也可以是后文将介绍的复杂表单。
可变的路由
下例定义的路由中就有一部分是可变的:
@app.route('/user/<name>')
def user(name):
return '<h1>Hello, {}!</h1>'.format(name)
路由 URL 中放在尖括号里的内容就是动态部分,任何能匹配静态部分的 URL 都会映射到这个路由上。
调用视图函数时,Flask 会将动态部分作为参数传入函数。在这个视图函数中, name 参数用于生成个性化的欢迎消息。
路由中的动态部分默认使用字符串,不过也可以是其他类型。
例如,路由 /user/只会匹配动态片段 id 为整数的 URL,
例如 /user/123。
Flask 支持在路由中使用 string 、int 、 float 和 path 类型。
path 类型是一种特殊的字符串,与 string 类型不同的是,它可以包含正斜线。
类型转化器 |
作用 |
|---|---|
缺省 |
字符串,不能有斜杠(‘/’) |
int: |
整数 |
float: |
浮点型 |
path: |
字符串,允许有斜杠(‘/’) |
2.3 一个完整的应用¶
hello.py:一个完整的 Flask 应用
from flask import Flask
app = Flask(__name__)
@app.route('/')
def index():
return '<h1>Hello World!</h1>'
如 果 你 已 经 从 GitHub 上 克 隆 了 这 个 应 用 的 Git 仓 库, 那 么 可 以 执 行 git checkout 2a 检出应用的这个版本。
2.4 Web开发服务器¶
Flask 应用自带 Web 开发服务器,通过 flask run 命令启动。这个命令在 FLASK_APP 环境变量指定的 Python 脚本中寻找应用实例。
若想启动前一节编写的 hello.py 应用,首先确保之前创建的虚拟环境已经激活,而且里面安装了 Flask。Linux 和 macOS 用户执行下述命令启动 Web 服务器:
(venv) $ export FLASK_APP=hello.py
(venv) $ flask run
* Serving Flask app "hello"
* Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)
微软 Windows 用户执行的命令和刚才一样,只不过设定 FLASK_APP 环境变量的方式不同:
(venv) $ set FLASK_APP=hello.py
(venv) $ flask run
* Serving Flask app "hello"
* Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)
2.5 动态路由¶
示例 2-2 hello.py:包含动态路由的 Flask 应用
from flask import Flask
app = Flask(__name__)
@app.route('/')
def index():
return '<h1>Hello World!</h1>'
@app.route('/user/<name>')
def user(name):
return '<h1>Hello, {}!</h1>'.format(name)
2.6 调试模式¶
Flask 应用可以在调试模式中运行。在这个模式下,开发服务器默认会加载两个便利的工具:重载器和调试器。
启用重载器后,Flask 会监视项目中的所有源码文件,发现变动时自动重启服务器。
在开发过程中运行启动重载器的服务器特别方便,因为每次修改并保存源码文件后,服务器都会自动重启,让改动生效。
调试模式默认禁用。若想启用,在执行 flask run 命令之前设定 FLASK_DEBUG=1 环境变量:
(venv) $ export FLASK_APP=hello.py
(venv) $ export FLASK_DEBUG=1
(venv) $ flask run
* Serving Flask app "hello"
* Forcing debug mode on
* Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)
* Restarting with stat
* Debugger is active!
* Debugger PIN: 273-181-528
在微软 Windows 中,环境变量使用 set 设置。
(venv) $ set FLASK_APP=hello.py
(venv) $ set FLASK_DEBUG=1
(venv) $ flask run
使用 app.run() 方法启动服务器时,不会用到 FLASK_APP 和 FLASK_DEBUG 环境变量。
若想以编程的方式启动调试模式,就使用 app.run(debug=True) 。
千万不要在生产服务器中启用调试模式。客户端通过调试器能请求执行远程代码,因此可能导致生产服务器遭到攻击。作为一种简单的保护措施,启动调试模式时可以要求输入 PIN 码,执行 flask run 命令时会打印在控制台中。
2.7 命令行选项¶
flask 命令支持一些选项。执行 flask –help ,或者执行 flask 而不提供任何参数,可以查看哪些选项可用:
(venv) $ flask --help
Usage: flask [OPTIONS] COMMAND [ARGS]...
A general utility script for Flask applications.
An application to load must be given with the '--app' option, 'FLASK_APP'
environment variable, or with a 'wsgi.py' or 'app.py' file in the current
directory.
Options:
-e, --env-file FILE Load environment variables from this file. python-
dotenv must be installed.
-A, --app IMPORT The Flask application or factory function to load, in
the form 'module:name'. Module can be a dotted import
or file path. Name is not required if it is 'app',
'application', 'create_app', or 'make_app', and can be
'name(args)' to pass arguments.
--debug / --no-debug Set debug mode.
--version Show the Flask version.
--help Show this message and exit.
Commands:
routes Show the routes for the app.
run Run a development server.
shell Run a shell in the app context.
lask shell 命令在应用的上下文中打开一个 Python shell 会话。
在这个会话中可以运行维护任务或测试,也可以调试问题。
几章之后将举例说明这个命令的用途。flask run 命令我们已经用过,从名称可以看出,它的作用是在 Web 开发服务器中运行应用。
这个命令有多个参数:
(venv) $ flask run --help
Usage: flask run [OPTIONS]
Run a local development server.
This server is for development purposes only. It does not provide the
stability, security, or performance of production WSGI servers.
The reloader and debugger are enabled by default with the '--debug' option.
Options:
-h, --host TEXT The interface to bind to.
-p, --port INTEGER The port to bind to.
--cert PATH Specify a certificate file to use HTTPS.
--key FILE The key file to use when specifying a
certificate.
--reload / --no-reload Enable or disable the reloader. By default
the reloader is active if debug is enabled.
--debugger / --no-debugger Enable or disable the debugger. By default
the debugger is active if debug is enabled.
--with-threads / --without-threads
Enable or disable multithreading.
--extra-files PATH Extra files that trigger a reload on change.
Multiple paths are separated by ';'.
--exclude-patterns PATH Files matching these fnmatch patterns will
not trigger a reload on change. Multiple
patterns are separated by ';'.
--help Show this message and exit.
–host 参数特别有用,它告诉 Web 服务器在哪个网络接口上监听客户端发来的连接。
默认情况下,Flask 的 Web 开发服务器监听 localhost 上的连接,因此服务器只接受运行服务器的计算机发送的连接。
下述命令让 Web 服务器监听公共网络接口上的连接,因此同一网络中的其他计算机发送的连接也能接收到:
(venv) $ flask run --host 0.0.0.0
* Serving Flask app 'hello.py'
* Running on all addresses (0.0.0.0)
* Running on http://127.0.0.1:5000
* Running on http://172.16.3.134:5000
2.8 请求–响应循环¶
2.8.1 应用和请求上下文¶
Flask 从客户端收到请求时,要让视图函数能访问一些对象,这样才能处理请求。
请求对象就是一个很好的例子,它封装了客户端发送的 HTTP 请求。
要想让视图函数能够访问请求对象,一种直截了当的方式是将其作为参数传入视图函数, 不过这会导致应用中的每个视图函数都多出一个参数。
除了访问请求对象,如果视图函数在处理请求时还要访问其他对象,情况会变得更糟。
为了避免大量可有可无的参数把视图函数弄得一团糟,Flask 使用上下文临时把某些对象变为全局可访问。有了上下文,便可以像下面这样编写视图函数:
from flask import request
@app.route('/')
def index():
user_agent = request.headers.get('User-Agent')
return '<p>Your browser is {}</p>'.format(user_agent)
注意,在这个视图函数中我们把 request 当作全局变量使用。事实上, request 不可能是全局变量。
试想,在多线程服务器中,多个线程同时处理不同客户端发送的不同请求时,
每个线程看到的 request 对象必然不同。Flask 使用上下文让特定的变量在一个线程中全局可访问,与此同时却不会干扰其他线程。
在 Flask 中有两种上下文:应用上下文和请求上下文。
Flask上下文全局变量
变量名 上下文 说 明
current_app 应用上下文 当前应用的应用实例
g 应用上下文 处理请求时用作临时存储的对象,每次请求都会重设这个变量
request 请求上下文 请求对象,封装了客户端发出的 HTTP 请求中的内容
session 请求上下文 用户会话,值为一个字典,存储请求之间需要“记住”的值
2.8.2 请求分派¶
应用收到客户端发来的请求时,要找到处理该请求的视图函数。
为了完成这个任务,Flask会在应用的 URL 映射中查找请求的 URL。
URL 映射是 URL 和视图函数之间的对应关系。 Flask 使用 app.route 装饰器或者作用相同的 app.add_url_rule() 方法构建映射。
要想查看 Flask 应用中的 URL 映射是什么样子,可以在 Python shell 中审查为 hello.py 生成的映射。测试之前,请确保你激活了虚拟环境:
(venv) $ python
>>> from hello import app
>>> app.url_map
Map([<Rule '/' (HEAD, OPTIONS, GET) -> index>,
<Rule '/static/<filename>' (HEAD, OPTIONS, GET) -> static>,
<Rule '/user/<name>' (HEAD, OPTIONS, GET) -> user>])
/ 和 /user/<name> 路由在应用中使用 app.route 装饰器定义。
/static/<filename>路由是Flask 添加的特殊路由,用于访问静态文件。
2.8.3 请求对象¶
我们知道,Flask 通过上下文变量 request 对外开放请求对象。
这个对象非常有用,包含客户端发送的 HTTP 请求的全部信息。Flask 请求对象中最常用的属性和方法见表
Flask请求对象
属性或方法 说 明
form 一个字典,存储请求提交的所有表单字段
args 一个字典,存储通过 URL 查询字符串传递的所有参数
values 一个字典, form 和 args 的合集
cookies 一个字典,存储请求的所有 cookie
headers 一个字典,存储请求的所有 HTTP 首部
files 一个字典,存储请求上传的所有文件
get_data() 返回请求主体缓冲的数据
get_json() 返回一个 Python 字典,包含解析请求主体后得到的 JSON
blueprint 处理请求的 Flask 蓝本的名称;
endpoint 处理请求的 Flask 端点的名称;Flask 把视图函数的名称用作路由端点的名称
method HTTP 请求方法,例如 GET 或 POST
scheme URL 方案( http 或 https )
is_secure() 通过安全的连接(HTTPS)发送请求时返回 True
host 请求定义的主机名,如果客户端定义了端口号,还包括端口号
path URL 的路径部分
query_string URL 的查询字符串部分,返回原始二进制值
full_path URL 的路径和查询字符串部分
url 客户端请求的完整 URL
base_url 同 url ,但没有查询字符串部分
remote_addr 客户端的 IP 地址
environ 请求的原始 WSGI 环境字典
2.8.4 请求钩子¶
请求钩子通过装饰器实现。Flask 支持以下 4 种钩子。
before_first_request:注册一个函数,在处理第一个请求之前运行
before_request:注册一个函数,在处理每一个请求的时候运行
after_request:注册一个函数,如果没有未处理的异常抛出,在每次请求之后运行
teardown_request:注册一个函数,即使有未处理的异常抛出,也在每次请求之后运行
在请求钩子函数和视图函数之间共享数据一般使用上下文全局变量g。例如,before_request 处理程序可以从数据库中加载已登录用户,并将其保存到g.user 中。随后调用视图函数时,视图函数再使用g.user 获取用户,钩子的具体用法会在后面详细介绍。
2.9 响应¶
Flask 调用视图函数后,会将其返回值作为响应的内容。
多数情况下,响应就是一个简单的字符串,作为 HTML 页面回送客户端。
但 HTTP 协议需要的不仅是作为请求响应的字符串。
HTTP 响应中一个很重要的部分是状态码,Flask 默认设为 200,表明请求已被成功处理。 如果视图函数返回的响应需要使用不同的状态码,可以把数字代码作为第二个返回值,添加到响应文本之后。
例如,下述视图函数返回 400 状态码,表示请求无效:
@app.route('/')
def index():
return '<h1>Bad Request</h1>', 400
如果不想返回由 1 个、2 个或 3 个值组成的元组,Flask 视图函数还可以返回一个响应对象。
make_response() 函数可接受 1 个、2 个或 3 个参数(和视图函数的返回值一样),然后返回一个等效的响应对象。
有时我们需要在视图函数中生成响应对象,然后在响应对象上调用各个方法,进一步设置响应。下例创建一个响应对象,然后设置 cookie:
from flask import make_response
@app.route('/')
def index():
response = make_response('<h1>This document carries a cookie!</h1>')
response.set_cookie('answer', '42')
return response
Flask响应对象
属性或方法 说 明
status_code HTTP 数字状态码
headers 一个类似字典的对象,包含随响应发送的所有首部
set_cookie() 为响应添加一个 cookie
delete_cookie() 删除一个 cookie
content_length 响应主体的长度
content_type 响应主体的媒体类型
set_data() 使用字符串或字节值设定响应
get_data() 获取响应主体
响应有个特殊的类型,称为重定向。这种响应没有页面文档,只会告诉浏览器一个新URL,用以加载新页面。重定向经常在 Web 表单中使用。
重定向的状态码通常是 302,在 Location 首部中提供目标 URL。
重定向响应可以使用 3个值形式的返回值生成,也可在响应对象中设定。不过,由于使用频繁,Flask 提供了redirect() 辅助函数,用于生成这种响应:
from flask import redirect
@app.route('/home')
def redirectbaidu():
return redirect('http://www.baidu.com')
还有一种特殊的响应由 abort() 函数生成,用于处理错误。在下面这个例子中,如果 URL中动态参数 id 对应的用户不存在,就返回状态码 404:
from flask import abort
@app.route('/user/<id>')
def get_user(id):
user = load_user(id)
if not user:
abort(404)
return '<h1>Hello, {}</h1>'.format(user.name)
注意, abort() 不会把控制权交还给调用它的函数,而是抛出异常。
2.10 Flask扩展¶
Flask 的设计考虑了可扩展性,故而没有提供一些重要的功能,例如数据库和用户身份验证,所以开发者可以自由选择最适合应用的包,或者按需求自行开发。
社区成员开发了大量不同用途的 Flask 扩展,如果这还不能满足需求,任何 Python 标准包或代码库都可以使用。
3.模板¶
3.1 Jinja2模板引擎¶
templates/index.html:Jinja2 模板
<h1>Hello World!</h1>
templates/user.html:Jinja2 模板
<h1>Hello, {{ name }}!</h1>
3.1.1 渲染模板¶
hello.py:渲染模板
from flask import Flask, render_template
app = Flask(__name__)
@app.route('/')
def index():
return render_template('index.html')
@app.route('/user/<name>')
def user(name):
return render_template('user.html', name=name)
3.1.2 变量¶
Jinja2 能识别所有类型的变量,甚至是一些复杂的类型,例如列表、字典和对象。
下面是在模板中使用变量的一些示例:
<p>A value from a dictionary: {{ mydict['key'] }}.</p>
<p>A value from a list: {{ mylist[3] }}.</p>
<p>A value from a list, with a variable index: {{ mylist[myintvar] }}.</p>
<p>A value from an object's method: {{ myobj.somemethod() }}.</p>
变量的值可以使用过滤器修改。过滤器添加在变量名之后,二者之间以竖线分隔。
例如,下述模板把 name 变量的值变成首字母大写的形式:
Hello, {{ name|capitalize }}
列出了 Jinja2 提供的部分常用过滤器。
过滤器名 |
说明 |
|---|---|
safe |
渲染值时不转义 |
capitalize |
把值的首字母转换成大写,其他字母转换成小写 |
lower |
把值转换成小写形式 |
upper |
把值转换成大写形式 |
title |
把值中每个单词的首字母都转换成大写 |
trim |
把值的首尾空格删掉 |
striptags |
渲染之前把值中所有的 HTML 标签都删掉 |
safe 过滤器值得特别说明一下。
默认情况下,出于安全考虑,Jinja2
会转义所有变量。例如,如果一个变量的值为 ‘<h1>Hello</h1>’ ,Jinja2
会将其渲染成 ‘<h1>Hello</h1>’ ,浏览器能显示这个 h1
元素,
但不会解释它。很多情况下需要显示变量中存储的HTML 代码,这时就可使用 safe 过滤器。
千万别在不可信的值上使用 safe 过滤器,例如用户在表单中输入的文本。
完整的过滤器列表可在 Jinja2 文档(http://jinja.pocoo.org/docs/2.10/templates/#builtin-filters)中查看。
3.1.3 控制结构¶
Jinja2 提供了多种控制结构,可用来改变模板的渲染流程。
本节通过简单的例子介绍其中最有用的一些控制结构。 下面这个例子展示如何在模板中使用条件判断语句:
{% if user %}
Hello, {{ user }}!
{% else %}
Hello, Stranger!
{% endif %}
另一种常见需求是在模板中渲染一组元素。下例展示了如何使用 for 循环实现这一需求:
<ul>
{% for comment in comments %}
<li>{{ comment }}</li>
{% endfor %}
</ul>
Jinja2 还支持宏。宏类似于 Python 代码中的函数。例如:
{% macro render_comment(comment) %}
<li>{{ comment }}</li>
{% endmacro %}
<ul>
{% for comment in comments %}
{{ render_comment(comment) }}
{% endfor %}
</ul>
为了重复使用宏,可以把宏保存在单独的文件中,然后在需要使用的模板中导入:
{% import 'macros.html' as macros %}
<ul>
{% for comment in comments %}
{{ macros.render_comment(comment) }}
{% endfor %}
</ul>
需要在多处重复使用的模板代码片段可以写入单独的文件,再引入所有模板中,以避免重复:
{% include 'common.html' %}
另一种重复使用代码的强大方式是模板继承,这类似于 Python 代码中的类继承。
首先,创建一个名为 base.html 的基模板:
<html>
<head>
{% block head %}
<title>{% block title %}{% endblock %} - My Application</title>
{% endblock %}
</head>
<body>
{% block body %}
{% endblock %}
</body>
</html>
基模板中定义的区块可在衍生模板中覆盖。
Jinja2 使用 block 和 endblock 指令在基模板中定义内容区块。在本例中,我们定义了名为 head 、 title 和 body 的区块。注意, title 包含在 head 中。下面这个示例是基模板的衍生模板:
{% extends "base.html" %}
{% block title %}Index{% endblock %}
{% block head %}
{{ super() }}
<style>
</style>
{% endblock %}
{% block body %}
<h1>Hello, World!</h1>
{% endblock %}
extends 指令声明这个模板衍生自 base.html。在 extends 指令之后,基模板中的 3 个区块被重新定义,模板引擎会将其插入适当的位置。
如果基模板和衍生模板中的同名区块中都有内容,衍生模板中的内容将显示出来。
在衍生模板的区块里可以调用 super() ,引用基模板中同名区块里的内容。上例中的 head 区块就是这么做的。
3.2 使用Flask-Bootstrap集成Bootstrap¶
Bootstrap 是 Twitter 开发的一个开源 Web 框架,它提供的用户界面组件可用于创建整洁且具有吸引力的网页,而且兼容所有现代的桌面和移动平台 Web 浏览器。
Bootstrap 是客户端框架,因此不会直接涉及服务器。服务器需要做的只是提供引用了 Bootstrap 层叠样式表(CSS,cascading style sheet)和 JavaScript 文件的 HTML 响应,并在HTML、CSS 和 JavaScript 代码中实例化所需的用户界面元素。
这些操作最理想的执行场所就是模板。 要想在应用中集成 Bootstrap,最直接的方法是根据 Bootstrap 文档中的说明对 HTML 模板进行必要的改动。
不过,这个任务使用 Flask 扩展处理要简单得多,而且相关的改动不会导致主逻辑凌乱不堪。
我们要使用的扩展是 Flask-Bootstrap,它可以使用 pip 安装:
(venv) $ pip install flask-bootstrap
Flask 扩展在创建应用实例时初始化。
from flask import Flask, render_template
from flask_bootstrap import Bootstrap
app = Flask(__name__)
bootstrap = Bootstrap(app)
@app.route('/')
def index():
return render_template('index.html')
@app.route('/user/<name>')
def user(name):
return render_template('user.html', name=name)
扩展通常从 flask_<name>包中导入,其中
<name>是扩展的名称。多数 Flask
扩展采用两种初始化方式中的一种。初始化扩展的方式是把应用实例作为参数传给构造函数。
初始化 Flask-Bootstrap 之后,就可以在应用中使用一个包含所有 Bootstrap 文件和一般结构的基模板。
应用利用 Jinja2 的模板继承机制来扩展这个基模板。
templates/user.html:使用 Flask-Bootstrap 的模板
{% extends "bootstrap/base.html" %}
{% block title %}Flasky{% endblock %}
{% block navbar %}
<div class="navbar navbar-inverse" role="navigation">
<div class="container">
<div class="navbar-header">
<button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="navbar-brand" href="/">Flasky</a>
</div>
<div class="navbar-collapse collapse">
<ul class="nav navbar-nav">
<li><a href="/">Home</a></li>
</ul>
</div>
</div>
</div>
{% endblock %}
{% block content %}
<div class="container">
<div class="page-header">
<h1>Hello, {{ name }}!</h1>
</div>
</div>
{% endblock %}
Jinja2 中的 extends 指令从 Flask-Bootstrap 中导入 bootstrap/base.html,从而实现模板继承。 Flask-Bootstrap 的基模板提供了一个网页骨架,引入了 Bootstrap 的所有 CSS 和 JavaScript文件。
上面这个 user.html 模板定义了 3 个区块,分别名为 title 、 navbar 和 content 。这些区块都是基模板提供的,可在衍生模板中重新定义。
title 区块的作用很明显,其中的内容会出现在渲染后的 HTML 文档头部,放在
<title> 标签中。 navbar 和 content
这两个区块分别表示页面中的导航栏和主体内容。
Flask-Bootstrap 的 base.html 模板还定义了很多其他区块,都可在衍生模板中使用。
表3-2:Flask-Bootstrap基模板中定义的区块
区块名 |
说明 |
|---|---|
doc |
整个 HTML 文档 |
html_attribs |
|
html |
|
head |
|
title |
|
metas |
一组 |
styles |
CSS 声明 |
body_attribs |
|
body |
|
navbar |
用户定义的导航栏 |
content |
用户定义的页面内容 |
scripts |
文档底部的 JavaScript 声明 |
3.3 自定义错误页面¶
如果你在浏览器的地址栏中输入了无效的路由,会看到一个状态码为 404 的错误页面。
与使用 Bootstrap 的页面相比,现在这个错误页面太简陋、平庸,而且与现有页面不一致。 像常规路由一样,Flask 允许应用使用模板自定义错误页面。
最常见的错误代码有两个:404,客户端请求未知页面或路由时显示;500,应用有未处理的异常时显示。示例 3-6 使 用 app.errorhandler 装饰器为这两个错误提供自定义的处理函数。
hello.py:自定义错误页面
@app.errorhandler(404)
def page_not_found(e):
return render_template('404.html'), 404
@app.errorhandler(500)
def internal_server_error(e):
return render_template('500.html'), 500
与视图函数一样,错误处理函数也返回一个响应。此外,错误处理函数还要返回与错误对应的数字状态码。状态码可以直接通过第二个返回值指定。
错误处理函数中引用的模板也需要我们编写。这些模板应该和常规页面使用相同的布局,因此要有一个导航栏和显示错误消息的页头。
编写这些模板最直接的方法是复制 templates/user.html,分别创建 templates/404.html 和templates/500.html,然后把这两个文件中的页头元素改为相应的错误消息。但是这么做会带来很多重复劳动。
Jinja2 的模板继承机制可以帮助我们解决这一问题。
Flask-Bootstrap 提供了一个具有页面基本布局的基模板,同样,应用也可以定义一个具有统一页面布局的基模板,其中包含导航栏,而页面内容则留给衍生模板定义。
示例 3-7 展示了 templates/base.html 的内容,这是一个继承自 bootstrap/base.html 的新模板,其中定义了导航栏。这个模板本身也可作为其他模板的二级基模板,例如 templates/user.html、templates/404.html 和 templates/500.html。
templates/base.html:包含导航栏的应用基模板
{% extends "bootstrap/base.html" %}
{% block title %}Flasky{% endblock %}
{% block navbar %}
<div class="navbar navbar-inverse" role="navigation">
<div class="container">
<div class="navbar-header">
<button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="navbar-brand" href="/">Flasky</a>
</div>
<div class="navbar-collapse collapse">
<ul class="nav navbar-nav">
<li><a href="/">Home</a></li>
</ul>
</div>
</div>
</div>
{% endblock %}
{% block content %}
<div class="container">
{% block page_content %}{% endblock %}
</div>
{% endblock %}
这个模板中的 content 区块里只有一个
容器,其中包含一个新的空区块,名为 page_ content ,区块中的内容由衍生模板定义。 现在,应用中的模板继承自这个模板,而不直接继承自 Flask-Bootstrap 的基模板。通过继 承 templates/base.html 模板编写自定义的 404 错误页面就简单了
templates/404.html:使用模板继承机制自定义 404 错误页面
{% extends "base.html" %}
{% block title %}Flasky - Page Not Found{% endblock %}
{% block page_content %}
<div class="page-header">
<h1>Not Found</h1>
</div>
{% endblock %}
templates/user.html 模板也可以通过继承这个基模板来简化内容
{% extends "base.html" %}
{% block title %}Flasky{% endblock %}
{% block page_content %}
<div class="page-header">
<h1>Hello, {{ name }}!</h1>
</div>
{% endblock %}
3.4 链接¶
任何具有多个路由的应用都需要可以连接不同页面的链接,例如导航栏。
在模板中直接编写简单路由的 URL 链接不难,但对于包含可变部分的动态路由,在模板中构建正确的 URL 就很困难了。
而且,直接编写 URL 会对代码中定义的路由产生不必要的依赖关系。
如果重新定义路由,模板中的链接可能会失效。为了避免这些问题,Flask 提供了 url_for() 辅助函数,它使用应用的 URL 映射中保存的信息生成 URL。
url_for() 函数最简单的用法是以视图函数名(或者 app.add_url_route() 定义路由时使用的端点名)作为参数,返回对应的 URL。
例如,在当前版本的 hello.py 应用中调用url_for(‘index’) 得到的结果是 / ,即应用的根 URL。调用 url_for(‘index’, _external=True)返回的则是绝对地址,在这个示例中是 http://localhost:5000/。
生成连接应用内不同路由的链接时,使用相对地址就足够了。如果要生成在浏览器之外使用的链接,则必须使用绝对地址,例如在电子邮件中发送的链接。
使用 url_for() 生成动态 URL 时,将动态部分作为关键字参数传入。例如, url_for(‘user’,name=‘john’, _external=True) 的返回结果是 http://localhost:5000/user/john。
传给 url_for() 的关键字参数不仅限于动态路由中的参数,非动态的参数也会添加到查询字符串中。
例如, url_for(‘user’, name=‘john’, page=2, version=1) 的返回结果是 /user/john?page=2&version=1。
3.5 静态文件¶
Web 应用不是仅由 Python 代码和模板组成。
多数应用还会使用静态文件,例如模板中HTML 代码引用的图像、JavaScript 源码文件和 CSS。
你可能还记得,在第 2 章中审查 hello.py 应用的 URL 映射时,其中有一个 static 路由。 这是 Flask 为了支持静态文件而自动添加的,这个特殊路由的 URL 是 /static/ 。 例如,调用 url_for(‘static’, filename=‘css/styles.css’, _external=True) 得到的结果是 http://localhost:5000/static/css/styles.css。
默认设置下,Flask 在应用根目录中名为 static 的子目录中寻找静态文件。
如果需要,可在static 文件夹中使用子文件夹存放文件。
服务器收到映射到 static 路由上的 URL 后,生成的响应包含文件系统中对应文件里的内容。
示例 3-10 展示了如何在应用的基模板中引入 favicon.ico 图标。这个图标会显示在浏览器的地址栏中。
templates/base.html:定义收藏夹图标
{% block head %}
{{ super() }}
<link rel="shortcut icon" href="{{ url_for('static', filename='favicon.ico') }}" type="image/x-icon">
<link rel="icon" href="{{ url_for('static', filename='favicon.ico') }}" type="image/x-icon">
{% endblock %}
这个图标的声明插入 head 区块的末尾。注意,为了保留基模板中这个区块里的原始内容,我们调用了 super() 。
3.6使用Flask-Moment本地化日期和时间¶
如果 Web 应用的用户来自世界各地,那么处理日期和时间可不是一个简单的任务。
服务器需要统一时间单位,这和用户所在的地理位置无关,所以一般使用协调世界时(UTC,coordinated universal time)。
不过用户看到 UTC 格式的时间会感到困惑,他们更希望看到当地时间,而且采用当地惯用的格式。
要想在服务器上只使用 UTC 时间,一个优雅的解决方案是,把时间单位发送给 Web 浏览器,转换成当地时间,然后用 JavaScript 渲染。
Web 浏览器可以更好地完成这一任务,因为它能获取用户计算机中的时区和区域设置。
有一个使用 JavaScript 开发的优秀客户端开源库,名为 Moment.js,它可以在浏览器中渲染日期和时间。
Flask-Moment 是一个 Flask 扩展,能简化把 Moment.js 集成到 Jinja2 模板中的过程。
Flask-Moment 使用 pip 安装:
(venv) $ pip install flask-moment
这个扩展的初始化方法与 Flask-Bootstrap 类似,所需的代码如示例 3-11 所示。
from flask_moment import Moment
app = Flask(__name__)
moment = Moment(app)
除了 Moment.js,Flask-Moment 还依赖 jQuery.js。因此,要在 HTML 文档的某个地方引入 这两个库,可以直接引入,这样可以选择使用哪个版本,也可以使用扩展提供的辅助函 数,从内容分发网络(CDN,content delivery network)中引入通过测试的版本。
Bootstrap已经引入了 jQuery.js,因此只需引入 Moment.js 即可。
参考文献:
4.Web表单¶
Flask请求对象包含客户端发出的所有请求信息。其中,request.form 能获取POST 请求中提交的表单数据。尽管Flask 的请求对象提供的信息足够用于处理Web 表单,但有些任务很单调,而且要重复操作。比如,生成表单的HTML 代码和验证提交的表单数据。
Flask-WTF(http://pythonhosted.org/Flask-WTF/)扩展可以把处理Web 表单的过程变成一种愉悦的体验。
这个扩展对独立的WTForms(http://wtforms.simplecodes.com)包进行了包装,方便集成到Flask 程序中。Flask-WTF 及其依赖可使用pip 安装:
(venv) $ pip install flask-wtf
4.1 配置¶
与其他多数扩展不同,Flask-WTF 无须在应用层初始化,但是它要求应用配置一个密钥。
密钥是一个由随机字符构成的唯一字符串,通过加密或签名以不同的方式提升应用的安全 性。
Flask 使用这个密钥保护用户会话,以防被篡改。每个应用的密钥应该不同,而且不 能让任何人知道。示例 4-1 展示如何在 Flask 应用中配置密钥。
示例 4-1 hello.py:配置 Flask-WTF
app = Flask(__name__)
app.config['SECRET_KEY'] = 'hard to guess string'
app.config 字典可用于存储 Flask、扩展和应用自身的配置变量。使用标准的字典句法就 能把配置添加到 app.config 对象中。这个对象还提供了一些方法,可以从文件或环境中导 入配置。第 7 章将介绍管理大型应用配置的合理方式。
Flask-WTF 之所以要求应用配置一个密钥,是为了防止表单遭到跨站请求伪造(CSRF, cross-site request forgery)攻击。
恶意网站把请求发送到被攻击者已登录的其他网站时,就 会引发 CSRF 攻击。Flask-WTF 为所有表单生成安全令牌,存储在用户会话中。令牌是一 种加密签名,根据密钥生成。
为了增强安全性,密钥不应该直接写入源码,而要保存在环境变量中。这一 技术在第 7 章介绍。
4.2 表单类¶
4.3 把表单渲染成HTML¶
4.4 在视图函数中处理表单¶
4.5 重定向和用户会话¶
4.6 闪现消息¶
请求完成后,有时需要让用户知道状态发生了变化,可以是确认消息、警告或者错误提醒。
一个典型例子是,用户提交有一项错误的登录表单后,服务器发回的响应重新渲染登录表单,并在表单上面显示一个消息,提示用户名或密码无效。
Flask 本身内置这个功能。如示例 4-6 所示, flash() 函数可实现这种效果。
示例 4-6 hello.py:闪现消息
from flask import Flask, render_template, session, redirect, url_for, flash
@app.route('/', methods=['GET', 'POST'])
def index():
form = NameForm()
if form.validate_on_submit():
old_name = session.get('name')
if old_name is not None and old_name != form.name.data:
flash('Looks like you have changed your name!')
session['name'] = form.name.data
return redirect(url_for('index'))
return render_template('index.html',form = form, name = session.get('name'))
在这个示例中,每次提交的名字都会和存储在用户会话中的名字进行比较,而会话中存储的名字是前一次在这个表单中提交的数据。如果两个名字不一样,就会调用 flash() 函数, 在发给客户端的下一个响应中显示一个消息。
仅调用 flash() 函数并不能把消息显示出来,应用的模板必须渲染这些消息。最好在基模板中渲染闪现消息,因为这样所有页面都能显示需要显示的消息。Flask 把 get_flashed_messages() 函数开放给模板,用于获取并渲染闪现消息,如示例 4-7 所示。
示例 4-7 templates/base.html:渲染闪现消息
{% block content %}
<div class="container">
{% for message in get_flashed_messages() %}
<div class="alert alert-warning">
<button type="button" class="close" data-dismiss="alert">×</button>
{{ message }}
</div>
{% endfor %}
{% block page_content %}{% endblock %}
</div>
{% endblock %}
参考文献:
5.数据库¶
5.1 SQL数据库¶
5.2 NoSQL数据库¶
5.3 使用SQL还是NoSQL¶
SQL 数据库擅于用高效且紧凑的形式存储结构化数据。这种数据库需要花费大量精力保证数据的一致性,需要考虑停电或硬件失效。
为了达到这种程度的可靠性,关系型数据库采用一种称为 ACID 的范式,即 atomicity(原子性)、consistency(一致性)、isolation(隔离性)和 durability(持续性)。
NoSQL 数据库放宽了对 ACID 的要求,从而获得性能上的优势。
对不同类型数据库的全面分析和对比超出了本书范畴。
对中小型应用来说,SQL 和 NoSQL 数据库都是很好的选择,而且性能相当。
5.4 Python数据库框架¶
5.5 使用Flask-SQLAlchemy管理数据库¶
Flask-SQLAlchemy 是一个 Flask 扩展,简化了在 Flask 应用中使用 SQLAlchemy 的操作。 SQLAlchemy 是一个强大的关系型数据库框架,支持多种数据库后台。SQLAlchemy 提供了高层 ORM,也提供了使用数据库原生 SQL 的低层功能。
与其他多数扩展一样,Flask-SQLAlchemy 也使用 pip 安装:
(venv) $ pip install flask-sqlalchemy
在 Flask-SQLAlchemy 中,数据库使用 URL 指定。几种最流行的数据库引擎使用的URL格式如表 5-1 所示。
数据库引擎 |
URL |
|---|---|
MySQL |
mysql://username:password@hostname/database |
Postgres |
postgresql://username:password@hostname/database |
SQLite(Linux,macOS) |
sqlite:////absolute/path/to/database |
SQLite(Windows) |
sqlite:///c:/absolute/path/to/database |
在这些 URL 中,hostname 表示数据库服务所在的主机,可以是本地主机(localhost),也可以是远程服务器。数据库服务器上可以托管多个数据库,因此 database 表示要使用的数据库名。如果数据库需要验证身份,使用 username 和 password 提供数据库用户的凭据。
SQLite 数据库没有服务器,因此不用指定 hostname、username 和 password。URL 中的 database 是磁盘中的文件名。
应用使用的数据库 URL 必须保存到 Flask 配置对象的 SQLALCHEMY_DATABASE_URI 键中。
Flask-SQLAlchemy 文档还建议把 SQLALCHEMY_TRACK_MODIFICATIONS 键设为 False,以便在不需要跟踪对象变化时降低内存消耗。
其他配置选项的作用参阅 Flask-SQLAlchemy 的文档。
示例 5-1 展示如何初始化及配置一个简单的 SQLite 数据库。
示例 5-1 hello.py:配置数据库
import os
from flask_sqlalchemy import SQLAlchemy
basedir = os.path.abspath(os.path.dirname(__file__))
app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] =\
'sqlite:///' + os.path.join(basedir, 'data.sqlite') app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
db = SQLAlchemy(app)
db 对象是 SQLAlchemy 类的实例,表示应用使用的数据库,通过它可获得 Flask-SQLAlchemy提供的所有功能。
5.6 定义模型¶
5.7 关系¶
5.8 数据库操作¶
现在模型已经按照图 5-1 所示的数据库关系图完成配置,可以随时使用了。
学习使用模型的最好方法是在 Python shell 中实际操作。接下来的几节将介绍最常用的数据库操作。
shell使用 flask shell 命令启动。
不过在执行这个命令之前,要按照第 2 章的说明,把 FLASK_APP 环境变量设为 hello.py 。
5.8.1 创建表¶
# 安装flask-sqlalchemy
(venv) $ pip install flask-sqlalchemy
注意:Python 2版本中需要安装MySQLdb,可以使用pip install flask-MySQLdb命令来完成安装。
Python 3中不再支持MySQLdb,推荐使用PyMySQL。
#要连接mysql数据库,仍需要安装flask-mysqldb
# python2
(venv) $ pip install flask-MySQLdb
# python3
(venv) $ pip install PyMySQL
首先,要让 Flask-SQLAlchemy 根据模型类创建数据库。 db.create_all() 函数将寻找所有 db.Model 的子类,然后在数据库中创建对应的表:
(venv) $ set FLASK_APP=hello.py
(venv) $ set FLASK_DEBUG=1
(venv) $ flask shell
>>> from hello import db
>>> db.create_all()
现在查看应用目录,你会发现有个名为 data.sqlite 的文件,文件名与配置中指定的一样。 如果数据库表已经存在于数据库中,那么 db.create_all() 不会重新创建或者更新相应的表。
如果修改模型后要把改动应用到现有的数据库中,这一行为会带来不便。更新现有数据库表的蛮力方式是先删除旧表再重新创建:
>>> db.drop_all()
>>> db.create_all()
遗憾的是,这个方法有个我们不想看到的副作用,它把数据库中原有的数据都销毁了。
本章末尾将介绍一种更好的数据库更新方式。
5.8.2 插入行¶
下面这段代码创建一些角色和用户:
>>> from hello import Role, User
>>> admin_role = Role(name='Admin')
>>> mod_role = Role(name='Moderator')
>>> user_role = Role(name='User')
>>> user_john = User(username='john', role=admin_role)
>>> user_susan = User(username='susan', role=user_role)
>>> user_david = User(username='david', role=user_role)
模型的构造函数接受的参数是使用关键字参数指定的模型属性初始值。注意,role 属性也可使用,虽然它不是真正的数据库列,但却是一对多关系的高级表示。这些新建对象的id属性并没有明确设定,因为主键是由Flask-SQLAlchemy 管理的。
现在这些对象只存在于Python 中,还未写入数据库。因此id 尚未赋值:
>>> print(admin_role.id)
None
>>> print(mod_role.id)
None
>>> print(user_role.id)
None
通过数据库会话管理对数据库所做的改动,在Flask-SQLAlchemy 中,会话由db.session表示。准备把对象写入数据库之前,先要将其添加到会话中:
>>> db.session.add(admin_role)
>>> db.session.add(mod_role)
>>> db.session.add(user_role)
>>> db.session.add(user_john)
>>> db.session.add(user_susan)
>>> db.session.add(user_david)
或者简写成:
db.session.add_all([admin_role, mod_role, user_role,
... user_john, user_susan, user_david])
为了把对象写入数据库,我们要调用 commit() 方法提交会话:
>>> db.session.commit()
提交数据后再查看 id 属性,现在它们已经赋值了:
>>> print(admin_role.id)
1
>>> print(mod_role.id)
2
>>> print(user_role.id)
3
数据库会话 db.session 和第 4 章介绍的 Flask session 对象没有关系。数据库会话也称为事务。
数据库会话能保证数据库的一致性。
提交操作使用原子方式把会话中的对象全部写入数据库。如果在写入会话的过程中发生了错误,那么整个会话都会失效。如果你始终把相关改动放在会话中提交,就能避免因部分更新导致的数据库不一致。
数据库会话也可回滚。调用 db.session.rollback() 后,添加到数据库会话中的所有对象都将还原到它们在数据库中的状态。
5.8.3 修改行¶
在数据库会话上调用 add() 方法也能更新模型。我们继续在之前的 shell 会话中进行操作, 下面这个例子把 “Admin” 角色重命名为 “Administrator” :
>>> admin_role.name = 'Administrator'
>>> db.session.add(admin_role)
>>> db.session.commit()
5.8.4 删除行¶
数据库会话还有个 delete() 方法。下面这个例子把 “Moderator” 角色从数据库中删除:
>>> db.session.delete(mod_role)
>>> db.session.commit()
注意,删除与插入和更新一样,提交数据库会话后才会执行。
5.8.5 查询行¶
Flask-SQLAlchemy 为每个模型类都提供了 query 对象。
最基本的模型查询是使用 all() 方法取回对应表中的所有记录:
>>> Role.query.all()
[<Role 'Administrator'>, <Role 'User'>]
>>> User.query.all()
[<User 'john'>, <User 'susan'>, <User 'david'>]
使用过滤器可以配置 query 对象进行更精确的数据库查询。
下面这个例子查找角色为“User” 的所有用户:
>>> User.query.filter_by(role=user_role).all()
[<User 'susan'>, <User 'david'>]
若想查看 SQLAlchemy 为查询生成的原生 SQL 查询语句,只需把 query 对象转换成字符串:
>>> str(User.query.filter_by(role=user_role))
'SELECT users.id AS users_id, users.username AS users_username,
users.role_id AS users_role_id \nFROM users \nWHERE :param_1 = users.role_id'
如果你退出了 shell 会话,前面这些例子中创建的对象就不会以 Python 对象的形式存在, 但在数据库表中仍有对应的行。
如果打开一个新的 shell 会话,要从数据库中读取行,重新创建 Python 对象。下面这个例子发起一个查询,加载名为 “User” 的用户角色:
>>> user_role = Role.query.filter_by(name='User').first()
注意,这里发起查询的不是 all() 方法,而是 first() 方法。
all() 方法返回所有结果构成的列表,而 first() 方法只返回第一个结果,如果没有结果的话,则返回 None 。因此,如 果知道查询最多返回一个结果,就可以用这个方法。
filter_by() 等过滤器在 query 对象上调用,返回一个更精确的 query 对象。多个过滤器可以一起调用,直到获得所需结果。
下面列出了Query对象上调用的常用过滤器
过滤器 |
说明 |
|---|---|
filter() |
把过滤器添加到原查询上,返回一个新查询 |
filter_by() |
把等值过滤器添加到原查询上,返回一个新查询 |
limit() |
使用指定的值限制原查询返回的结果数量,返回一个新查询 |
offset() |
偏移原查询返回的结果,返回一个新查询 |
order_by() |
根据指定条件对原查询结果进行排序,返回一个新查询 |
group_by() |
根据指定条件对原查询结果进行分组,返回一个新查询 |
在查询上应用指定的过滤器后,通过调用all() 执行查询,以列表的形式返回结果。
除了all() 之外,还有其他方法能触发查询执行。
下表 列出了执行查询的其他方法。
最常用的SQLAlchemy查询执行方法
方法 |
说明 |
|---|---|
all() |
以列表形式返回查询的所有结果 |
first() |
返回查询的第一个结果,如果没有结果,则返回None |
first_or_404() |
返回查 询的第一个结果,如果没有结果,则终止请求,返回404 错误响应 |
get() |
返回指定主键对应的行,如果没有对应的行,则返回None |
get_or_404() |
返回指定主键对 应的行,如果没找到指定的主键,则终止请求,返回404 错误响应 |
count() |
返回查询结果的数量 |
paginate() |
返回一个Paginate 对象,它包含指定范围内的结果 |
关系与查询的处理方式类似。下面这个例子分别从关系的两端查询角色和用户之间的一对 多关系:
>>> users = user_role.users
>>> users
[<User 'susan'>, <User 'david'>]
>>> users[0].role
<Role 'User'>
这个例子中的 user_role.users 查询有个小问题。执行 user_role.users 表达式时,隐式的查询会调用 all() 方法,返回一个用户列表。
此时, query 对象是隐藏的,无法指定更精确的查询过滤器。就这个示例而言,返回一个按照字母顺序排列的用户列表可能更好。在示例5-4 中,我们修改了关系的设置,加入了 lazy=‘dynamic’ 参数,从而禁止自动执行查询。
class Role(db.Model):
# ...
users = db.relationship('User', backref='role', lazy='dynamic')
# ...
这样配置关系之后, user_role.users 将返回一个尚未执行的查询,因此可以在其上添加过滤器:
>>> user_role.users.order_by(User.username).all()
[<User 'david'>, <User 'susan'>]
>>> user_role.users.count()
2
5.9 在视图函数中操作数据库¶
前一节介绍的数据库操作可以直接在视图函数中进行。
示例 5-5 是首页路由的新版本,把用户输入的名字记录到数据库中。
示例 5-5 hello.py:在视图函数中操作数据库
import os
from flask import Flask, render_template, session, redirect, url_for
from flask_bootstrap import Bootstrap
from flask_moment import Moment
from flask_wtf import FlaskForm
from wtforms import StringField, SubmitField
from wtforms.validators import DataRequired
from flask_sqlalchemy import SQLAlchemy
basedir = os.path.abspath(os.path.dirname(__file__))
app = Flask(__name__)
app.config['SECRET_KEY'] = 'hard to guess string'
# app.config['SQLALCHEMY_DATABASE_URI'] =\
# 'sqlite:///' + os.path.join(basedir, 'data.sqlite')
app.config['SQLALCHEMY_DATABASE_URI'] ='mysql://root:123456@127.0.0.1/flask_test'
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
bootstrap = Bootstrap(app)
moment = Moment(app)
db = SQLAlchemy(app)
class Role(db.Model):
__tablename__ = 'roles'
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(64), unique=True)
users = db.relationship('User', backref='role', lazy='dynamic')
def __repr__(self):
return '<Role %r>' % self.name
class User(db.Model):
__tablename__ = 'users'
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(64), unique=True, index=True)
role_id = db.Column(db.Integer, db.ForeignKey('roles.id'))
def __repr__(self):
return '<User %r>' % self.username
class NameForm(FlaskForm):
name = StringField('What is your name?', validators=[DataRequired()])
submit = SubmitField('Submit')
@app.errorhandler(404)
def page_not_found(e):
return render_template('404.html'), 404
@app.errorhandler(500)
def internal_server_error(e):
return render_template('500.html'), 500
@app.route('/', methods=['GET', 'POST'])
def index():
form = NameForm()
if form.validate_on_submit():
user = User.query.filter_by(username=form.name.data).first()
if user is None:
user = User(username=form.name.data)
db.session.add(user)
db.session.commit()
session['known'] = False
else:
session['known'] = True
session['name'] = form.name.data
return redirect(url_for('index'))
return render_template('index.html', form=form, name=session.get('name'),
known=session.get('known', False))
在这个修改后的版本中,提交表单后,应用会使用 filter_by() 查询过滤器在数据库中查找提交的名字。
变量 known 被写入用户会话中,因此重定向之后,可以把数据传给模板,用于显示自定义的欢迎消息。注意,为了让应用正常运行,必须按照前面介绍的方法,在Python shell 中创建数据库表。
对应的模板新版本如示例 5-6 所示。这个模板使用 known 参数在欢迎消息中加入了第二行,从而对已知用户和新用户显示不同的内容。
示例 5-6 templates/index.html:在模板中定制欢迎消息
{% extends "base.html" %}
{% import "bootstrap/wtf.html" as wtf %}
{% block title %}Flasky{% endblock %}
{% block page_content %}
<div class="page-header">
<h1>Hello, {% if name %}{{ name }}{% else %}Stranger{% endif %}!</h1>
{% if not known %}
<p>Pleased to meet you!</p>
{% else %}
<p>Happy to see you again!</p>
{% endif %}
</div>
{{ wtf.quick_form(form) }}
{% endblock %}
5.10 集成Python shell¶
每次启动 shell 会话都要导入数据库实例和模型,这真是份枯燥的工作。
为了避免一直重复导入,我们可以做些配置,让 flask shell 命令自动导入这些对象。
若想把对象添加到导入列表中,必须使用 app.shell_context_processor 装饰器创建并注册一个 shell 上下文处理器,如示例 5-7 所示。
@app.shell_context_processor
def make_shell_context():
return dict(db=db, User=User, Role=Role)
这个 shell 上下文处理器函数返回一个字典,包含数据库实例和模型。
除了默认导入的 app之外, flask shell 命令将自动把这些对象导入 shell。
$ flask shell
>>> app
<Flask 'hello'>
>>> db
<SQLAlchemy engine='sqlite:////home/flask/flasky/data.sqlite'>
>>> User
<class 'hello.User'>
5.11 使用Flask-Migrate实现数据库迁移¶
在开发应用的过程中,你会发现有时需要修改数据库模型,而且修改之后还要更新数据库。
仅当数据库表不存在时,Flask-SQLAlchemy 才会根据模型创建。因此,更新表的唯一方式就是先删除旧表,但是这样做会丢失数据库中的全部数据。
更新表更好的方法是使用数据库迁移框架。
源码版本控制工具可以跟踪源码文件的变化; 类似地,数据库迁移框架能跟踪数据库模式的变化,然后以增量的方式把变化应用到数据库中。
SQLAlchemy 的开发人员编写了一个迁移框架,名为 Alembic。
除了直接使用 Alembic 之外,Flask 应用还可使用 Flask-Migrate 扩展。这个扩展是对 Alembic 的轻量级包装,并与 flask 命令做了集成。
5.11.1 创建迁移仓库¶
首先,要在虚拟环境中安装 Flask-Migrate:
(venv) $ pip install flask-migrate
这个扩展的初始化方法如示例 5-8 所示。
示例 5-8:hello.py:初始化 Flask-Migrate
from flask_migrate import Migrate
basedir = os.path.abspath(os.path.dirname(__file__))
app = Flask(__name__)
app.config['SECRET_KEY'] = 'hard to guess string'
# app.config['SQLALCHEMY_DATABASE_URI'] =\
# 'sqlite:///' + os.path.join(basedir, 'data.sqlite')
app.config['SQLALCHEMY_DATABASE_URI'] ='mysql://root:123456@127.0.0.1/flask_test'
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
bootstrap = Bootstrap(app)
moment = Moment(app)
db = SQLAlchemy(app)
migrate = Migrate(app, db)
为了开放数据库迁移相关的命令,Flask-Migrate 添加了 flask db 命令和几个子命令。
在新项目中可以使用 init 子命令添加数据库迁移支持:
(venv) $ flask db init
Creating directory /home/flask/flasky/migrations...done
Creating directory /home/flask/flasky/migrations/versions...done
Generating /home/flask/flasky/migrations/alembic.ini...done
Generating /home/flask/flasky/migrations/env.py...done
Generating /home/flask/flasky/migrations/env.pyc...done
Generating /home/flask/flasky/migrations/README...done
Generating /home/flask/flasky/migrations/script.py.mako...done
Please edit configuration/connection/logging settings in
'/home/flask/flasky/migrations/alembic.ini' before proceeding.
数据库迁移仓库中的文件要和应用的其他文件一起纳入版本控制。
5.11.2 创建迁移脚本¶
在 Alembic 中,数据库迁移用迁移脚本表示。
脚本中有两个函数,分别是 upgrade() 和downgrade() 。
upgrade() 函数把迁移中的改动应用到数据库中, downgrade() 函数则将改动删除。
Alembic 具有添加和删除改动的能力,意味着数据库可重设到修改历史的任意一点。
我们可以使用 revision 命令手动创建 Alembic 迁移,也可使用 migrate 命令自动创建。 手动创建的迁移只是一个骨架, upgrade() 和 downgrade() 函数都是空的,开发者要使用 Alembic 提供的 Operations 对象指令实现具体操作。
自动创建的迁移会根据模型定义和数据库当前状态之间的差异尝试生成 upgrade() 和 downgrade() 函数的内容。
自动创建的迁移不一定总是正确的,有可能会漏掉一些细节。
比如说我们重命名了一列,自动生成的迁移可能会把这当作删除了一列,然后又新增了一列。
如果原封不动地使用自动生成的迁移,这一列中的数据就会丢失!鉴于此,自动生成迁移脚本后一定要进行检查,把不准确的部分手动改过来。
使用 Flask-Migrate 管理数据库模式变化的步骤如下。
对模型类做必要的修改。
执行 flask db migrate 命令,自动创建一个迁移脚本。
检查自动生成的脚本,根据对模型的实际改动进行调整。
把迁移脚本纳入版本控制。
执行 flask db upgrade 命令,把迁移应用到数据库中。
flask db migrate 子命令用于自动创建迁移脚本:
(venv) $ flask db migrate -m "initial migration"
INFO [alembic.migration] Context impl SQLiteImpl.
INFO [alembic.migration] Will assume non-transactional DDL.
INFO [alembic.autogenerate] Detected added table 'roles'
INFO [alembic.autogenerate] Detected added table 'users'
INFO [alembic.autogenerate.compare] Detected added index
'ix_users_username' on '['username']'
Generating /home/flask/flasky/migrations/versions/1bc
594146bb5_initial_migration.py...done
5.11.3 更新数据库¶
检查并修正好迁移脚本之后,执行 flask db upgrade 命令,把迁移应用到数据库中:
(venv) $ flask db upgrade
INFO [alembic.migration] Context impl SQLiteImpl.
INFO [alembic.migration] Will assume non-transactional DDL.
INFO [alembic.migration] Running upgrade None -> 1bc594146bb5, initial migration
对第一个迁移来说,其作用与调用 db.create_all()
方法一样。但在后续的迁移中, flask db upgrade
命令能把改动应用到数据库中,且不影响其中保存的数据。
如果你按照之前的说明操作过,那么已经使用 db.create_all() 函数创建了数据库文件。此时, flask db upgrade 命令将失败,因为它试图创建已经存在的数据库表。
一种简单的处理方法是,把 data.sqlite 数据库文件删掉,然后执行 flask db upgrade 命令,通过迁移框架重新创建数据库。
另一种方法是不执行 flask db upgrade 命令,而是使用 flask db stamp 命令把现有数据库标记为已更新。
5.11.4 添加几个迁移¶
在开发项目的过程中,时常要修改数据库模型。
如果使用迁移框架管理数据库,必须在迁移脚本中定义所有改动,否则改动将不可复现。
修改数据库的步骤与创建第一个迁移类似。
对数据库模型做必要的修改。
执行 flask db migrate 命令,生成迁移脚本。
检查自动生成的脚本,改正不准确的地方。
执行 flask db upgrade 命令,把改动应用到数据库中。
实现一个功能时,可能要多次修改数据库模型才能得到预期结果。
如果前一个迁移还未提交到源码控制系统中,可以继续在那个迁移中修改,以免创建大量无意义的小迁移脚本。
在前一个迁移脚本的基础上修改的步骤如下。 (1) 执行 flask db downgrade 命令,还原前一个脚本对数据库的改动(注意,这可能导致部分数据丢失)。
删除前一个迁移脚本,因为现在已经没什么用了。
执行 flask db migrate 命令生成一个新的数据库迁移脚本。这个迁移脚本除了前面删除的那个脚本中的改动之外,还包括这一次对模型的改动。
根据前面的说明,检查并应用迁移脚本。
与数据库迁移相关的其他子命令参见 Flask-Migrate 文档(https://flask-migrate.readthedocs.io/)
6. 电子邮件¶
6.1 使用Flask-Mail提供电子邮件支持¶
虽然 Python 标准库中的 smtplib 包可用于在 Flask 应用中发送电子邮件,
但包装了 smtplib的 Flask-Mail 扩展能更好地与 Flask 集成。
Flask-Mail 使用 pip 安装:
(venv) $ pip install flask-mail
Flask-Mail 连接到简单邮件传输协议(SMTP,simple mail transfer protocol)服务器,把邮件交给这个服务器发送。
如果不进行配置,则 Flask-Mail 连接 localhost 上的 25 端口,无须验证身份即可发送电子邮件。
表 6-1 列出了可用来设置 SMTP 服务器的配置。
配置 |
默认值 |
说明 |
|---|---|---|
MAIL_SERVER |
localhost |
电子邮件服务器的主机名或IP地址 |
MAIL_PORT |
25 |
电子邮件服务器的端口 |
MAIL_USE_TLS |
FALSE |
启用传输层安全( |
MAIL_USE_SSL |
False |
启用安全套接层(SSL,secure sockets layer)协议 |
MAIL_USERNAME |
None |
邮件账户的用户名 |
MAIL_PASSWORD |
None |
邮件账户的密码 |
在开发过程中,连接到外部SMTP服务器可能更方便。
配置Flask-Mail使用GMAIL。
app.config['MAIL_SERVER'] = 'smtp.googlemail.com'
app.config['MAIL_PORT'] = 587
app.config['MAIL_USE_TLS'] = True
app.config['MAIL_USERNAME'] = os.environ.get('MAIL_USERNAME')
app.config['MAIL_PASSWORD'] = os.environ.get('MAIL_PASSWORD')
初始化方法:
from flask import Mail
mail = Mail(app)
保存电子邮件服务器用户名和密码的两个环境变量要在环境中定义。如果你使用的是 Linux 或 macOS,可以按照下面的方式设定这两个变量:
(venv) $ export MAIL_USERNAME=<Gmail username>
(venv) $ export MAIL_PASSWORD=<Gmail password>
微软 Windows 用户可按照下面的方式设定环境变量:
(venv) $ set MAIL_USERNAME=<Gmail username>
(venv) $ set MAIL_PASSWORD=<Gmail password>
6.2 在Python shell中发送电子邮件¶
你可以打开一个 shell 会话,发送一封测试邮件,检查配置是否正确(记得把 you@example.com 换成你自己的电子邮件地址):
(venv) $ flask shell
>>> from flask_mail import Message
>>> from hello import mail
>>> msg = Message('test email', sender='you@example.com',
... recipients=['you@example.com'])
>>> msg.body = 'This is the plain text body'
>>> msg.html = 'This is the <b>HTML</b> body'
>>> with app.app_context():
... mail.send(msg)
...
注意,Flask-Mail 的 send() 函数使用 current_app ,因此要在激活的应用上下文中执行。
6.3 在应用中集成电子邮件发送功能¶
为了避免每次都手动编写电子邮件消息,我们最好把应用发送电子邮件的通用部分抽象出来,定义成一个函数。
这么做还有个好处,即该函数可以使用 Jinja2 模板渲染邮件正文,灵活性极高。具体实现如示例 6-3 所示。
示例 6-3 hello.py:电子邮件支持
from flask_mail import Message
app.config['FLASKY_MAIL_SUBJECT_PREFIX'] = '[Flasky]'
app.config['FLASKY_MAIL_SENDER'] = 'Flasky Admin <flasky@example.com>'
def send_email(to, subject, template, **kwargs):
msg = Message(app.config['FLASKY_MAIL_SUBJECT_PREFIX'] + subject,
sender=app.config['FLASKY_MAIL_SENDER'], recipients=[to])
msg.body = render_template(template + '.txt', **kwargs)
msg.html = render_template(template + '.html', **kwargs)
mail.send(msg)
这个函数用到了两个应用层面的配置项,分别定义邮件主题的前缀和发件人的地址。
send_email() 函数的参数分别为收件人地址、主题、渲染邮件正文的模板和关键字参数列表。 指定模板时不能包含扩展名,这样才能使用两个模板分别渲染纯文本正文和 HTML 正文。 调用者传入的关键字参数将传给 render_template() 函数,作为模板变量提供给模板使用,用于生成电子邮件正文。
我们可以轻松扩展 index() 视图函数,每当表单接收到新的名字,应用就给管理员发送一封电子邮件。
示例 6-4 hello.py:电子邮件示例
......
app.config['MAIL_SERVER'] = 'smtp.googlemail.com'
app.config['MAIL_PORT'] = 587
app.config['MAIL_USE_TLS'] = True
app.config['MAIL_USERNAME'] = os.environ.get('MAIL_USERNAME')
app.config['MAIL_PASSWORD'] = os.environ.get('MAIL_PASSWORD')
app.config['FLASKY_MAIL_SUBJECT_PREFIX'] = '[Flasky]'
app.config['FLASKY_MAIL_SENDER'] = 'Flasky Admin <flasky@example.com>'
app.config['FLASKY_ADMIN'] = os.environ.get('FLASKY_ADMIN')
.....
@app.route('/', methods=['GET', 'POST'])
def index():
form = NameForm()
if form.validate_on_submit():
user = User.query.filter_by(username=form.name.data).first()
if user is None:
user = User(username=form.name.data)
db.session.add(user)
db.session.commit()
session['known'] = False
if app.config['FLASKY_ADMIN']:
send_email(app.config['FLASKY_ADMIN'], 'New User',
'mail/new_user', user=user)
else:
session['known'] = True
session['name'] = form.name.data
return redirect(url_for('index'))
return render_template('index.html', form=form, name=session.get('name'),
known=session.get('known', False))
电子邮件的收件人保存在环境变量 FLASKY_ADMIN 中,在应用启动过程中,它会加载到一个 同名配置变量中。我们要创建两个模板文件,分别用于渲染纯文本和 HTML 版本的邮件正文。
这两个模板文件都保存在 templates 目录下的 mail 子目录中,以便和普通模板区分开来。
电子邮件的模板中有一个模板参数是用户,因此调用 send_email() 函数时要以关键字参数的形式传入用户。
除了前面提到的环境变量 MAIL_USERNAME 和 MAIL_PASSWORD 之外,应用的这个版本还需要使 用环境变量 FLASKY_ADMIN 。
Linux 和 macOS 用户可使用下面的命令设置这个变量:
(venv) $ export FLASKY_ADMIN=<your-email-address>
对微软 Windows 用户来说,等价的命令是:
(venv) $ set FLASKY_ADMIN=<your-email-address>
设置好这些环境变量后,我们就可以测试应用了。
每次你在表单中填写新名字,管理员都会收到一封电子邮件。
6.4 异步发送电子邮件¶
如果你发送了几封测试邮件,可能会注意到 mail.send() 函数在发送电子邮件时停滞了几秒钟,在这个过程中浏览器就像无响应一样。
为了在处理请求过程中避免不必要的延迟,我们可以把发送电子邮件的函数移到后台线程中。修改方法如示例 6-5 所示。
示例 6-5 hello.py:异步发送电子邮件
from threading import Thread
def send_async_email(app, msg):
with app.app_context():
mail.send(msg)
def send_email(to, subject, template, **kwargs):
msg = Message(app.config['FLASKY_MAIL_SUBJECT_PREFIX'] + ' ' + subject,
sender=app.config['FLASKY_MAIL_SENDER'], recipients=[to])
msg.body = render_template(template + '.txt', **kwargs)
msg.html = render_template(template + '.html', **kwargs)
thr = Thread(target=send_async_email, args=[app, msg])
thr.start()
return thr
上述实现涉及一个有趣的问题。
很多 Flask 扩展都假设已经存在激活的应用上下文和(或)请求上下文。
前面说过,Flask-Mail 的 send() 函数使用 current_app ,因此必须激活应用上下文。
不过,上下文是与线程配套的,在不同的线程中执行 mail.send() 函数时,要使用 app.app_context() 人工创建应用上下文。
app 实例作为参数传入线程,因此可以通过它来创建上下文。
现在再运行应用,你会发现应用流畅多了。
不过要注意,应用要发送大量电子邮件时,使用专门发送电子邮件的作业要比给每封邮件都新建一个线程更合适。
例如,我们可以把执行 send_async_email() 函数的操作发给 Celery 任务队列。
7.大型应用的结构¶
尽管在单一脚本中编写小型Web 程序很方便,但这种方法并不能广泛使用。程序变复杂后,使用单个大型源码文件会导致很多问题。不同于大多数其他的Web 框架,Flask 并不强制要求大型项目使用特定的组织方式,程序结构的组织方式完全由开发者决定。在本节,我们将介绍一种使用包和模块组织大型程序的方式。
7.1 项目结构¶
Flask 应用的基本结构如示例
|-flasky
|-app/
|-templates/
|-static/
|-main/
|-__init__.py
|-errors.py
|-forms.py
|-views.py
|-__init__.py
|-email.py
|-models.py
|-migrations/
|-tests/
|-__init__.py
|-test*.py
|-venv/
|-requirements.txt
|-config.py
|-flasky.py
这种结构有 4 个顶级文件夹:
Flask 应用一般保存在名为 app 的包中
和之前一样,数据库迁移脚本在 migrations 文件夹中;
单元测试在 tests 包中编写
和之前一样,Python 虚拟环境在 venv 文件夹中。
此外,这种结构还多了一些新文件:
requirements.txt 列出了所有依赖包,便于在其他计算机中重新生成相同的虚拟环境;
config.py 存储配置;
flasky.py 定义 Flask 应用实例,同时还有一些辅助管理应用的任务。
为了帮助你完全理解这个结构,下面几节会说明把 hello.py 应用转换成这种结构的过程。
7.2 项目结构规范¶
为了方便,这里使用 shell 脚本生成项目基础骨架:
# !/bin/bash
dirname=$1
if [ ! -d "$dirname" ]
then
mkdir ./$dirname && cd $dirname
mkdir ./application
mkdir -p ./application/{controllers,models,static,static/css,static/js,templates}
touch {manage.py,requirements.txt}
touch ./application/{__init__.py,app.py,configs.py,extensions.py}
touch ./application/{controllers/__init__.py,models/__init__.py}
touch ./application/{static/css/style.css,templates/404.html,templates/base.html}
echo "File created"
else
echo "File exists"
fi
参考文献
https://www.bookstack.cn/read/head-first-flask/chapter04-section4.01.md