Web 开发中, 在前后端不分离的模式状态下, 模板是最为重要的一部分, 我们可以将前端的页面放在 templates 目录下, 然后再视图函数中, 调用render_templates 函数, 将后端数据通过 key=value 的形式, 传递模板, 更能方便的处理数据。
模板基本使用:
在templates目录下创建一个watchlist.html作为模板文件
<!DOCTYPE html>
<html lang="en">
<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 %}
{# 下面是电影清单(这是注释) #}
<h5>{{ user.username }}'s Watchlist ({{ movies|length }}):</h5>
<ul>
{% for movie in movies %}
<li>{{ movie.name }} - {{ movie.year }}</li>
{% endfor %}
</ul>
</body>
</html>
有了模板, 自然要有一个视图函数去渲染它:
@app.route('/')
def movie_list():
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'},
]
return render_template('watchlist.html', user=user, movies=movies)
假如 user, movies 的数据是从数据库中查询出来的。
可以看到这里调用了 flask 提供的 render_template 方法, 然后将数据以 key=value 的形式传递到模板中, 就可以在模板中方便的处理渲染了
上下文:
模板上下文包含了很多变量,其中包括我们调用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 模板中提供了一些内置变量, 这些变量可以直接在模板中使用,而无需传入
自定义上下文:
除了内置的变量,我们也可以通过flask 提供的 context_processor 装饰器, 来自定义一些变量供模板使用:
@app.context_processor
def inject_foo():
data = 'Hello Flask!!!'
return {'data': data}
然后, 可以直接在模板中调用这个 data 变量:
当然, 我们也可以不用定义一个函数, 来自定义变量传入模板, 我们也可以 使用全局变量, 这个视情况而定, 这时候, 我们需要使用 lamda 匿名函数
内置全局函数:
在jinja2 中, 同样提供了一些内置函数
这里只列出了部分常用的全局函数,完整的全局函数列表请访问 http://jinja.pocoo.org/docs/2.10/templates/#list-of-global-functions 查看。
除了Jinja2内置的全局函数,Flask也在模板中内置了两个全局函数
Flask除了把g、session、config、request对象注册为上下文变量,也将它们设为全局变量,因此可以全局使用。
url_for()用来获取URL,用法和在Python脚本中相同。在前面给出的watchlist.html模板中,用来返回主页的链接直接写出。在实际的代码中,这个URL使用url_for()生成,传入index视图的名称:
<a href="{{ url_for('index') }}">hello flask</a>
自定义全局函数:
除了使用app.context_processor注册模板上下文处理函数来传入函数,我们也可以使用app.template_global装饰器直接将函数注册为模板全局函数。
@app.template_global()
def test_func():
return 'i am flask test'
默认使用函数的原名称传入模板,在app.template_global()装饰器中使用name参数可以指定一个自定义名称。app.template_global()仅能用于注册全局函数。
你可以直接使用app.add_template_global()方法注册自定义全局函数,传入函数对象和可选的自定义名称(name),比如app.add_template_global(your_global_function)。
过滤器:
在模板中, 使用过滤器是可以方便的修饰和过滤变量的特殊函数。过滤器和变量之间用 | 隔开, 例如, 把 flask 这个单词的 f 转换为大写:
{{ name|title }}
{{ movies|length }}
这会将name变量的值标题化,相当于在Python里调用name.title()。再比如,我们在本章开始的示例模板watchlist.html中使用length获取movies列表的长度,类似于在Python中调用len(movies)
另一种用法是将过滤器作用于一部分模板数据,使用filter标签和endfilter标签声明开始和结束。比如,下面使用upper过滤器将一段文字转换为大写:
{% filter upper %}
This text becomes uppercase.
{% endfilter %}
内置过滤器:
这里只列出了一部分常用的过滤器,完整的列表请访问
http://jinja.pocoo.org/docs/2.10/templates/#builtin-filters
在使用过滤器时,列表中过滤器函数的第一个参数表示被过滤的变量值(value)或字符串(s),即竖线符号左侧的值,其他的参数可以通过添加括号传入。
另外,过滤器可以叠加使用,下面的示例为name变量设置默认值,并将其标题化:
<h1>Hello, {{ name|default('flask')|title }}!</h1>
使用 safe 过滤器可以将 html 文本内容转变成浏览器可渲染的内容, 但是需要注意的是, 在完成一些列的判断处理之后,才可以调用safe, 以免用户传递恶意代码。
{{ sanitized_text|safe }}
另一种将文本标记为安全的方法是在渲染前将变量转换为Markup对象:
注:高版本的flask 好像已经弃用了 Markup, 这里只做一个了解即可
from flask import Markup
@app.route('/hello')
def hello():
text = Markup('<h1>Hello, Flask!</h1>')
return render_template('index.html', text=text)
自定义过滤器:
如果提供的内置过滤器满足不了我们的需求, 那么我们就需要自定义过滤器了.
使用app.template_filter()装饰器可以注册自定义过滤器.
from flask import Flask, render_template
app = Flask(__name__)
@app.route('/')
def index():
data = 1
return render_template('index.html', data=data)
@app.template_filter()
def my_filter(data):
if data >=5:
return 'hello! everyone'
else:
return 'hello! flask'
if __name__ == '__main__':
app.run(debug=True)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
{{ data|my_filter }}
</body>
</html>
和注册全局函数类似,你可以在app.template_filter()中使用name关键字设置过滤器的名称,默认会使用函数名称。过滤器函数需要接收被处理的值作为输入,返回处理后的值。
你可以直接使用app.add_template_filter()方法注册自定义过滤器,传入函数对象和可选的自定义名称(name),比如app.add_template_filter(your_filter_function)但是更推荐使用装饰器的方式, 简单方便高效
测试器:
在Jinja2中,测试器(Test)是一些用来测试变量或表达式,返回布尔值(True或False)的特殊函数。比如,number测试器用来判断一个变量或表达式是否是数字,我们使用is连接变量和测试器:
{% if age is number %}
{{ age * 365 }}
{% else %}
无效的数字。
{% endif %}
内置测试器:
这里只列出了一部分常用的测试器,完整的内置测试器列表请访问
http://jinja.pocoo.org/docs/2.10/templates/#list-of-builtin-tests
在使用测试器时,is的左侧是测试器函数的第一个参数(value),其他参数可以添加括号传入,也可以在右侧使用空格连接,以sameas为例:
自定义测试器:
和过滤器类似,我们可以使用Flask提供的app.template_test()装饰器来注册一个自定义测试器
@app.template_test()
def baz(n):
if n == 'baz':
return True
return False
测试器的名称默认为函数名称,你可以在app.template_test()中使用name关键字指定自定义名称。测试器函数需要接收被测试的值作为输入,返回布尔值。
宏:
宏(macro)是Jinja2提供的一个非常有用的特性,它类似Python中的函数。使用宏可以把一部分模板代码封装到宏里,使用传递的参数来构建内容,最后返回构建后的内容。在功能上,它和局部模板类似,都是为了方便代码块的重用。
{% 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使用内置的模板上下文处理函数提供的g、session、config、request。
·扩展使用内置的模板上下文处理函数提供的变量。
·自定义模板上下文处理器传入的变量。
·使用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 %}
模板继承:
Jinja2的模板继承允许你定义一个基模板,把网页上的导航栏、页脚等通用内容放在基模板中,而每一个继承基模板的子模板在被渲染时都会自动包含这些部分。使用这种方式可以避免在多个模板中编写重复的代码。
父模板:
基模板存储了程序页面的固定部分,通常被命名为base.html或layout.html。示例程序中的基模板base.html中包含了一个基本的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代码。
子模板:
因为基模板中定义了HTML的基本结构,而且包含了页脚等固定信息,在子模板中我们不再需要定义这些内容,只需要对特定的块进行修改。这时我们可以修改前面创建的电影清单模板watchlist.html和主页模板index.html,将这些子模板的通用部分合并到基模板中,并在子模板中定义块来组织内容,以便在渲染时将块中的内容插入到基模板的对应位置。
我们使用extends标签声明扩展基模板,它告诉模板引擎当前模板派生自base.html。
{% 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必须是子模板的第一个标签。
我们在基模板中定义了四个块,在子模板中,我们可以对父模板中的块执行两种操作:
(1)覆盖内容
当在子模板里创建同名的块时,会使用子块的内容覆盖父块的内容。比如我们在子模板index.html中定义了title块,内容为Home,这会把块中的内容填充到基模板里的title块的位置,最终渲染为<title>Home</title>,content块的效果同理。
(2)追加内容
如果想要向基模板中的块追加内容,需要使用Jinja2提供的super()函数进行声明,这会向父块添加内容。比如,下面的示例向基模板中的styles块追加了一行<style>样式定义:
当子模板被渲染时,它会继承基模板的所有内容,然后根据我们定义的块进行覆盖或追加操作
{% block styles %}
{{ super() }}
<style>
.foo {
color: red;
}
</style>
{% endblock %}