模板
在动态Web程序中,视图函数返回的HTML数据往往需要根据相应的变量(比如查询参数)动态生成。
当HTML代码保存到单独的文件中时,就无法使用字符串格式化或拼接字符串的方式来在HTML代码中插入变量,这时需要使用模板引擎(template engine)。
借助模板引擎,我们可以在HTML文件中使用特殊的语法来标记出变量,这类包含固定内容和动态部分的可重用文件称为模板(template)。
模板引擎的作用就是读取并执行模板中的特殊语法标记,并根据传入的数据将变量替换为实际值,输出最终的HTML页面,这个过程被称为渲染(rendering)
Flask默认使用的模板引擎是Jinja2
模板的基本用法
创建虚拟数据
假设我们需要编写一个用户的电影清单页面,类似IMDB的watchlist页面的简易版,模板中要显示用户信息以及用户收藏的电影列表,包含电影的名字和年份。我们首先创建一些虚拟数据用于测试显示效果:
user = {
'username': 'Grey Li',
'bio': 'A boy who loves movies and music.',
}
movies = [
{'name': 'My Neighbor Totoro', 'year': '1988'},
{'name': 'Three Colours trilogy', 'year': '1993'},
{'name': 'Forrest Gump', 'year': '1994'},
{'name': 'Perfect Blue', 'year': '1997'},
{'name': 'The Matrix', 'year': '1999'},
{'name': 'Memento', 'year': '2000'},
{'name': 'The Bucket list', 'year': '2007'},
{'name': 'Black Swan', 'year': '2010'},
{'name': 'Gone Girl', 'year': '2014'},
{'name': 'CoCo', 'year': '2017'},
]
创建一个电影清单模板
我们在templates目录下创建一个watchlist.html作为模板文件,然后使用Jinja2支持的语法在模板中操作这些变量
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>{{ user.username }}'s Watchlist</title>
</head>
<body>
{# html的注释好像可以这样打 #}
{# 这是一个链接 名字是Return &larr应该是蓝色? 跳转去url是index的页面#}
<a href="{{ url_for('index') }}">← Return</a>
{# 应该是一个ifelse语句,如果判断用户的bio(就是签名)是否存在#}
<h2>{{ user.username }}</h2>
{% if user.bio %}
<i>{{ user.bio }}</i>
{% else %}
<i>This user has not provided a bio.</i> {% endif %}
{# 下面是电影清单 #}
<h5>{{ user.username }}'s Watchlist ({{ movies|length }}):</h5>
{# <ul> 标签用来定义无序列表 #}
<ul>
{% for movie in movies %}
{# <li> 标签定义列表项,有序列表和无序列表中都使用 <li> 标签。 #}
<li>{{ movie.name }} - {{ movie.year }}</li>
{% endfor %}
</ul>
</body>
</html>
在模板中添加Python语句和表达式时,需要使用特定的定界符把它们标示出来。
watchlist.html中涉及的模板语法,会在下面逐一介绍。
定界符
首先,可以在上面的代码中看到Jinja2里常见的三种定界符
-
语句:比如说if判断,for判断
{% ... %}
-
表达式:比如字符串、变量、函数调用等:
{{ ... }}
-
注释
{{#这里是注释#}}
另外,在模板中,Jinja2支持使用“.”获取变量的属性,比如user字典中的username键值通过“.”获取,即user.username,在效果上等同于 user[‘username’]。
模板语法
Jinja2提供了多种控制结构来控制模板的输出,其中for和if是最常用的两种。在Jinja2里,语句使用{%…%}标识,尤其需要注意的是,在语句结束的地方,我们必须添加结束标签:
{% if user.bio %}
<i>{{ user.bio }}</i>
{% else %}
<i>This user has not provided a bio.</i>
{% endif %}
末尾的{%endif%}
用来声明if语句的结束,这一行不能省略。
<ul>
{% for movie in movies %}
{# <li> 标签定义列表项,有序列表和无序列表中都使用 <li> 标签。 #}
<li>{{ movie.name }} - {{ movie.year }}</li>
{% endfor %}
</ul>
和在Python里一样,for语句用来迭代一个序列,和其他语句一样,你需要在for循环的结尾使用endfor标签声明for语句的结束。
在for循环内,Jinja2提供了多个特殊变量,常用的循环变量 如表所示
渲染模板
渲染一个模板,就是执行模板中的代码,并传入所有在模板中使用的变量,渲染后的结果就是我们要返回给客户端的HTML响应。
在视图函数中渲染模板时,我们并不直接使用Jinja2提供的函数,而是使用 Flask提供的渲染函数render_template()
@app.route('/watchlist')
def watchlist():
return render_template('watchlist.html',
user=user, movies=movies)
那么此时,根据我们传入的虚拟数据,render_template()渲染后返回的 HTML数据如下所示:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Grey Li's Watchlist</title>
</head>
<body>
<a href="/">← Return</a>
<h2>Grey Li</h2>
<i>A boy who loves movies and music.</i>
<h5>Grey Li's Watchlist (10):</h5>
<ul>
<li>My Neighbor Totoro - 1988</li>
<li>Three Colours trilogy - 1993</li>
<li>Forrest Gump - 1994</li>
<li>Perfect Blue - 1997</li>
<li>The Matrix - 1999</li>
<li>Memento - 2000</li>
<li>The Bucket list - 2007</li>
<li>Black Swan - 2010</li>
<li>Gone Girl - 2014</li>
<li>CoCo - 2017</li>
</ul>
</body>
</html>
注意:/index必须写一个界面才能运行成功
总代码
app.py
from flask import Flask,render_template
app = Flask(__name__)
@app.route('/')
def Hello_world():
return 'This is main interface'
user = {
'username': 'Grey Li',
'bio': 'A boy who loves movies and music.',
}
movies = [
{'name': 'My Neighbor Totoro', 'year': '1988'},
{'name': 'Three Colours trilogy', 'year': '1993'},
{'name': 'Forrest Gump', 'year': '1994'},
{'name': 'Perfect Blue', 'year': '1997'},
{'name': 'The Matrix', 'year': '1999'},
{'name': 'Memento', 'year': '2000'},
{'name': 'The Bucket list', 'year': '2007'},
{'name': 'Black Swan', 'year': '2010'},
{'name': 'Gone Girl', 'year': '2014'},
{'name': 'CoCo', 'year': '2017'},
]
@app.route('/watchlist')
def watchlist():
return render_template('watchlist.html', user=user, movies=movies)
@app.route('/index')
def index():
return "Good Job"
if __name__ == '__main__':
app.run()
watchlist,html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>{{ user.username }}'s Watchlist</title>
</head>
<body>
{# html的注释好像可以这样打 #}
{# 这是一个链接 名字是Return &larr是左箭头 跳转去url是index的页面#}
<a href="{{ url_for('index') }}">← Return</a>
{# 应该是一个ifelse语句,如果判断用户的bio(就是签名)是否存在#}
<h2>{{ user.username }}</h2>
{% if user.bio %}
<i>{{ user.bio }}</i>
{% else %}
<i>This user has not provided a bio.</i>
{% endif %}
{# 下面是电影清单 #}
<h5>{{ user.username }}'s Watchlist ({{ movies|length }}):</h5>
{# <ul> 标签用来定义无序列表 #}
<ul>
{% for movie in movies %}
{# <li> 标签定义列表项,有序列表和无序列表中都使用 <li> 标签。 #}
<li>{{ movie.name }} - {{ movie.year }}</li>
{% endfor %}
</ul>
</body>
</html>
模板辅助工具
为方便测试模板辅助工具,于是创建了一个基本的index.html 并在主页面写
from flask import render_template
@app.route('/')
def index():
return render_template('index.html')
上下文
模板上下文包含了很多变量,其中包括我们调用 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 %}
如果传入的是’/’,那就名字变为Home
如果传入的是’/home‘,那就名字变为About
自定义上下文
如果多个模板都需要使用同一变量,那么比起在多个视图函数中重复传入,更好的方法是能够设置一个模板全局变量。
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变量。
和在render_template()函数中传入变量也类似,除了字符串、列表 等数据结构,你也可以传入函数、类或类实例。
除了使用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.'))
全局对象
全局对象是指在所有的模板中都可以直接使用的对象,包括在模板中导入的模板,后面我们会详细介绍导入的概念。
- 内置全局函数
Flask除了把g、session、config、request对象注册为上下文变量,也 将它们设为全局变量,因此可以全局使用。 - 自定义全局函数:除了使用app.context_processor注册模板上下文处理函数来传入函数,我们也可以使用app.template_global装饰器直接将函数注册为模板全局函数。
@app.template_global()
def bar():
return 'I am bar.'
app.template_global()仅能用于注册全局函数,后面会介绍如何注册全局变量。
过滤器
在Jinja2中,过滤器(filter)是一些可以用来修改和过滤变量值的特殊函数,过滤器和变量用一个竖线隔开,需要参数的过滤器可以像函数一样使用括号传递。
{{ name|title }}
这会将name变量的值标题化,相当于在Python里调用 name.title()
再比如,我们在本章开始的示例模板watchlist.html中使用length获取movies列表的长度,类似于在Python中调用 len(movies):
{{ movies|length }}
另一种用法是将过滤器作用于一部分模板数据,使用filter标签和 endfilter标签声明开始和结束。比如,下面使用upper过滤器将一段文字转换为大写:
{% filter upper %}
This text becomes uppercase.
{% endfilter %}
Jinja2常用过滤器
在使用过滤器时,列表中过滤器函数的第一个参数表示被过滤的变量值(value)或字符串(s),即竖线符号左侧的值,其他的参数可以通过添加括号传入。
另外,过滤器可以叠加使用,下面的示例为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过滤器会在被过滤的变量字符后面添加一个音符(single bar note)图标,因为音符通过HTML 实体♫;表示,我们使用Markup类将它标记为安全字符。在使用时和其他过滤器用法相同
{{ name|musical }}
测试器
在Jinja2中,测试器(Test)是一些用来测试变量或表达式,返回布尔值(True或False)的特殊函数。
比如,number测试器用来判断一个变量或表达式是否是数字,我们使用is连接变量和测试器:
{% if age is number %}
{{ age * 365 }}
{% else %}
无效的数字。
{% endif %}
内置测试器
在使用测试器时,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