✅【Flask笔记】第三节:模版

img

旧的笔记,以前存放的笔记软件停止运营了,重新迁移到博客上,删除和修改一些过时的内容。

0x00 模版基础用法

1. 创建模版

Jinja2常见的三种定界符:

语句:比如if判断,for循环等。

1
{%...%}

表达式:比如字符串,变量,函数调用等。

1
{{...}}

注释。

1
{#...#}

2. 模版语法

利用Jinja2这样等模版引擎,可以将一部分程序逻辑放在模版中,但是Jinja2并不支持所有的Python语法,我们应该适度使用模版,仅把和输出控制相关的逻辑操作放到模版中。

Jinja2允许在模版中使用大量Python对象,比如字符串,列表,字典,元组,整型,浮点型,布尔值。它支持基本的运算符号(+、-、*、/等)、比较符号(比如==、!=等)、逻辑符号(and、or、not和括号)以及in、is、None和布尔值(Ture、False)。

Jinja2提供了多种控制结构,其中for和if是最常用的两种:

1
2
3
4
5
6
7
8
9
10
11
{% if user.bio %}
<i>{{user.bio}}</i>
{% else %}
<i>This is user has not provided a bio.</i>
{% endif %}

<ul>
{% for movie in movies %}
<li>{{movie.name}}-{{movie.year}}</li>
{% endfor %}
</ul>

另外在模版中,Jinja2支持使用.获取变量属性。比如user字典中的username可以通过.获取,等同于user['username']

for循环内,Jinja2提供了多个特殊变量,常用的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 序列包含的元素数量

3. 渲染模版

渲染模版就是执行模版中代码,渲染后的结果就是我们要返回给客户的的HTML响应。在视图函数中渲染模版并不知节使用jinja2提供的函数,而是使用Flash中的render_template()。在render_template()函数中,Flask会在程序的根目录下的templates文件夹里寻找模版文件。同时还以关键字参数的形式传入模版中使用的变量值。

1
2
3
4
5
from flask import Flask,render_template
...
@app.route('/watchlist')
def watchlist():
return render_template('watchlist.html',user = user,movies = movies)

除了render_template()函数,Flask还提供了一个render_template_string()函数用来渲染模版字符串

0x01 模版辅助工具

1. 上下文

在模版的上下文中包含很多变量,其中包括渲染模版时候render_template()函数中传递的变量以及Flask默认传入的变量。

模版中也可以定义变量:

1
{% set navigation = [('/','Home'),('/about','About')]%}

也可以将一部分模版数据定义为变量,使用setendset标签声明开始和结束:

1
2
3
4
{% set navigation%}
<li><a href="/">Home</a></li>
<li><a href="/about">About</a></li>
{% endset %}

1.1 内置上下文变量

Flask在模版上下文中提供了一些内置变量,可以在模版中直接使用。

变量 说明
config 当前配置对象
request 当前请求对象,在已激活的请求环境下使用
session 当前会话对象,在已激活的请求环境下使用
g 与请求绑定的全局变量,在已激活的请求环境下使用

1.2 自定义上下文

如果多个模版都需要使用同一个变量,那么比起在多个视图函数中重复传入,更好的方法是能够设置一个模版全局变量。使用@app.context_processor注册模版上下文处理函数,它可以帮助我们完成统一传入变量的工作:

1
2
3
4
@app.context_processor
def inject_foo():
foo = 'I am foo.'
return dict(foo = foo) #等同于 retunr {'foo':foo}

当我们调用render_template()函数渲染任意一个模版时,所有使用@app.context_processor装饰器的模版上下文处理函数都会被执行,这些函数的返回值会被添加到模版中。因此我们可以在模版中直接使用foo变量。

除了使用@app.context_processor装饰器,也可以直接将其作为方法调用,传入模版上下文处理函数:

1
2
3
4
def inject_foo():
foo = 'I am foo.'
return dict(foo = foo)
app.context_processor(inject_foo)

使用lambda简化:

1
app.context_processor(lambda:dict(foo='I am foo.'))

2. 全局对象

全局对象是指在所有模版中都可以直接使用的对象。

2.1 内置全局函数

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()用法相同

Flask内置模版全局函数:

函数 说明
url_for() 用于生成URL的函数
get_flashed_messages() 用于获取flash消息的函数

Flask除了把g,session,config,request对象注册为上下文变量,也将它们设为全局变量,因此可以全局使用url_for()。

2.2 自定义全局函数

除了使用@app.context_processor注册模版上下文处理函数来传入函数,也可以使用app.template_global装饰器可以直接将函数注册为模版全局函数。默认使用函数的原名称传入模版,在app.template_global()装饰器中使用name参数可以指定一个自定义名称,app.template_global仅能用于注册全局函数。

1
2
3
@app.template_global()
def bar():
return 'I am bar.'

你可以直接使用app.add_template_global()方法注册自定义全局函数,传入函数对象和可选的自定义名称(name),比如app.add_template_global(your_global_function)

3. 过滤器

在Jinja2中,过滤器是一些可以修改和过滤变量的特殊函数。

两种过滤器的写法:

1
{{ name|title }} #将name变量的值标题化。相当于Python中的name.title()
1
2
3
4
#使用filter和endfilter标签声明开始和结束,upper过滤器将一段文字转换为大写。
{% filter upper %}
This text becomes uppercase.
{% endfilter %}

3.1 内置过滤器

Jinja2常用的内置过滤器:

过滤器 说明
default(value,default_value = ‘’,boolean=False) 设置默认值,默认值作为参数传入,别名为d
escape(s) 转义HTML文本,别名为e
first(seq) 返回序列的第一个元素
last(seq) 返回序列的最后一个元素
length(object) 返回变量的长度
random(seq) 返回序列中的随机元素
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参数设置结尾符号

在使用过滤器时,列表中过滤器函数的第一个参数表示被过滤的变量值(Value)或字符串(s),即竖线符号左侧的值,其他参数可以通过添加括号传入。

过滤器可以叠加使用:

1
<h1>Hello,{{name|default('陌生人')|title}}</h1>
XSS相关

jinja2会自动对模版中的变量进行转义,所以不需要手动使用escape过滤器或调用escape()函数对变量进行转义。默认情况下,仅对.html.htm.xml.xhtml后缀名进行转义。

如果像避免转义可以使用safe过滤器{{ sanitized_text|safe }},另一种将文本标记安全的方法是在渲染前将变量转换为Markup对象:

1
2
3
4
5
6
from flask import Markup

@app.route('/hello')
def hello():
text = Markup('<h1>Hello,Flask!</h1>')
return render_template('index.html',text = text)

这时在模版中可以直接使用{{ text }}

3.2 自定义过滤器

使用app.template_filter()装饰器可以注册自定义的过滤器:

1
2
3
4
5
from flask import Markup

@app.template_filter()
def musical(s):
return s+Markup('#%9835;')

和注册全局函数类型,你可以在app.template_filter()中使用name关键字设置过滤器的名称,默认会使用函数名称。

1
{{ name|musical}}

你可以直接使用app.add_template_filter()方法注册自定义过滤器,传入函数对象和可选的自定义名称(name),比如app.add_template_filter()(your_filter_function)

4. 测试器

在Jinja2中,测试器是一些用来测试变量或表达式,返回布尔值的特殊函数。

1
2
3
4
5
{% if age is number %}
{{ age* 365}}
{% else %}
无效数字
{% endif %}

4.1 内置测试器

常用的内置测试器:

测试器 说明
callable(object) 判断变量是否可被调用
defined(value) 判断变量是否已定义
undefined(value) 判断变量是否为定义
none(value) 判断变量是否是否为None
number(value) 判断变量是否为数字
string(value) 判断变量是否为字符串
sequence(value) 判断变量是否为序列,比如字符串,列表,元组
iterable(value) 判断变量是否可迭代
mapping(value) 判断变量是否匹配对象,比如字典
sameas(value,other) 判断变量与other是否指向相同的内存地址

在使用测试器时,is左侧是测试器函数的第一个参数(value),其他参数可以添加括号传入,也可以在右侧使用空格链接:

1
{% if foo is sameas(bar) %}

等同于:

1
{% if foo is sameas bar %}

4.2 自定义测试器

可以使用Flask提供的app.template_test()装饰器来注册一个自定义测试器:

1
2
3
4
5
@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)

5. 模版环境对象

在Jinja2中渲染行为由jinja2.Environment类控制,所有配置选项,上下文变量,全局函数,过滤器,测试器都存储在Environment的实例上。当与Flask结合后,不再单独创建Environment对象。而是使用Flask创建的Environment对象。它存储在app.jinja_env属性上。

例如,可以自定义jinja2的变量定界符:

1
2
3
app = Flask(__name)
app.jinja_env.varibale_start_string = '[['
app.jinja_env.varibale_end_string = ']]'

实际开发中没必要修改。

模版环境中的全局函数,过滤器,测试器分别存储在Environment对象的globals、filters和tests属性中,这三个属性都是字典对象,除了使用Flask装饰器和方法自定义函数,也可以直接操作这三个字典来添加对应的函数或变量。

注意:以下内容选修,实际开发过程中基本不用用到。

5.1 添加自定义全局对象

直接操作globals字典允许我们传入任意python对象。下面代码使用app.jinja_env.globals添加全局函数和全局变量

1
2
3
4
5
def bar():
return 'I am bar'
foo = 'I am foo'
app.jinja_env.globals['bar'] = bar
app.jinja_env.globals['bar'] = bar

5.2 添加自定义过滤器

使用app.jinja_env.filters添加自定义过滤器。

1
2
3
def smiling(s):
return s+':)'
app.jinja_env.filters['smiling'] = smiling

5.3 添加自定义测试器

使用app.jinja_env.tests添加自定义测试器。

1
2
3
4
5
def baz(n):
if n == 'baz':
return True
return False
app.jinja_env.tests('baz') = baz

0x02 模版组织结构

除了使用函数,过滤器等工具控制模版输出外,Jinja2还提供了一些工具来宏观上组织模版内容。

1. 局部模版

当多个独立模版中都会使用同一块HTML代码时,我们可以把这部分代码抽离出来,存储到局部模版中,达到一个复用代码等效果。我们用include标签插入一个局部模版,这会把局部模版的全部内容插在使用include标签的位置。

1
{% include '_banner.html' %}

为了和普通模版区分开,局部模版命名通常以一个下划线开始。

当程序中某个视图用来处理Ajax请求时,返回数据不需要包含完整的HTML结构,这时可以返回渲染后的局部模版。

2. 宏(macro)

宏类似Python中的函数,用宏可以把一部分模版封装到宏里,使用传递参数来构建内容。为了方便管理,我们可以把宏存储在单独的文件中,这个文件通常命名为macros.html_macros.html

1
2
3
4
5
6
7
{% macros qux(amount=1) %}
{% if amount ==1 %}
I am qux.
{% elif amount>1 %}
We are qux.
{% endif %}
{% endmacros %}

使用时需要像从Python模块中导入函数一样使用import语句导入它,然后作为函数调用,传入必要参数。

1
2
3
{% from 'macros.html' import qux %}
...
{% qux(amount=5) %}

出于性能的考虑,include一个局部模版会传递当前上下文,但是import却不会。

例如,当使用render_template()函数渲染一个foo.html模版时候,foo.html的模版上下文包含下列对象:

  1. Flask使用内置模版上下文处理函数提供的g,session,config,request。
  2. 扩展使用内置的模版上下文处理函数提供的变量。
  3. 自定义模版上下文处理器传入的变量。
  4. 使用render_template()函数传入的变量。
  5. Jinja2和Flask内置及自定义的全局对象。
  6. Jinja2内置和自定义的过滤器。
  7. Jinja2内置和自定义的测试器。

使用include标签插入的局部模版同样地可以使用上述上下文中的变量和函数,而import的模版(例如宏),仅包含下列这些对象:

  1. Jinja2和Flask内置及自定义的全局对象。
  2. Jinja2内置和自定义的过滤器。
  3. Jinja2内置和自定义的测试器。

如果我们想使用2,3,4项,就需要在导入的时候显式地使用with context声明,传入当前模版上下文:

1
{% from 'macros.html' import qux with context %}

app.jinja_env.globals中将g、session、config和request设置为了全局变量,所以仍然可以不用显式声明,而在宏中直接使用它们。

3. 模版继承

在jinja2中还可以使用继承方式,减少重复的代码。例如可以把导航,页脚这些通用内容放在基模版中,而每一个继承的子模版在被渲染时候都会包含这些。

3.1 编写基模版

基模版存储了程序页面固定部分,通常被命名为base.htmllayout.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
<!DOCTYPE html>
<html>
<head>
{% block head %}
<meta charset="utf-9">
<title>
{% block title %}
Template - HelloFlask
{% endblock %}
</title>
{% block style %}
{% 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>

为了避免块的混乱,块结束的标签可以指明块名:

1
2
{% block body %}
{% endblock body %}

3.2 编写子模版

1
2
3
4
5
6
7
8
9
10
11
12
13
{% extends 'base.html' %}
{% from 'macros.html' import qux %}
{% block content %}
{% set name='baz' %}
<h1>Template</h1>
<ul>
<li><a href=" '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必须是子模版的第一个标签。

在子模版中可以对父模版中的块执行两种操作:

覆盖内容

在子模版中创建同名的块,可以覆盖父模版中同名的块。

追加内容

使用super()函数可以追加内容。

1
2
3
4
5
6
7
8
{% block styles %}
{{ super() }}
<style>
.foo {
color: red;
}
</style>
{% endblock %}

0x03 模版进阶

1. 空白控制

在实际输出的HTML文件中,模版中的jinja2语句会被渲染为空行,仍然占有位置,例如:

1
2
3
4
5
<div>
{% if True %}
<p>Hello!</p >
{% endif %}
</div>

实际输出:

1
2
3
4
5
<div>

<p>Hello!</p >

</div>

如果想要在去掉这些空行,可以在定界符内添加减号,比如{%- endfor%}会移除该语句的前的空白。

1
2
3
4
5
<div>
{% if True -%}
<p>Hello!</p >
{%- endif %}
</div>

添加减号后输出如下:

1
2
3
<div>
<p>Hello!</p >
</div>

另一种方法是使用模版环境对象:

1
2
app.jinja_env.trim_blocks = True
app.jinja_env.lstrip_blocks = True

宏内空白不受这两个属性控制。

实际上我们没必要严格控制HTML输出,在部署的时候也可以使用工具来去除。

2. 静态文件

在Flask中默认需要将静态文件存储在与主脚本同级目录的static文件夹中。

1
< img src="{{ url_for('static', filename='avatar.jpg') }}" width="50">

在URL中图片的访问路径是http://localhost:5000/static/avatar.jpg

另外也可以通过实例化Flask类时候,指定static_folder参数来自定义存储目录,URL路径会跟随文件夹的名称变化。也可以独立指定static_url_path参数,来自定义URL路径。

例如:

1
app = Flask(__name__, static_folder='assets')

存储的文件夹是assets,URL访问路径是:http://localhost:5000/static/avatar.jpg

1
app = Flask(__name__, static_folder='assets', static_url_path='/static')

同时修改static_folder参数和static_url_path参数,存储的文件夹是assets,URL访问路径是:http://localhost:5000/static/avatar.jpg

3. 消息闪现

Flask提供一个非常有用的函数flash()函数,比如当用户登录成功后,显示“欢迎回来!”。flash()函数发送的消息会存储在session中,我们需要在模版中使用全局函数get_flashed_message()获取消息并将其显示出来。

通过flash()发送的消息存储在session中。需要对程序设置密钥,可以通过app.secert_key属性或配置变量SECERT_KEY设置。

1
2
3
4
5
6
7
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'))

Flask中使用get_flashed_messages()函数在模版中获取消息,因为程序中每一个页面都有可能需要显示消息,所以我们把显示消息的代码放在基版本中,因为同一个页面可能包含多条要显示的消息,所以这里使用for循环迭代get_flashed_messages()返回的消息列表。

1
2
3
4
5
6
<main>
{% for message in get_flashed_messages() %}
<div class="alert">{{ message }}</div>
{% endfor %}
{% block content %}{% endblock %}
</main>

4. 自定义错误页面

可以通过注册错误处理函数来自定义错误页面,错误处理函数和视图函数很类似,返回值将会作为响应主体。

先创建错误页面的模版文件:

1
2
3
4
5
6
{% 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状态码。

1
2
3
4
5
from flask import Flask, render_template
...
@app.errorhandler(404)
def page_not_found(e):
return render_template('errors/404.html'), 404

另外也可以使用app.errorhandler()装饰器为其他异常注册处理函数,并返回自定义响应,只需要在app.errorhandler()传入对应的异常类即可。

错误处理函数接收异常对象e作为参数,内置的异常对象提供了以下常用的属性:

属性 说明
code 状态码
name 原因短语
description 错误描述,另外使用get_description()方法还可以获取HTML格式的错误描述代码

5. JS和CSS中的Jinja2

有时候我们会需要在JS和CSS中使用Jinja2提供的变量值,甚至是控制语句。比如通过传入模版的theme_color来控制页面主题色彩,或是根据用户是否登录来决定是否执行某个JS。

首先要知道只有使用render_template()传入的模版文件才会被渲染,如果你把jinja2代码写在单独的JS或CSS文件中,尽管你在HTML中引用了,但是其中的jinja2代码永远不会执行。

5.1 定义JS/CSS变量

想要在JS中获取数据,可以通过HTML元素的data-*属性存储,你可以自定义横线后面的名称,作为自定义数据。

1
<span data-id="{{ user.id }}" data-username="{{ user.username }}">{{ user.username }}</span>

在JS中可以使用DOM元素的dataset属性获取data-*的值,例如:element.dataset.username,或使用getAttribute()方法:element.getAttribute('data-username');使用JQuery时,可以直接对JQuery对象调用data方法,比如$element.data('username')

如果是需要全局使用的数据,可以在页面中嵌入JS定义变量。

1
2
3
<script type="text/javascript">
var foo = '{{ foo_variable }}';
</script>

CSS同理:

1
2
3
4
5
6
<style>
:root {
--theme-color: {{ theme_color }};
--background-url: {{ url_for('static', filename='background.jpg') }}
}
</style>

在CSS文件中使用var()函数传入变量名即可获取对应的值:

1
2
3
4
5
6
#foo {
color: var(--theme-color);
}
#bar {
background: var(--background-url);
}