本文主要精炼的记录书中容易忘记的知识点,作为速查笔记使用。
第1章 初识Flask
从PyPI上下载并安装指定的包:
pip install <某个包的名称>
PyPI(Python Package Index,Python包索引)中的包名称不区分大小写。
使用国内的 PyPI 镜像源
使用 pip 安装 Python 包的速度比较慢的解决办法:使用国内的 PyPI 镜像源(mirror source)。
使用镜像源需要注意一个问题:包的版本可能不会及时更新,遇到这种情况可以通过临时换回官方源解决。
临时使用
pip install -i https://pypi.doubanio.com/simple/ flask
永久设置
如果希望永久设置镜像,需要将镜像地址写入 pip 的配置文件。
在不同的生效范围下,pip 的配置文件的文件位置也不同,以下以Windows系统为例给出的配置文件位置,其他系统见 pip 文档的配置部分。
-
用户全局(Per-user)
%APPDATA%\pip\pip.ini
兼容旧版本(legacy)的用户全局配置文件位置为:
%HOMEPATH%\pip\pip.ini
也可以通过设置环境变量
PIP_CONFIG_FILE
来自定义用户全局配置文件位置。 -
虚拟环境全局(Inside a virtualenv)
%VIRTUAL_ENV%\pip.ini
-
全局配置文件
全局配置文件由所有Python安装共享。- Windows XP:
C:\Documents and Settings\All Users\Application Data\pip\pip.ini
- Windows Vista 不支持全局设置
- Windows 7 以及之后的版本:
C:\ProgramData\pip\pip.ini
(该文件虽然被隐藏,但是可写)
- Windows XP:
在 Windows 下,环境变量
%HOMEPATH%
具体值可以使用echo %HOMEPATH%
命令查看。
选择好需要的配置文件生效范围后,通常你需要手动创建对应的目录和文件,然后写入下面的内容:
[global]
index-url = https://pypi.doubanio.com/simple
[install]
trusted-host = pypi.doubanio.com
其中的 https://pypi.doubanio.com/simple/
可以修改为以下任意一条镜像,注意trusted-host
也要相应更改为对应的域名。
常用的国内 PyPI 镜像列表
- 豆瓣:
https://pypi.doubanio.com/simple/
- 网易:
https://mirrors.163.com/pypi/simple/
- 阿里云:
https://mirrors.aliyun.com/pypi/simple/
- 腾讯云:
https://mirrors.cloud.tencent.com/pypi/simple
- 清华大学:
https://pypi.tuna.tsinghua.edu.cn/simple/
包管理工具
根据李辉大神的文章,本书中的包管理工具由 pipenv
更换为使用 virtualenv/venv
来管理虚拟环境,搭配 pip
来管理依赖。
包管理工具的准备
-
使用 Python 3.3+,推荐使用标准库内置的
venv
模块替代virtualenv
,两者的使用方式基本相同,唯一不同的是创建虚拟环境的方式。 -
如果你使用 Python 2,那就只能选择
virtualenv
pip install virtualenv
创建虚拟环境
假设我们的项目名叫 snow
,创建对应的文件夹然后切换到根目录:
mkdir snow
cd snow
-
使用内置模块
venv
,那么使用下面的命令创建虚拟环境python -m venv snow-venv
其中
snow-venv
是虚拟环境的名字,也作为创建的虚拟环境文件夹名称,可以自由修改(通常会使用venv
或env
作为虚拟环境名)。 -
如果使用
virtualenv
,则使用下面的命令:virtualenv snow-venv
注意:上述命令会在当前目录创建名为
snow-venv
的虚拟环境文件夹。如果正在使用 Git,你需要把这个文件夹名称加入.gitignore
文件以便让 Git 忽略。
激活虚拟环境
-
Windows(CMD.exe)使用下面的命令激活:
snow-venv\scripts\activate
-
Linux 和 macOS(bash/zsh)使用下面的命令:
source snow-venv/bin/activate
或:
. snow-venv/bin/activate
类似的,其他终端程序可以执行对应的激活脚本来激活虚拟环境。
激活虚拟环境以后,命令行提示符前会显示当前虚拟环境的名字:
(snow-venv) $
使用 deactivate
命令可以退出虚拟环境。
使用 pip 管理依赖
-
安装依赖:
(snow-venv) $ pip install flask
-
更新依赖:
(snow-venv) $ pip install --upgrade flask
或是:
(snow-venv) $ pip install -U flask
-
卸载依赖:
(snow-venv) $ pip uninstall flask
除此之外,还有 pip show <package>
命令可以查看某个包的详细依赖信息,pip list
列出所有依赖。
下面的命令可以手动生成依赖列表:
(snow-venv) $ pip freeze > requirements.txt
如果你需要手动开发依赖和生产依赖,可以手动把开发相关的依赖放到单独的文件,比如 requirements-dev.txt
。
当你需要在新的机器创建程序运行环境时,(创建虚拟环境后)只需要使用下面的命令从依赖文件安装所有依赖:
(snow-venv) $ pip install -r requirements.txt
第一章切换进 helloflask 目录后,整个 1.1 小节你只需要执行下面的命令:
$ python -m venv env # Linux、macOS 系统的 Python3 用户,使用 python3 -m venv env
$ env\Scripts\activate # Linux、macOS 系统使用 source env/bin/activate
$ pip install -r requirements.txt # 这个命令会安装对应项目的所有依赖
附注 :上面命令里
#
号及之后的文字是注释,不需要输入。如果你使用 Python 2,第一条命令需要改为virtualenv env
。这三行命令的作用依次为:创建虚拟环境、激活虚拟环境、从requirements.txt
文件安装依赖列表。
第二部分每章开头的下面这两行命令:
$ pipenv install --dev
$ pipenv shell
都要替换为:
$ python -m venv env # Linux、macOS 系统的 Python3 用户,使用 python3 -m venv env
$ env\Scripts\activate # Linux、macOS 系统使用 source env/bin/activate
$ pip install -r requirements.txt # 这个命令会安装对应项目的所有依赖
退出虚拟环境时,使用下面的命令:
$ deactivate
最小的Flask程序与route
路由规则
from flask import Flask
app = Flask(__name__)
@app.route('/')
def index():
return '<h1>Hello Flask!</h1>'
传入Flask类构造方法的第一个参数是模块或包的名称,我们应该使用特殊变量__name__
。这会帮助Flask在相应的文件夹里找到需要的资源,比如模板和静态文件。
Python会根据所处的模块来赋予__name__
变量相应的值,对于我们的程序来说(app.py
),这个值为app
。
存储程序名称的属性为app.name
。
app.route()
装饰器把根地址/和index()
函数绑定起来,当用户访问这个URL时就会触发index()
函数。URL规则和视图函数名称没有任何必然关系。
route()
装饰器的第一个参数是URL规则,用字符串表示,必须以斜杠(/
)开始。这里的URL是相对URL(又称为内部URL),即不包含域名的URL。
一个视图函数可以绑定多个URL:
@app.route('/hi')
@app.route('/hello')
def say_hello():
return '<h1>Hello, Flask!</h1>'
还可以在URL规则中添加变量部分,使用<变量名>
的形式表示。Flask处理请求时会把变量传入视图函数,所以我们可以添加参数获取这个变量值。
@app.route('/greet/<name>')
def greet(name):
return f'<h1>Hello, { name }!</h1>'
当URL规则中包含变量时,如果用户访问的URL中没有添加变量,比如/greet,那么Flask在匹配失败后会返回一个404错误响应。
在app.route()
装饰器里使用defaults
参数设置URL变量的默认值或是在函数中设置默认参数值来防止给出404相应。
@app.route('/greet', defaults={'name': 'Programmer'})
@app.route('/greet/<name>')def greet(name):
return f'<h1>Hello, { name }!</h1>'
# 上下两种写法等价
@app.route('/greet')
@app.route('/greet/<name>')
def greet(name='Programmer'):
return f'<h1>Hello, { name }!</h1>'
内置的开发服务器
在命令行中输入flask run
命令用来启动内置的开发服务器,默认会监听http://127.0.0.1:5000/地址(按Ctrl+C
退出)。
请确保执行命令前激活了虚拟环境。
flask run
命令是Flask依赖包Click提供的,后续还会提及如何自定义一个Flask命令。
输入flask run
命令后,Flask会自动按照以下顺序规则寻找程序实例:
-
从当前目录寻找
app.py
和wsgi.py
模块,并从中寻找名为app
或application
的程序实例。 -
从环境变量
FLASK_APP
对应的模块名/导入路径寻找名为app
或application
的程序实例。在Windows中使用
set
命令来设置环境变量:set FLASK_APP=hello
-
如果安装了
python-dotenv
,那么在使用flask run
或其他命令时会使用它自动从.flaskenv
文件和.env
文件中加载环境变量。加载优先级是:手动设置的环境变量>
.env
中设置的环境变量>.flaskenv
设置的环境变量安装命令:
pip install python-dotenv
-
.flaskenv
用来存储和Flask相关的公开环境变量,比如FLASK_APP
; -
.env
用来存储包含敏感信息的环境变量,比如后面我们会用来配置Email服务器的账户名与密码。除非是私有项目,否则绝对不能提交到Git仓库中,记得把
.env
添加到.gitignore
文件中,这会告诉Git忽略这个文件。
在
.flaskenv
或.env
文件中,环境变量使用键值对的形式定义,每行一个,以#
开头的为注释,如下所示:SOME_VAR=1 #这是注释 FOO="BAR"
-
run
命令的更多选项
在run
命令后添加--host
选项将主机地址设为0.0.0.0
,会让服务器监听所有外部请求:
flask run --host=0.0.0.0
内网穿透/端口转发工具:
Flask提供的Web服务器默认监听5000端口,你可以在启动时传入参数--port
来改变为其他值(如8000
):
flask run --port=8000
--host
和--port
选项也可以通过环境变量FLASK_RUN_HOST
和FLASK_RUN_PORT
设置。事实上,Flask内置的命令都可以使用这种模式定义默认选项值,即
FLASK_<COMMAND>_<OPTION>
,你可以使用flask --help
命令查看所有可用的命令。
设置运行环境
Flask提供了一个FLASK_ENV
环境变量用来设置环境,默认为production
(生产)。在开发时,我们可以将其设为development
(开发),这会开启所有支持开发的特性。
为了方便管理,我们将把环境变量FLASK_ENV
的值写入.flaskenv
文件中:FLASK_ENV=development
在开发环境下,调试模式(Debug Mode)将被开启,这时执行flask run启动程序会自动激活Werkzeug内置的调试器(debugger)和重载器(reloader),它们会为开发带来很大的帮助。
如果你想单独控制调试模式的开关,可以通过
FLASK_DEBUG
环境变量设置,设为1
则开启,设为0
则关闭,不过通常不推荐手动设置这个值。
在生产环境中部署程序时,绝不能开启调试模式。尽管PIN码可以避免用户任意执行代码,提高攻击者利用调试器的难度,但并不能确保调试器完全安全,会带来巨大的安全隐患。而且攻击者可能会通过调试信息获取你的数据库结构等容易带来安全问题的信息。另一方面,调试界面显示的错误信息也会让普通用户感到困惑。
默认会使用Werkzeug内置的stat重载器,它的缺点是耗电较严重,而且准确性一般。为了获得更优秀的体验,我们可以安装另一个用于监测文件变动的Python库Watchdog
,安装后Werkzeug会自动使用它来监测文件变动:
pip install watchdog
如果项目中使用了单独的CSS或JavaScript文件时,那么浏览器可能会缓存这些文件,从而导致对文件做出的修改不能立刻生效。在浏览器中,我们可以按下Ctrl+F5或Shift+F5执行硬重载(hard reload),即忽略缓存并重载(刷新)页面。
Flask Shell
在开发Flask程序时,我们并不会直接使用python命令启动Python Shell,而是使用flask shell
命令。
和其他flask命令相同,执行这个命令前我们要确保程序实例可以被正常找到。
在Python Shell中可以执行
exit()
或quit()
退出,在Windows系统上可以使用Ctrl+Z并按Enter退出;在Linux和macOS则可以使用Ctrl+D退出。使用
flask shell
命令打开的Python Shell自动包含程序上下文,并且已经导入了app实例。
项目配置
在Flask中,配置变量就是一些大写形式的Python变量,包括:
- Flask提供的配置
- 扩展提供的配置
- 程序特定的配置。
和平时使用变量不同,这些配置变量都通过Flask对象的app.config
属性作为统一的接口来设置和获取,它指向的Config
类实际上是字典的子类,所以你可以像操作其他字典一样操作它,例如
app.config['ADMIN_NAME']='Peter'
注意:配置的名称必须是全大写形式,小写的变量将不会被读取。
Flask内置的配置可以访问Flask文档的配置章节查看,扩展提供的配置也可以在对应的文档中查看。
使用update()
方法则可以一次加载多个值:
app.config.update(
TESTING=True,
SECRET_KEY='_5#yF4Q8z\n\xec]/'
)
除此之外,你还可以把配置变量存储在单独的Python脚本、JSON格式的文件或是Python类中,config
对象提供了相应的方法来导入配置,具体我们会在后面了解。
提示:某些扩展需要读取配置值来完成初始化操作,比如Flask-Mail,因此我们应该尽量将加载配置的操作提前,最好在程序实例
app
创建后就加载配置。
URL与端点
使用Flask提供的url_for()
函数获取URL而不是硬编码的方式写出,这样能够在路由中定义的URL规则被修改时仍然使用正确的URL。
调用url_for()
函数时,第一个参数为端点(endpoint)值。在Flask中,端点用来标记一个视图函数以及对应的URL规则。
端点的默认值为视图函数的名称,至于为什么不直接使用视图函数名,而要引入端点这个概念,我们会在后面了解。
在
app.route()
装饰器中使用endpoint
参数可以自定义端点值,不过我们通常不需要这样做。
如果URL含有动态部分,那么我们需要在url_for()
函数里传入相应的参数,以下面的视图函数为例:
@app.route('/hello/<name>')
def greet(name):
return f'Hello { name }!'
# url_for('greet',name='Jack')得到的URL为 /hello/Jack
默认情况下,url_for()
函数生成的URL是相对URL(即内部URL)。如果你想要生成供外部使用的绝对URL,可以在使用url_for()
函数时,将_external
参数设为True
。
自定义Flask命令
通过创建任意一个函数,并为其添加app.cli.command()
装饰器,我们就可以注册一个flask命令,函数的名称即为命令名称,例如注册命令hello
,你可以使用flask hello
命令在命令行下来触发函数:
import click
@app.cli.command()
def hello():
"""该命令只是打印一行问候。"""
click.echo('你好啊!')
$ flask hello
你好啊!
作为替代,你也可以在app.cli.command()
装饰器中传入参数来设置命令名称,比如app.cli.command('say-hello')
会把命令名称设置为say-hello
,完整的命令即flask say-hello
。
借助click模块的echo()
函数,我们可以在命令行界面输出字符。
命令函数的文档字符串会作为帮助信息显示(例如上述命令可以用flask hello --help
来查看对应的帮助信息)。
在命令下执行flask --help
可以查看Flask提供的命令以及我们自定义的命令的帮助文档:
$ flask --help
Usage: flask [OPTIONS] COMMAND [ARGS]...
A general utility script for Flask applications.
...
Options:
--version Show the flask version
--help Show this message and exit.
Commands:
hello Just say hello. # 我们注册的自定义命令
routes Show the routes for the app. # 显示所有注册的路由
run Runs a development server.
shell Runs a shell in the app context.
关于自定义命令更多的设置和功能请参考Click的官方文档。
模板与静态文件
一个完整的网站当然不能只返回用户一句“Hello,World!”,我们需要模板和静态文件来生成更加丰富的网页。
- 模板(template)即包含程序页面的HTML文件
- 静态文件(static file)则是需要在HTML文件中加载的CSS和JavaScript文件,以及图片、字体文件等资源文件。
默认情况下,模板文件存放在项目根目录中的templates
文件夹中,静态文件存放在static
文件夹下,这两个文件夹需要和包含程序实例的模块处于同一个目录下,对应的项目结构示例如下所示:
<project-name>/
- templates/
- static/
- app.py
在开发Flask程序时,使用CSS框架和JavaScript库是很常见的需求,而且有很多扩展都提供了对CSS框架和JavaScript库的集成功能。
使用这些扩展时都需要加载对应的CSS和JavaScript文件,通常这些扩展都会提供一些可以在HTML模板中使用的加载方法/函数,使用这些方法即可渲染出对应的link
标签和script
标签。
这些方法一般会直接从CDN加载资源,有些提供了手动传入资源URL的功能,有些甚至提供了内置的本地资源。
建议在开发环境下使用本地资源,这样可以提高加载速度。最好自己下载到
static
目录下,统一管理,出于方便的考虑也可以使用扩展内置的本地资源。在过渡到生产环境时,自己手动管理所有本地资源或自己设置CDN,避免使用扩展内置的资源。这个建议主要基于下面这些考虑因素:
- 鉴于国内的网络状况,扩展默认使用的国外CDN可能会无法访问或访问过慢。
- 不同扩展内置的加载方法可能会加载重复的依赖资源,比如jQuery。
- 在生产环境下,将静态文件集中在一起更方便管理。
- 扩展内置的资源可能会出现版本过旧的情况。
第2章 Flask与HTTP
HTTP的详细定义在RFC 7231~7235中,完整的RFC列表可以在这里看到:https://tools.ietf.org/rfc/。
每一个Web应用都包含这种处理模式,即“请求-响应循环(Request-Response Cycle)”:客户端发出请求,服务器端处理请求并返回响应
这部分笔记优先级不高,以后有时间整理,待办任务:
- 2.1 请求响应循环
- 2.2 HTTP请求
- 2.3 HTTP响应
- 2.4 Flask上下文
- 2.5 HTTP进阶实践
第3章 模板
一个完整的HTML页面往往需要几十行甚至上百行代码,如果都写到视图函数里,这样的代码既不简洁也难于维护。正确的做法是把HTML代码存储在单独的文件中,以便让程序的业务逻辑和表现逻辑分离,即控制器和用户界面的分离。
当HTML代码保存到单独的文件中时,我们没法再使用字符串格式化或拼接字符串的方式来在HTML代码中插入变量,这时我们需要使用模板引擎(template engine)。借助模板引擎,我们可以在HTML文件中使用特殊的语法来标记出变量,这类包含固定内容和动态部分的可重用文件称为模板(template)。模板引擎的作用就是读取并执行模板中的特殊语法标记,并根据传入的数据将变量替换为实际值,输出最终的HTML页面,这个过程被称为渲染(rendering)。
Flask默认使用的模板引擎是 Jinja2,它是一个功能齐全的Python模板引擎,除了设置变量,还允许我们在模板中添加if判断,执行for迭代,调用函数等,以各种方式控制模板的输出。对于 Jinja2 来说,模板可以是任何格式的纯文本文件,比如HTML、XML、CSV、LaTeX等。
模板基本用法
示例:
<!DOCTYPE html>
<html lang="zh-cmn-Hans">
<head>
<meta charset="utf-8">
<title>{{ user.username }}'s Watchlist</title>
</head>
<body>
<a href="{{ url_for('index') }}">← Return</a>
<h2>{{ user.username }}</h2>
{% if user.bio %}
<i>{{ user.bio }}</i>
{% else %}
<i>This user has not provided a bio.</i>
{% endif %}
{# Below is the movie list (this is comment) #}
<h5>{{ user.username }}'s Watchlist ({{ movies|length }}):</h5>
<ul>
{% for movie in movies %}
<li>{{ movie.name }} - {{ movie.year }}</li>
{% endfor %}
</ul>
</body>
</html>
国语中文简体语言的代码为
zh-cmn-Hans
,推荐大家使用。在模板中使用的
←
是HTML实体。HTML实体除了用来转义HTML保留符号外,通常会被用来显示不容易通过键盘输入的字符。这里的←
会显示为左箭头←,另外,我们还经常使用©
来显示版权标志©,你可以访问https://dev.w3.org/html5/html-author/charref查看所有可用的HTML实体。
在模板中添加Python语句和表达式时,我们需要使用特定的定界符把它们标示出来。
-
语句
比如
if
判断、for
循环等:{% ... %}
-
表达式
比如字符串、变量、函数调用等。
{{ ... }}
-
注释
{# ... #}
另外,在模板中,Jinja2支持使用.
取变量的属性,比如user
字典中的username
键值通过.
获取,即user.user-name
,在效果上等同于user['username']
。
简单地说,我们可以在模板中使用Python语句和表达式来操作数据的输出。但需要注意的是,Jinja2并不支持所有Python语法。而且出于效率和代码组织等方面的考虑,我们应该适度使用模板,仅把和输出控制有关的逻辑操作放到模板中。
Jinja2允许你在模板中使用大部分Python对象,比如字符串、列表、字典、元组、整型、浮点型、布尔值。它支持基本的运算符号(+
、-
、*
、/
等)、比较符号(比如==
、!=
等)、逻辑符号(and
、or
、not
和括号)以及in
、is
、None
和布尔值(True
、False
)。
Jinja2提供了多种控制结构来控制模板的输出,其中for
和if
是最常用的两种。
在Jinja2里,语句使用{% ... %}
标识,尤其需要注意的是,在语句结束的地方,我们必须添加结束标签,不能省略。
{% if user.bio %}
<i>{{ user.bio }}</i>
{% else %}
<i>This user has not provided a bio.</i>
{% endif %}
和在Python里一样,for语句用来迭代一个序列:
<ul>
{% for movie in movies %}
<li>{{ movie.name }} - {{ movie.year }}</li>
{% endfor %}
</ul>
在for循环内,Jinja2提供了多个特殊变量,常用的循环变量如下表所示。
变量名 | 说明 |
---|---|
loop.index | 当前迭代数(从1开始计数) |
loop.index0 | 当前迭代数(从0开始计数) |
loop.revindex | 当前反向迭代数(最后一个元素计为1) |
loop.revindex0 | 当前反向迭代数(最后一个元素计为0) |
loop.first | 如果是第一个元素,则为True |
loop.last | 如果是最后一个元素,则为True |
loop.previtem | 上一个迭代的条目 |
loop.nextitem | 下一个迭代的条目 |
loop.length | 序列包含的元素数量 |
在视图函数中渲染模板时,我们使用Flask提供的渲染函数render_template()
:
from flask import Flask, render_template
# ...
@app.route('/watchlist')
def watchlist():
return render_template('watchlist.html', user=user, movies=movies)
在render_template()
函数中,我们首先传入模板的文件名作为参数。Flask会在程序根目录下的templates
文件夹里寻找模板文件,所以这里传入的文件路径是相对于templates根目录的。
除了模板文件路径,我们还以关键字参数的形式传入了模板中使用的变量值,以user
为例:左边的user
表示传入模板的变量名称,右边的user
则是要传入的对象。
此外,Flask还提供了一个
render_template_string()
函数用来渲染模板字符串。
传入Jinja2中的变量值可以是字符串、列表和字典,也可以是函数、类和类实例,这完全取决于你在视图函数传入的值。
模板辅助工具
上下文变量
模板上下文包含了很多变量,其中包括我们调用render_template()
函数时手动传入的变量以及Flask默认传入的变量。除了渲染时传入变量,你也可以在模板中定义变量,使用set
标签:
{% set navigation = [('/', 'Home'), ('/about', 'About')] %}
你也可以将一部分模板数据定义为变量,使用set
和endset
标签声明开始和结束:
{% set navigation %}
<li><a href="/">Home</a>
<li><a href="/about">About</a>
{% endset %}
Flask在模板上下文中提供了一些内置变量,可以在模板中直接使用:
变量 | 说明 |
---|---|
config | 当前的配置对象 |
request | 当前的请求对象,在已激活的请求环境下可用 |
session | 当前的会话对象,在已激活的请求环境下可用 |
g | 与请求绑定的全局变量,在已激活的请求环境下可用 |
提示:Flask除了把
g
、session
、config
、request
对象注册为上下文变量,也将它们设为全局变量,因此可以全局使用。
如果多个模板都需要使用同一变量,那么比起在多个视图函数中重复传入,更好的方法是能够设置一个模板全局变量。
Flask提供了一个app.context_processor
装饰器,可以用来注册模板上下文处理函数,它可以帮我们完成统一传入变量的工作。模板上下文处理函数需要返回一个包含变量键值对的字典:
@app.context_processor
def inject_foo():
foo = 'I am foo.'
return dict(foo=foo) # 等同于 return {'foo': foo}
当我们调用render_template()
函数渲染任意一个模板时,所有使用app.context_processor
装饰器注册的模板上下文处理函数(包括Flask内置的上下文处理函数)都会被执行,这些函数的返回值会被添加到模板中,因此我们可以在模板中直接使用foo
变量。
除了使用
app.context_processor
装饰器,也可以直接将其作为方法调用,传入模板上下文处理函数:def inject_foo(): foo = 'I am foo.' return dict(foo=foo) app.context_processor(inject_foo)
使用
lambda
可以简化为:app.context_processor(lambda: dict(foo='I am foo.'))
全局对象
全局对象是指在所有的模板中都可以直接使用的对象,包括在模板中导入的模板,后面我们会详细介绍导入的概念。
Jinja2在模板中默认提供了一些全局函数,常用的三个函数如下表所示:
函数 | 说明 |
---|---|
range([start, ]stop[, step]) | 和 Python中的range 用法相同 |
lipsum(n=5, html=True, min=20, max=100) | 生成随机文本(lorem ipsum),可以在测试时用来填充页面,默认生成5段HTML文本,每段包含20~100个单词 |
dict(**items) | 和 Python中的dict() 用法相同 |
除了Jinja2内置的全局函数,Flask也在模板中内置了两个全局函数:
函数 | 说明 |
---|---|
url_for() | 用于生成URL的函数,用法和在Python脚本中相同。 |
get_flashed_messages() | 用于获取flash消息的函数 |
与注册模板上下文处理函数类似,我们可以使用app.template_global
装饰器直接将函数注册为模板全局函数:
@app.template_global()
def bar():
return 'I am bar.'
默认使用函数的原名称传入模板,在app.template_global()
装饰器中使用name
参数可以指定一个自定义名称。
app.template_global()
仅能用于注册全局函数,后面我们会介绍如何注册全局变量。
你可以直接使用
app.add_template_global()
方法注册自定义全局函数,传入函数对象和可选的自定义名称(name
),比如app.add_template_global(your_global_function)
。
过滤器
在Jinja2中,过滤器(filter)是一些可以用来修改和过滤变量值的特殊函数。过滤器的使用方法之一是将变量和过滤器用一个竖线|
(管道符号)隔开,过滤器函数的第一个参数表示被过滤的变量值(value)或字符串(s),即竖线符号左侧的值,其他的参数可以通过添加括号传入:
{{ name|title }} {# 相当于在Python里调用 name.title() #}
{{ movies|length }} {# 相当于在Python里调用 len(movies) #}
另一种用法是将过滤器作用于一部分模板数据,使用filter
标签和endfilter
标签声明开始和结束。比如,下面使用upper
过滤器将一段文字转换为大写:
{% filter upper %}
This text becomes uppercase.
{% endfilter %}
Jinja2提供了许多内置过滤器,常用的过滤器如下表所示。
过滤器 | 说明 |
---|---|
default(value, default_value='', boolean=False) | 设置默认值,默认值作为参数传入,别名为d |
escape(s) | 转义HTML文本,别名为e |
first(seq) | 返回序列的第一个元素 |
last(seq) | 返回序列的最后一个元素 |
length(object) | 返回变量的长度 |
random(seg) | 返回序列中的随机元素 |
safe(value) | 将变量值标记为安全,避免转义 |
trim(value) | 清除变量值前后的空格 |
max(value, case_sensitive=False, attribute=None) | 返回序列中的最大值 |
min(value, case_sensitive=False, attribute=None) | 返回序列中的最小值 |
unique(value, case_sensitive=False, attribute=None) | 返回序列中的不重复的值 |
striptags(value) | 清除变量值内的HTML标签 |
urlize(value, trim_url_limit=None, nofollow=False, target=None, rel=None) | 将URL文本转换为可单击的HTML链接 |
wordcount(s) | 计算单词数量 |
tojson(value, indent=None) | 将变量值转换为JSON格式 |
truncate(s, length=255,killwords=False, end='...', leeway=None) | 截断字符串,常用于显示文章摘要,length 参数设置截断的长度,killwords 参数设置是否截断单词,end 参数设置结尾的符号 |
附注 这里只列出了一部分常用的过滤器,完整的列表请访问http://jinja.pocoo.org/docs/2.10/templates/#builtin-filters查看。
过滤器可以叠加使用,下面的示例为name
变量设置默认值,并将其标题化:
<h1>Hello, {{ name|default('陌生人')|title }}!</h1>
如果内置的过滤器不能满足你的需要,还可以添加自定义过滤器。使用app.template_filter()
装饰器可以注册自定义过滤器:
from flask import Markup
@app.template_filter()
def musical(s):
return s + Markup(' ♫')
和注册全局函数类似,你可以在app.template_filter()
中使用name
关键字设置过滤器的名称,默认会使用函数名称。
过滤器函数需要接收被处理的值作为输入,返回处理后的值。过滤器函数接收s
作为被过滤的变量值,返回处理后的值。示例中创建的musical
过滤器会在被过滤的变量字符后面添加一个音符(singlebar note)图标,因为音符通过HTML实体♫
表示,我们使用Markup类将它标记为安全字符。在使用时和其他过滤器用法相同:
{{ name|musical }}
你可以直接使用
app.add_template_filter()
方法注册自定义过滤器,传入函数对象和可选的自定义名称(name
),比如app.add_template_filter(your_filter_function)
。
XSS攻击与转义
为了防范XSS攻击,根据Flask的设置,Jinja2会自动对模板中的变量进行转义,所以我们不用手动使用escape
过滤器或调用escape()
函数对变量进行转义。
默认的自动开启转义仅针对
.html
、.htm
、.xml
以及.xhtml
后缀的文件。此外,用于渲染模板字符串的
render_template_string()
函数也会对所有传入的字符串进行转义。
在确保变量值安全的情况下,这通常意味着你已经对用户输入的内容进行了“消毒”处理。这时如果你想避免转义,将变量作为HTML解析,可以对变量使用safe
过滤器:
另一种将文本标记为安全的方法是在渲染前将变量转换为Markup对象:
from flask import Markup
@app.route('/hello')
def hello():
text = Markup('<h1>Hello, Flask!</h1>')
return render_template('index.html', text=text)
这时在模板中可以直接使用{{ text }}
。
绝对不要直接对用户输入的内容使用
safe
过滤器,否则容易被植入恶意代码,导致XSS攻击。
测试器
在Jinja2中,测试器(Test)是一些用来测试变量或表达式,返回布尔值(True
或False
)的特殊函数。比如,number
测试器用来判断一个变量或表达式是否是数字,我们使用is
连接变量和测试器:
测试器 | 说明 |
---|---|
callable(object) | 判断对象是否可被调用 |
defined(value) | 判断变量是否已定义 |
undefined(value) | 判断变量是否未定义 |
none(value) | 判断变量是否为None |
number(value) | 判断变量是否是数字 |
string(value) | 判断变量是否是字符串 |
sequence(value) | 判断变量是否是序列,比如字符串、列表、元组 |
iterable(value) | 判断变量是否可迭代 |
mapping(value) | 判断变量是否是匹配对象,比如字典 |
sameas(value, other) | 判断变量与other 是否指向相同的内存地址 |
这里只列出了一部分常用的测试器,完整的内置测试器列表请访问http://jinja.pocoo.org/docs/2.10/tem-plates/#list-of-builtin-tests查看。
在使用测试器时,is
的左侧是测试器函数的第一个参数(value
),其他参数可以添加括号传入,也可以在右侧使用空格连接,以sameas
为例:
{% if foo is sameas(bar) %}
...
等同于:
{% if foo is sameas bar %}
...
和过滤器类似,我们可以使用Flask提供的app.template_test()
装饰器来注册一个自定义测试器。在示例程序中,我们创建了一个没有意义的baz
测试器,仅用来验证被测值是否为baz
@app.template_test()
def baz(n):
if n == 'baz':
return True
return False
测试器的名称默认为函数名称,你可以在app.template_test()
中使用name
关键字指定自定义名称。测试器函数需要接收被测试的值作为输入,返回布尔值。
你可以直接使用
app.add_template_test()
方法注册自定义测试器,传入函数对象和可选的自定义名称(name
),比如app.add_template_test(your_test_function)
。
模板环境对象
在Jinja2中,渲染行为由jinja2.Environment
类控制,所有的配置选项、上下文变量、全局函数、过滤器和测试器都存储在Environment
实例上。
当与Flask结合后,我们并不单独创建Environment
对象,而是使用Flask创建的Environment
对象,它存储在app.jinja_env
属性上。在程序中,我们可以使用app.jinja_env
更改Jinja2设置。
比如,你可以自定义所有的定界符。下面使用variable_start_string
和variable_end_string
分别自定义变量定界符的开始和结束符号:
app = Flask(__name__)
app.jinja_env.variable_start_string = '[['
app.jinja_env.variable_end_string = ']]'
注意:在实际开发中,如果修改Jinja2的定界符,那么需要注意与扩展提供模板的兼容问题,一般不建议修改。
模板环境中的全局函数、过滤器和测试器分别存储在Environment
对象的globals
、filters
和tests
属性中,这三个属性都是字典对象。
除了使用Flask提供的装饰器和方法注册自定义函数,我们也可以直接操作这三个字典来添加相应的函数或变量,这通过向对应的字典属性中添加一个键值对实现,要在模板里使用的变量名称作为键,对应的函数对象或变量作为值。下面是几个简单的示例。
-
添加自定义全局对象
和
app.template_global()
装饰器不同,直接操作globals
字典允许我们传入任意Python对象,而不仅仅是函数,类似于上下文处理函数的作用。下面的代码使用
app.jinja_env.globals
分别向模板中添加全局函数bar
和全局变量foo
:def bar(): return 'I am bar.' foo = 'I am foo.' app.jinja_env.globals['bar'] = bar app.jinja_env.globals['foo'] = foo
-
添加自定义过滤器
下面的代码使用
app.jinja_env.filters
向模板中添加自定义过滤器smiling
:def smiling(s): return s + ' :)' app.jinja_env.filters['smiling'] = smiling
-
添加自定义测试器
下面的代码使用
app.jinja_env.tests
向模板中添加自定义测试器baz
:def baz(n): if n == 'baz': return True return False app.jinja_env.tests['baz'] = baz
访问http://jinja.pocoo.org/docs/latest/api/#jinja2.Environment查看
Enviroment
类的所有属性及用法说明。
模板结构组织
局部模板
比如,多个页面中都要在页面顶部显示一个提示条,这个横幅可以定义在局部模板_ban-ner.html
中。
我们使用include
标签来插入一个局部模板。比如,在其他模板中,我们可以在任意位置使用下面的代码插入_banner.html
的内容:
{% include'_banner.html' %}
为了和普通模板区分开,局部模板的命名通常以一个下划线开始。
宏
宏(macro)是Jinja2提供的一个非常有用的特性,它类似Python中的函数。在创建宏时,我们使用macro
和endmacro
标签声明宏的开始和结束。在开始标签中定义宏的名称和接收的参数,下面是一个简单的示例:
{% macro qux(amount=1) %}
{% if amount == 1 %}
I am qux.
{% elif amount > 1 %}
We are quxs.
{% endif %}
{% endmacro %}
使用时,需要像从Python模块中导入函数一样使用import
语句导入它,然后作为函数调用,传入必要的参数,如下所示:
{% from 'macros.html' import qux %}
...
{{ qux(amount=5) }}
另外,在使用宏时我们需要注意上下文问题。在Jinja2中,出于性能的考虑,并且为了让这一切保持显式,默认情况下包含(include
)一个局部模板会传递当前上下文到局部模板中,但导入(import
)却不会。具体来说,当我们使用render_template()
函数渲染一个foo.html
模板时,这个foo.html
的模板上下文中包含下列对象:
- Flask使用内置的模板上下文处理函数提供的
session
、g
、request
和config
。 - 扩展使用内置的模板上下文处理函数提供的变量。
- 自定义模板上下文处理器传入的变量。
- 使用
render_template()
函数传入的变量。 - Jinja2和Flask内置及自定义全局对象。
- Jinja2内置及自定义过滤器。
- Jinja2内置及自定义测试器。
使用include标签插入的局部模板(比如_banner.html
)同样可以使用上述上下文中的变量和函数。而导入另一个并非被直接渲染的模板(比如macros.html
)时,这个模板仅包含下列这些对象:
- Jinja2和Flask内置的全局函数和自定义全局函数。
- Jinja2内置及自定义过滤器。
- Jinja2内置及自定义测试器。
因此,如果我们想在导入的宏中使用前一个列表中的2、3、4项,就需要在导入时显式地使用with context
声明传入当前模板的上下文:
{% from "macros.html" import foo with context %}
虽然Flask使用内置的模板上下文处理函数传入
session
、g
、request
和config
,但它同时也使用app.jinja_env.globals
字典将这几个变量设置为全局变量,所以我们仍然可以在不显式声明传入上下文的情况下,直接在导入的宏中使用它们。
关于宏的编写,更多的细节请访问http://jinja.pocoo.org/docs/latest/templates/#macros查看。
模板继承
Jinja2的模板继承允许你定义一个基模板,把网页上的导航栏、页脚等通用内容放在基模板中,而每一个继承基模板的子模板在被渲染时都会自动包含这些部分。使用这种方式可以避免在多个模板中编写重复的代码。
编写基模板
基模板存储了程序页面的固定部分,通常被命名为base.html
或layout.html
。
<!DOCTYPE html>
<html>
<head>
{% block head %}
<meta charset="utf-8">
<title>
{% block title %}Template - HelloFlask{% endblock %}
</title>
{% block styles %}{% endblock %}
{% endblock %}
</head>
<body>
<nav>
<ul><li><a href="{{ url_for('index') }}">Home</a></li></ul>
</nav>
<main>
{% block content %}{% endblock %}
</main>
<footer>
{% block footer %}
...
{% endblock %}
</footer>
{% block scripts %}{% endblock %}
</body>
</html>
当子模板继承基模板后,子模板会自动包含基模板的内容和结构。为了能够让子模板方便地覆盖或插入内容到基模板中,我们需要在基模板中定义块(block),在子模板中可以通过定义同名的块来执行继承操作。
块的开始和结束分别使用block
和endblock
标签声明,而且块之间可以嵌套。
在这个基模板中,我们创建了六个块:head、title、styles、content、footer和scripts,分别用来划分不同的代码。其中,head块表示<head>
标签的内容,title表示<title>
标签的内容,content块表示页面主体内容,footer表示页脚部分,styles块和scripts块,则分别用来包含CSS文件和JavaScript文件引用链接或页内的CSS和JavaScript代码。
这里的块名称可以随意指定,而且并不是必须的。你可以按照需要设置块,如果你只需要让子模板添加主体内容,那么仅定义一个
content
块就足够了。
为了避免块的混乱,块的结束标签可以指明块名,同时要确保前后名称一致。比如:
{% block body %}
...
{% endblock body %}
编写子模板
{% extends 'base.html' %}
{% from 'macros.html' import qux %}
{% block content %}{% set name='baz' %}
<h1>Template</h1>
<ul>
<li><a href="{{ url_for('watchlist') }}">Watchlist</a></li>
<li>Filter: {{ foo|musical }}</li>
<li>Global: {{ bar() }}</li>
<li>Test: {% if name is baz %}I am baz.{% endif %}</li>
<li>Macro: {{ qux(amount=5) }}</li>
</ul>
{% endblock %}
我们使用extends
标签声明扩展基模板,它告诉模板引擎当前模板派生自base.html
。extends
必须是子模板的第一个标签。
在子模板中,我们可以对父模板中的块执行两种操作:
-
覆盖内容
当在子模板里创建同名的块时,会使用子块的内容覆盖父块的内容。比如我们在子模板
index.html
中定义了title块,内容为Home,这会把块中的内容填充到基模板里的title块的位置,最终渲染为<title>Home</title>
,content块的效果同理。 -
追加内容
如果想要向基模板中的块追加内容,需要使用Jinja2提供的
super()
函数进行声明,这会向父块添加内容。比如,下面的示例向基模板中的styles块追加了一行<style>
样式定义:{% block styles %} {{ super() }} <style> .foo { color: red; } </style> {% endblock %}
当子模板被渲染时,它会继承基模板的所有内容,然后根据我们定义的块进行覆盖或追加操作。
模板进阶实践
这部分笔记优先级不高,以后有时间整理,待办任务:
3.4.1 空白控制
事实上,我们没有必要严格控制HTML输出,因为多余的空白并不影响浏览器的解析。
在部署时,我们甚至可以使用工具来去除HTML响应中所有的空白、空行和换行,这样可以减小文件体积,提高数据传输速度。所以,编写模板时应以可读性为先。
加载静态文件
在Flask程序中,默认我们需要将静态文件存储在与主脚本(包含程序实例的脚本)同级目录的static
文件夹中。
Flask内置了用于获取静态文件的视图函数,端点值为static
,它的默认URL规则为/static/<path:filename>
,URL变量filename
是相对于static
文件夹的文件路径。
如果你想使用其他文件夹来存储静态文件,可以在实例化
Flask
类时使用static_folder
参数指定,静态文件的URL路径中的static也会自动跟随文件夹名称变化。在实例化Flask类时使用static_url_path
参数则可以自定义静态文件的URL路径。
使用范例:
-
添加样式表
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename= 'styles.css' ) }}">
-
添加Favicon
<link rel="icon" type="image/x-icon" href="{{ url_for('static', filename='favicon.ico') }}">
-
使用宏加载静态资源
{% macro static_file(type, filename_or_url, local=True) %} {% if local %} {% set filename_or_url = url_for('static', filename=filename_or_url) %} {% endif %} {% if type == 'css' %} <link rel="stylesheet" href="{{ filename_or_url }}" type="text/css"> {% elif type == 'js' %} <script type="text/javascript" src="{{ filename_or_url }}"></script> {% elif type == 'icon' %} <link rel="icon" href="{{ filename_or_url }}"> {% endif %} {% endmacro %} {# 使用方法 #} {{ static_file('css', 'css/bootstrap.min.css') }}
消息闪现
Flask提供了一个非常有用的flash()
函数,它可以用来“闪现”需要显示给用户的消息,比如当用户登录成功后显示“欢迎回来!”。
在视图函数调用flash()
函数,传入消息内容即可“闪现”一条消息。
from flask import Flask, render_template, flash
app = Flask(__name__)
app.secret_key = 'secret string'
@app.route('/flash')
def just_flash():
flash('I am flash, who is looking for me?')
return redirect(url_for('index'))
通过
flash()
函数发送的消息会存储在session
对象中,所以我们需要为程序设置密钥。可以通过app.secret_key
属性或配置变量SECRET_KEY
设置,具体可参考2.3.4节的相关内容。
当然,它并不是我们想象的,能够立刻在用户的浏览器弹出一条消息。实际上,使用功能flash()
函数发送的消息会存储在session中,我们需要将用户的页面刷新(重定向到可获取flash消息的模板),并在模板中使用全局函数get_flashed_messages()
获取消息并将其显示出来。
<main>
{% for message in get_flashed_messages() %}
<div class="alert">{{ message }}</div>
{% endfor %}
{% block content %}{% endblock %}
</main>
因为同一个页面可能包含多条要显示的消息,所以这里使用for
循环迭代get_flashed_messages()
返回的消息列表。
自定义错误页面
错误处理函数和视图函数很相似,返回值将会作为响应的主体,因此我们首先要创建错误页面的模板文件。
为了和普通模板区分开来,我们在模板文件夹templates
里为错误页面创建了一个errors
子文件夹,并在其中为最常见的404
和500
错误创建了模板文件。例如,表示404
页面的404.html
模板内容如下所示。
{% extends 'base.html' %}
{% block title %}404 - Page Not Found{% endblock %}
{% block content %}
<h1>Page Not Found</h1>
<p>You are lost...</p>
{% endblock %}
错误处理函数需要附加app.errorhandler()
装饰器,并传入错误状态码作为装饰器参数。错误处理函数本身则需要接收异常类作为参数,并在返回值中注明对应的HTTP状态码。当发生错误时,对应的错误处理函数会被调用,它的返回值会作为错误响应的主体。
from flask import Flask, render_template
# ...
@app.errorhandler(404)
def page_not_found(e):
return render_template('errors/404.html'), 404
错误处理函数接收异常对象作为参数,内置的异常对象提供了下列常用属性,如下表所示。
属性 | 说明 |
---|---|
code | 状态码 |
name | 原因短语 |
description | 错误描述,另外使用get_description() 方法还可以获取HTML格式的错误描述代码 |
如果你不想手动编写错误页面的内容,可以将这些信息传入错误页面模板,在模板中用它们来构建错误页面。不过需要注意的是,传入500错误处理器的是真正的异常对象,通常不会提供这几个属性,你需要手动编写这些值。
Flask通过抛出Werkzeug中定义的HTTP异常类来表示HTTP错误,错误处理函数接收的参数就是对应的异常类。
基于这个原理,你也可以使用
app.errorhandler()
装饰器为其他异常注册处理函数,并返回自定义响应,只需要在app.errorhandler()
装饰器中传入对应的异常类即可。比如,使用
app.errorhandler(NameError)
可以注册处理NameError
异常的函数。
JavaScript和CSS中的Jinja2
只有使用render_template()
传入的模板文件才会被渲染,如果你把Jinja2代码写在单独的JavaScript或是CSS文件中,尽管你在HTML中引入了它们,但它们包含的Jinja2代码永远也不会被执行。
对于这类情况,下面有一些Tips:
-
行内/嵌入式JavaScript/CSS
如果要在JavaScript和CSS文件中使用Jinja2代码,那么就在HTML中使用
<style>
和<script>
标签定义这部分CSS和JavaScript代码。在这部分CSS和JavaScript代码中加入Jinja2时,不用考虑编写时的语法错误,比如引号错误,因为Jinja2会在渲染后被替换掉,所以只需要确保渲染后的代码正确即可。
不过我并不推荐使用这种方式,尤其是行内JavaScript/CSS会让维护变得困难。避免把大量JavaScript代码留在HTML中的办法就是尽量将要使用的Jinja2变量值在HTML模板中定义为JavaScript变量。
-
定义为JavaScript/CSS变量
-
JavaScript
对于想要在JavaScript中获取的数据,如果是元素特定的数据,比如某个文章条目对应的
id
值,可以通过HTML元素的data-*
属性存储。你可以自定义横线后的名称,作为元素上的自定义数据变量,如data-id
,data-username
等,比如:<span data-id="{{ user.id }}" data-username="{{ user.username }}">{{ user.username }}</span>
在JavaScript中,我们可以使用DOM元素的
dataset
属性获取data-*
属性值,比如element.dataset.username
,或是使用getAttribute()
方法,比如element.getAttribute('data-user-name')
;使用jQuery时,可以直接对jQuery对象调用data
方法获取,比如$element.data('username')
。在HTML中,
data-*
被称为自定义数据属性(custom data attribute),我们可以用它来存储自定义的数据供JavaScript获取。在后面的其他程序中,我们也会频繁使用这种方式来传递数据。对于需要全局使用的数据,则可以在页面中使用嵌入式JavaScript定义变量,如果没法定义为JavaScript变量,那就考虑定义为函数,比如:
<script type="text/javascript"> var foo = '{{ foo_variable }}'; </script>
当你在JavaScript中插入了太多Jinja2语法时,或许这时你该考虑将程序转变为Web API,然后专心使用JavaScript来编写客户端,在本书的第二部分我们会介绍如何编写WebAPI。
-
CSS
有些时候你会需要将Jinja2变量值传入CSS文件,比如我们希望将用户设置的主题颜色设置到对应的CSS规则中,或是需要将
static
目录下某个图片的URL传入CSS来设置为背景图片,除了将这部分CSS定义直接写到HTML中外,我们可以将这些值定义为CSS变量,如下所示:<style> :root { --theme-color: {{ theme_color }}; --background-url: {{ url_for('static', filename='background.jpg') }} } </style>
在CSS文件中,使用
var()
函数并传入变量名即可获取对应的变量值:#foo { color: var(--theme-color); } #bar { background: var(--background-url); }
-