03. 模板

当用户访问程序的根地址时,视图函数会向客户端返回一行HTML代码。然而,一个完整的HTML页面往往需要几十行甚至上百行代码,如果都写到视图函数里,那可真是个噩梦。这样的代码既不简洁也难于维护,正确的做法是把HTML代码存储在单独的文件中,以便让程序的业务逻辑和表现逻辑分离,即控制器和用户界面的分离。

在动态Web程序中,视图函数返回的HTML数据往往需要根据相应的变量(比如查询参数)动态生成。当HTML代码保存到单独的文件中时,没法再使用字符串格式化或拼接字符串的方式来在HTML代码中插入变量,这时需要使用模板引擎。借助模板引擎,可以在HTML文件中使用特殊的语法来标记出变量,这类包含固定内容和动态部分的可重用文件称为模板(template)。

模板引擎的作用就是读取并执行模板中的特殊语法标记,并根据传入的数据将变量替换为实际值,输出最终的HTML页面,这个过程被称为渲染。Flask默认使用的模板引擎是Jinja2,它是一个功能齐全的Python模板引擎,除了设置变量,还允许在模板中添加if判断,执行for迭代,调用函数等,以各种方式控制模板的输出。对于Jinja2来说,模板可以是任何格式的纯文本文件,比如HTML、XML、CSV、LaTeX等。

一、模板基本用法

使用Jinja2创建HTML模板,并在视图函数中渲染模板,最终实现HTML响应的动态化。

1.创建模板

假设现在需要编写一个用户的电影清单页面,类似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文件夹,用于存放模板文件,然后使用Jinja2支持的语法在模板中操作这些变量。

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') }}">&larr; 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>

在模板中添加Python语句和表达式时,需要使用特定的定界符把它们标示出来。watchlist.html中涉及的模板语法,会在下面逐一介绍。首先,可以在上面的代码中看到Jinja2里常见的三种定界符:

  1. 语句

    比如if判断、for循环等

    {%...%}
    
  2. 表达式

    比如字符串、变量、函数调用等

    {{...}}
    
  3. 注释

    {#...#}
    

另外,在模板中,Jinja2支持使用“.”获取变量的属性,比如user字典中的username键值通过“.”获取,即user.username,在效果上等同于user['username']

2.模板语法

利用Jinja2这样的模板引擎,可以将一部分的程序逻辑放到模板中去。简单地说,可以在模板中使用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 %}

在这个If语句里,如果user.bio已经定义,就渲染{%if user.bio%}{%else%}之间的内容,否则就渲染{%else%}{%endif%}之间的默认内容。末尾的{%endif%}用来声明if语句的结束,这一行不能省略。

和在Python里一样,for语句用来迭代一个序列:

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

和其他语句一样,需要在for循环的结尾使用endfor标签声明for语句的结束。在for循环内,Jinja2提供了多个特殊变量,常用的循环变量,如下

变量名 说明
loop.index 当前迭代数(从1开始计算)
loop.index0 当前迭代数(从0开始计算)
loop.reindex 当前反向迭代数(从1开始计算)
loop.reindex0 当前反向迭代数(从0开始计算)
loop.first 如果是第一个元素,则为True
loop.last 如果是最后一个元素,则为True
loop.previtem 上一个迭代的条目
loop.nextitem 下一个迭代的条目
loop.length 序列包含的元素数量

渲染一个模板,就是执行模板中的代码,并传入所有在模板中使用的变量,渲染后的结果就是要返回给客户端的HTML响应。

在视图函数中渲染模板时,并不直接使用Jinja2提供的函数,而是使用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则是要传入的对象。

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

其他类型的变量通过相同的方式传入。传入Jinja2中的变量值可以是字符串、列表和字典,也可以是函数、类和类实例,这完全取决于在视图函数传入的值。

<p>这是列表my_list的第一个元素:{{ my_list[0] }}</p>
<p>这是元组my_tuple的第一个元素:{{ my_tuple[0] }}</p>
<p>这是字典my_dict的键为name的值:{{ my_dict['name'] }}</p>
<p>这是函数my_func的返回值:{{ my_func() }}</p>
<p>这是对象my_object调用某方法的返回值:{{ my_object.name() }}</p>

如果想传入函数在模板中调用,那么需要传入函数对象本身,而不是函数调用(函数的返回值),所以仅写出函数名称即可。当把函数传入模板后,可以像在Python脚本中一样通过添加括号的方式调用,而且可以在括号中传入参数。根据传入的虚拟数据,render_template()渲染后返回的HTML数据如下所示:

<!DOCTYPE html> 
html lang="en">
<head>
    <meta charset="utf-8">
    <title>Grey Li's Watchlist</title>
</head>
<body>
    <a href="/">&larr; 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>

在和渲染前的模板文件对比时你会发现,原模板中所有的Jinja2语句、表达式、注释都会在执行后被移除,而所有的变量都会被替换为对应的数据。访问http://localhost:5000/watchlist即可看到渲染后的页面

二、模板辅助工具

除了基本语法,Jinja2还提供了许多方便的工具,这些工具可以更方便地控制模板的输出。为了方便测试,在示例程序的templates目录下创建了一个根页面模板index.html。返回主页的index视图和watchlist视图类似:

from flask import render_template

@app.route('/')
def index():
    return render_template('index.html')

1.上下文

模板上下文包含了很多变量,其中包括调用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提供了一个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)

当然,上述代码也可以使用匿名函数来简化

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

2.全局对象

全局对象是指在所有的模板中都可以直接使用的对象,包括在模板中导入的模板

①内置全局函数

Jiaja2在模板中默认提供了一些全局函数,常用的三种函数如下

函数 说明
range([start, ]stop[, step]) 和python中的range用法相同
lipsum(n=5, html=True, min=20, max=100) 生成随机文本,可以在测试时用来填充页面。默认生成5段HTML文本,每段包含20-100个单词
dict(**items) 和python的dict用法相同

除了Jinja2内置的全局函数,Flask也在模板中内置了两个全局函数

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

Flask除了把gsessionconfigrequest对象注册为上下文变量,也将它们设为全局变量,因此可以全局使用。url_for()用来获取URL,用法和在Python脚本中相同。在前面给出的watchlist.html模板中,用来返回主页的链接直接写出。在实际的代码中,这个URL使用url_for()生成,传入index视图的端点:

<a href="{{ url_for('index') }}">&larr; Return</a>

②自定义全局变量

除了使用app.context_processor注册模板上下文处理函数来传入函数,也可以使用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(全局函数名称)

def nowTime():
    return datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")

app.add_template_global(nowTime, "NowTime")  # 将函数nowTime更名为NowTime,而模板中则需要用NowTime

3.过滤器

在Jinja2中,过滤器是一些可以用来修改和过滤变量值的特殊函数,过滤器和变量用一个竖线(管道符号)隔开,需要参数的过滤器可以像函数一样使用括号传递。

下面是一个对name变量使用title过滤器的例子:

{{ 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提供了许多内置过滤器,常用的过滤器如下所示。

过滤器 说明
default(value, default_value=u", 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=Flase, end='...', leeway=None) 截断字符串,常用于显示文章摘要,length参数设置截断的长度,killwords参数设置是否截断单词,end参数设置结尾的符号

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

另外,过滤器可以叠加使用,下面的示例为name变量设置默认值,并将其标题化:

<h1>Hello, {{ name|default('陌生人')|title }}!</h1>

默认的自动开启转义仅针对.html.htm.xml以及.xhtml后缀的文件,用于渲染模板字符串的render_template_string()函数也会对所有传入的字符串进行转义。

在确保变量值安全的情况下,这通常意味着对用户输入的内容进行了“消毒”处理。这时如果想避免转义,将变量作为HTML解析,可以对变量使用safe过滤器:

{{ sanitized_text|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攻击。

②自定义过滤器

如果内置的过滤器不能满足需要,还可以添加自定义过滤器。使用app.template_filter()装饰器可以注册自定义过滤器,如下注册了一个musical过滤器:

from flask import Markup
@app.template_filter()
def musical(s):
    return s + Markup('&#9835;')

和注册全局函数类似,可以在app.template_filter()中使用name关键字设置过滤器的名称,默认会使用函数名称。过滤器函数需要接收被处理的值作为输入,返回处理后的值。

过滤器函数接收s作为被过滤的变量值,返回处理后的值。创建的musical过滤器会在被过滤的变量字符后面添加一个音符(single bar note)图标,因为音符通过HTML实体表示,使用Markup类将它标记为安全字符。在使用时和其他过滤器用法相同:

{{ name|musical }}

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

4.测试器

在Jinja2中,测试器(Test)是一些用来测试变量或表达式,返回布尔值(True或False)的特殊函数。比如,number测试器用来判断一个变量或表达式是否是数字,使用is连接变量和测试器:

{% if age is number %}
	{{ age * 365 }}
{% else %}
	无效的数字。
{% endif %}

①内置测试器

Jinja2内置了许多测试器,常用的测试器及用法说明如下。

测试器 说明
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), 其他参数可以添加括号传入,也可以在右侧使用空格连接,以sameas为例:

{% if foo is sameas(bar) %}

等同于

{% if foo is sameas bar %}

②自定义测试器

和过滤器类似,我们可以使用Flask提供的app.template_test()装饰器来注册一个自定义测试器。

例如,创建一个测试器用来测试值是否为中国移动的手机号码

@app.template_test()
def is_phone(phone):
    pattern = "(13[4-9]\d{8})|(15[01289]\d{8})$"
    match = re.match(pattern, str(phone))
    if match == None:
        return False
    else:
        return True
字符串{{ phone }}
{% if phone is is_phone %}
是
{% else %}
不是
{% endif %}
中国移动的手机号码

测试器的名称默认为函数名称,可以在app.template_test()中使用name关键字指定自定义名称。测试器函数需要接收被测试的值作为输入,返回布尔值。

5.模板环境对象

在Jinja2中,渲染行为由jinja2.Enviroment类控制,所有的配置选项、上下文变量、全局函数、过滤器和测试器都存储在Enviroment实例上。

当与Flask结合后,并不单独创建Enviroment对象,而是使用Flask创建的Enviroment对象,它存储在pp.jinja_env属性上。

在程序中,可以使用app.jinja_env更改Jinja2设置。

比如,可以自定义所有的定界符。下面使用variable_start_stringvariable_end_string分别自定义变量定界符的开始和结束符号:

app = Flask(__name__)

app.jinja_env.variable_start_string = '[['
app.jinja_env.variable_end_string = ']]'

在实际开发中,如果修改Jinja2的定界符,那么需要注意与扩展提供模板的兼容问题,一般不建议修改。

模板环境中的全局函数、过滤器和测试器分别存储在Enviroment对象的globals、filters和tests属性中,这三个属性都是字典对象。除了使用Flask提供的装饰器和方法注册自定义函数,也可以直接操作这三个字典来添加相应的函数或变量,这通过向对应的字典属性中添加一个键值对实现,传入模板的名称作为键,对应的函数对象或变量作为值。下面是几个简单的示例。

①添加自定义全局对象

app.template_global()装饰器不同,直接操作globals字典允许传入任意Python对象,而不仅仅是函数,类似于上下文处理函数的作用。下面的代码使用app.jinja_env.globals分别向模板中添加全局函数全局变量

def bar():
    return "I am a Bar!"
foo = "This is a foo."

app.jinja_env.globals['bar'] = bar
app.jinja_env.globals['foo'] = foo

②添加自定义过滤器

下面的代码使用app.jinja_env.filters向模板中添加自定义过滤器:

def smiling(s):
    return s + ' :)'

app.jinja_env.filters['smiling'] = smiling

③添加自定义测试器

下面的代码使用app.jinja_env.tests向模板中添加自定义测试器

def is_phone(phone):
    pattern = "(13[4-9]\d{8})|(15[01289]\d{8})$"
    match = re.match(pattern, str(phone))
    if match == None:
        return False
    else:
        return True

app.jinja_env.tests["is_phone"] = is_phone

三、模板结构组织

除了使用函数、过滤器等工具控制模板的输出外,Jinja2还提供了一些工具来在宏观上组织模板内容。借助这些技术,可以更好地实践DRY原则。

1.局部模板

在Web程序中,通常会为每一类页面编写一个独立的模板。比如主页模板、用户资料页模板、设置页模板等。这些模板可以直接在视图函数中渲染并作为HTML响应主体。除了这类模板,还会用到另一类非独立模板,这类模板通常被称为局部模板次模板,因为它们仅包含部分代码,所以不会在视图函数中直接渲染它,而是插入到其他独立模板中。

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

当多个独立模板中都会使用同一块HTML代码时,就可以把这部分代码抽离出来,存储到局部模板中。这样一方面可以避免重复,另一方面也可以方便统一管理。比如,多个页面中都要在页面顶部显示一个提示条,这个横幅可以定义在局部模板_banner.html中。

使用include标签来插入一个局部模板,这会把局部模板的全部内容插在使用include标签的位置。比如,在其他模板中,可以在任意位置使用下面的代码插入_banner.html的内容:

{% include '_banner.html' %}

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

2.宏

宏是Jinja2提供的一个非常有用的特性,它类似Python中的函数。使用宏可以把一部分模板代码封装到宏里,使用传递的参数来构建内容,最后返回构建后的内容。在功能上,它和局部模板类似,都是为了方便代码块的重用。

为了便于管理,把宏存储在单独的文件中,这个文件通常命名为macros.html_macors.html。在创建宏时,使用macroendmacro标签声明宏的开始和结束。在开始标签中定义宏的名称和接收的参数。

{% macro addition(startNumber=1, endNumber=1) %}
	{{ startNumber }} + {{ endNumber }} = {{ startNumber + endNumber }}
{% endmacro %}

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

{% from 'macros.html' import qux %}
{{ addition(startNumber=5, endNumber=3) }}

另外,在使用宏时需要注意上下文问题。在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 %}

虽然Flask使用内置的模板上下文处理函数传入session、g、request和config,但它同时也使用app.jinja_env.globals字典将这几个变量设置为全局变量,所以仍然可以在不显示声明传入上下文的情况下,直接在导入的宏中使用它们。

3.模板继承

Jinja2的模板继承允许定义一个基模板,把网页上的导航栏、页脚等通用内容放在基模板中,而每一个继承基模板的子模板在被渲染时都会自动包含这些部分。使用这种方式可以避免在多个模板中编写重复的代码。

①编写基模板

基模板存储了程序页面的固定部分,通常被命名为base.htmllayout.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),在子模板中可以通过定义同名的块来执行继承操作。

块的开始和结束分别使用blockendblock标签声明,而且块之间可以嵌套。

在这个基模板中,创建了六个块:head、title、styles、content、footer和scripts,分别用来划分不同的代码。其中,head块表示<head>标签的内容,title表示<title>标签的内容,content块表示页面主体内容,footer表示页脚部分,styles块和scripts块,则分别用来包含CSS文件和JavaScript文件引用链接或页内的CSS和JavaScript代码。

这里的块名称可以随意指定,而且并不是必须的。可以按照需要设置块,如果只需要让子模板添加主体内容,那么仅定义一个content块就足够了。

以content块为例,模板继承示意图:

为了避免块的混乱,块的结束标签可以指明块名,同时要确保前后名称一致。比如:

{% block body %}
	...
{% endblock body %}

②编写子模板

因为基模板中定义了HTML的基本结构,而且包含了页脚等固定信息,在子模板中不再需要定义这些内容,只需要对特定的块进行修改。这时可以修改前面创建的电影清单模板index.html,将这些子模板的通用部分合并到基模板中,并在子模板中定义块来组织内容,以便在渲染时将块中的内容插入到基模板的对应位置。

{% extends 'base.html' %}
{% block content %}
{% from '_macro.html' import addition %}

This is Index Page.
<br>
{{ foo }}
<br>
{{ bar() }}
<br>
{{ NowTime() }}
<br>
{{ name | musical }}
<br>
字符串{{ phone }}
{% if phone is is_phone %}
是
{% else %}
不是
{% endif %}
中国移动的手机号码
<br>
{{ addition(startNumber=5, endNumber=3) }}

{% endblock content %}

使用extends标签声明扩展基模板,它告诉模板引擎当前模板派生自base.html。

**extends必须是子模板的第一个标签。 **

在基模板中定义了四个块,在子模板中,可以对父模板中的块执行两种操作:

1)覆盖内容

当在子模板里创建同名的块时,会使用子块的内容覆盖父块的内容。比如在子模板index.html中定义了title块,内容为Home,这会把块中的内容填充到基模板里的title块的位置,最终渲染为<title>Home</title>,content块的效果同理。

2)追加内容

如果想要向基模板中的块追加内容,需要使用Jinja2提供的super()函数进行声明,这会向父块添加内容。比如,下面的示例向基模板中的styles块追加了一行<style>样式定义:

{% block styles %}
    {{ super() }}
    <style>
    body {
        color: red;
    }
    </style>
{% endblock %}

当子模板被渲染时,它会继承基模板的所有内容,然后根据定义的块进行覆盖或追加操作,渲染子模板index.html的结果如下所示:

<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8">
        <title>
            Template - HelloFlask
        </title>
    <style>
    body {
        color: red;
    }
    </style>
    </head>
<body>
<nav>
    <ul>
        <li>
            <a href="/">Home</a>
        </li>
    </ul>
</nav>
<main>
This is Index Page.
<br>
I am Foo.
<br>
I am Bar.
<br>
2022-03-24 01:05:38
<br>
Mr Zhang&#9835;
<br>
字符串151172****是中国移动的手机号码
<br>
	5 + 3 = 8
</main>
<footer>
</footer>
</body>
</html>

四、模板进阶实践

介绍模板在Flask程序中的常见应用,其中主要包括加载静态文件自定义错误页面

1.空白控制

在实际输出的HTML文件中,模板中的Jinja2语句、表达式和注释会保留移除后的空行,前面为了节省篇幅手动删掉了这些空行。

{% if user.bio %}
	<i>{{ user.bio }}</i>
{% else %}
	<i>This user has not provided a bio.</i>
{% endif %}

Jinja2语句中的HTML代码缩进并不是必须的,只是为了增加可读性,在编写大量Jinja2代码时可读性尤其重要。

实际输出的HTML代码如下所示

<i>{{ user.bio }}</i>

<i>This user has not provided a bio.</i>

如果想在渲染时自动去掉这些空行,可以在定界符内侧添加减号。比如,{%- endfor%}会移除该语句前的空白,同理,在右边的定界符内侧添加减号将移除该语句后的空白:

{% if user.bio -%}
	<i>{{ user.bio }}</i>
{% else -%}
	<i>This user has not provided a bio.</i>
{%- endif %}

现在输出的HTML代码如下所示:

<i>{{ user.bio }}</i>
<i>This user has not provided a bio.</i>

除了在模板中使用减号来控制空白外,也可以使用模板环境对象提供的trim_blockslstrip_blocks属性设置,前者用来删除Jinja2语句后的第一个空行,后者则用来删除Jinja2语句所在行之前的空格和制表符(tabs):

app.jinja_env.trim_blocks = True
app.jinja_env.lstrip_blocks = True

需要注意的是,宏内的空白控制行为不受trim_blockslstrip_blocks属性控制,需要手动设置,比如:

{% macro qux(amount=1) %}
	{% if amount == 1 -%}
		I am qux.
	{% elif amount > 1 -%}
		We are quxs.
	{%- endif %}
{% endmacro %}

在实际的开发中,没有必要严格控制HTML输出,因为多余的空白并不影响浏览器的解析。在部署时,甚至可以使用工具来去除HTML响应中所有的空白、空行和换行,这样可以减小文件体积,提高数据传输速度。所以,编写模板时应以可读性为先,在后面的示例程序中,将不再添加空白控制的代码,并且对Jinja2语句中的HTML代码进行必要的缩进来增加可读性。

2.加载静态文件

一个Web项目不仅需要HTML模板,还需要许多静态文件,比如CSS、JavaScript文件、图片以及音频等。

在Flask程序中,默认需要将静态文件存储在与主脚本(包含程序实例的脚本)同级目录的static文件夹中。为了在HTML文件中引用静态文件,需要使用url_for()函数获取静态文件的URL。

Flask内置了用于获取静态文件的视图函数,端点值为static,它的默认URL规则为/static/<path:filename>,URL变量filename是相对于static文件夹根目录的文件路径。

如果要使用其他文件夹来存储静态文件,可以在实例化Flask类时使用static_folder参数指定,静态文件的URL路径中的static也会自动跟随文件夹名称变化。在实例化Flask类时使用static_url_path参数则可以自定义静态文件的URL路径。

在static目录下保存一个头像图片avatar.jpg,可以通过url_for('static',filename='avatar.jpg')获取这个文件的URL,这个函数调用生成的URL为/static/avatar.jpg,在浏览器中输入http://localhost:5000/static/avatar.jpg即可访问这个图片。

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

加载css文件

<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename= 'styles.css' ) }}"> 

①添加Favicon

在运行flask项目中,经常看到一条404状态的请求,请求的URL路径为/favicon.ico

127.0.0.1 - - [08/Feb/2021 18:31:12] "GET /favicon.ico HTTP/1.1" 404 -

这个favicon.ico文件指的是Favicon(favorite icon,收藏夹头像/网站头像),又称为shortcut icon、tab icon、website icon或是bookmarkicon。

顾名思义,这是一个在浏览器标签页、地址栏和书签收藏夹等处显示的小图标,作为网站的特殊标记。浏览器在发起请求时,会自动向根目录请求这个文件,在前面的示例程序中,因为没有提供这个文件,所以才会产生上面的404记录。

要想为Web项目添加Favicon,要先有一个Favicon文件,并放置到static目录下。它通常是一个宽高相同的ICO格式文件,命名为favicon.ico。

除了ICO格式,PNG和(无动画的)GIF格式也被所有主流浏览器支持。

Flask中静态文件的默认路径为/static/filename,为了正确返回Favicon,可以显式地在HTML页面中声明Favicon的路径。首先可以在<head>部分添加一个<link>元素,然后将rel属性设置为icon,如下所示:

<link rel="icon" type="image/x-icon" href="{{ url_for('static', filename='favicon.ico') }

大部分教程将rel属性设置为shortcut icon,事实上,shortcut是多余的,可以省略掉。

②使用CSS框架

在编写Web程序时,手动编写CSS比较麻烦,更常见的做法是使用CSS框架来为程序添加样式。CSS框架内置了大量可以直接使用的CSS样式类和JavaScript函数,使用它们可以非常快速地让程序页面变得美观和易用,同时也可以定义自己的CSS文件来进行补充和调整。以 Bootstrap(http://getbootstrap.com/)为例,需要访问Bootstrap的下载页面(http://getbootstrap.com/docs/4.0/getting-started/download/)下载相应的资源文件,然后分类别放到static目录下。

Bootstrap是最流行的开源前端框架之一,它有浏览器支持广泛、响应式设计等特点。使用它可以快速搭建美观、现代的网页。Bootstrap的官方文档(http://getbootstrap.com/docs/)提供了很多简单易懂的示例代码。

通常情况下,CSS和JavaScript的资源引用会在基模板中定义,具体方式和加载自定义的styles.css文件相同:

{% block styles %}
	<link rel="stylesheet" href="{{ url_for('static', filename='css/bootstrap.min.css') }}">
{% endblock %}
	...
{% block scripts %}
    <script src="{{ url_for('static', filename='js/jquery.min.js') }}"></script>
    <script src="{{ url_for('static', filename='js/popper.min.js') }}"></script>
    <script src="{{ url_for('static', filename='js/bootstrap.min.js') }}"></script>
{% endblock %}

如果不使用Bootstrap提供的JavaScript功能,那么也可以不加载。另外,Bootstrap所依赖的jQuery(https://jquery.com/)和 Popper.js(https://popper.js.org/)需要单独下载,这三个JavaScript文件在引入时要按照jQuery→Popper.js→Boostrap的顺序引入。

虽然建议在开发时统一管理静态资源,如果想简化开发过程,那么从CDN加载是更方便的做法。从CND加载时,只需要将相应的URL替换为CDN提供的资源URL,比如:

...

{% block styles %}
	<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css">
	{% endblock %}
		...
	{% block scripts %}
		<script src="https://code.jquery.com/jquery-3.2.1.slim.min.js"></script>
		<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.12.9/umd/popper.min.js"></script>
		<script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/js/bootstrap.min.js"></script>
	{% endblock %} ...

③使用宏加载静态资源

为了方便加载静态资源,可以创建一个专门用于加载静态资源的宏

{% 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 %}

在模板中导入宏后,只需在调用时传入静态资源的类别和文件路径就会获得完整的资源加载语句。使用它加载CSS文件的示例如下:

static_file('css', 'css/bootstrap.min.css')

使用它也可以从CDN加载资源,只需要将关键字参数local设为False,然后传入资源的URL即可:

static_file('css', 'https://maxcdn.../css/bootstrap.min.css', local=False)

3.消息闪现

Flask提供了一个非常有用的flash()函数,它可以用来“闪现”需要显示给用户的消息。

比如当用户登录成功后显示“欢迎回来!”。在视图函数调用flash()函数,传入消息内容即可“闪现”一条消息。当然,它并不是想象的能够立刻在用户的浏览器弹出一条消息。实际上,使用功能flash()函数发送的消息会存储在session中,需要在模板中使用全局函数get_flashed_messages()获取消息并将其显示出来。

通过flash()函数发送的消息会存储在session对象中,所以需要为程序设置密钥。可以通过app.secret_key属性或配置变量SECRET_KEY设置

可以在任意视图函数中调用flash()函数发送消息。为了测试消息闪现,添加了一个just_flash视图,在函数中发送了一条消息,最后重定向到index视图

from flask import Flask, render_template, flash

app = Flask(__name__)

app.secret_key = 'secret string'

@app.route('/flash')
def just_flash():
    flash('欢迎回来继续浏览!')
    return redirect(url_for('index'))

Jinja2内部使用Unicode,所以需要向模板传递Unicode对象或只包含ASCII字符的字符串。

Flask提供了get_flashed_message()函数用来在模板里获取消息,因为程序的每一个页面都有可能需要显示消息,把获取并显示消息的代码放在基模板中content块的上面,这样就可以在页面主体内容的上面显示消息

get_flashed_message()函数被调用时,session中存储的所有消息都会被移除。如果这时刷新页面,会发现重载后的页面不再出现这条消息。

4.自定义错误页面

当程序返回错误响应时,会渲染一个默认的错误页面。默认的错误页面太简单了,而且和其他页面的风格不符,导致用户看到这样的页面时往往会不知所措。可以注册错误处理函数来自定义错误页面。

错误处理函数和视图函数很相似,返回值将会作为响应的主体,因们首先要创建错误页面的模板文件。为了和普通模板区分开来,在模板文件夹templates里为错误页面创建了一个errors子文件夹,并在其中为最常见的404和500错误创建了模板文件。

{% 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异常的函数。

5.JavaScript和CSS中的Jinja2

当程序逐渐变大时,很多时候会需要在JavaScript和CSS代码中使用Jinja2提供的变量值,甚至是控制语句。比如,通过传入模板的theme_color变量来为页面设置主题色彩,或是根据用户是否登录来决定是否执行某个JavaScript函数。

首先要明白的是,只有使用render_templat()传入的模板文件才会被渲染,如果把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中获取的数据,如果是元素特定的数据,比如某个文章条目对应的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-username');使用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来编写客户端。

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); 
}
posted @ 2023-07-21 00:04  ChanceySolo  阅读(23)  评论(0编辑  收藏  举报