模板结构组织
除了使用函数、过滤器等工具控制模板的输出外,Jinja2还提供了一些工具来在宏观上组织模板内容。
局部模板
当程序中的某个视图用来处理AJAX请求时,返回的数据不需要包含完整的HTML结构,这时就可以返回渲染后的局部模板。
当多个独立模板中都会使用同一块HTML代码时,我们可以把这部分代码抽离出来,存储到局部模板中。这样一方面可以避免重复,另一方面也可以方便统一管理。比如,多个页面中都要在页面顶部显示一个提示条,这个横幅可以定义在局部模板_banner.html中。
我们使用include标签来插入一个局部模板,这会把局部模板的全部内容插在使用include标签的位置。比如,在其他模板中,我们可以在任意位置使用下面的代码插入_banner.html的内容
为了和普通模板区分开,局部模板的命名通常以一个下划线开始。
{% include '_banner.html' %}
宏
宏(macro)是Jinja2提供的一个非常有用的特性,它类似Python中的函数。使用宏可以把一部分模板代码封装到宏里,使用传递的参数来构建内容,最后返回构建后的内容。在功能上,它和局部模板类似,都是为了方便代码块的重用。
为了便于管理,我们可以把宏存储在单独的文件中,这个文件通常命名为macros.html或_macors.html。在创建宏时,我们使用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使用内置的模板上下文处理函数提供的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的模板继承允许你定义一个基模板,把网页上的导航栏、页脚等通用内容放在基模板中,而每一个继承基模板的子模板在被渲染时都会自动包含这些部分。使用这种方式可以避免在多个模板中编写重复的代码。
1.编写基模板
基模板存储了程序页面的固定部分,通常被命名为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代码。
为了避免块的混乱,块的结束标签可以指明块名,同时要确保前后名称一致。
比如:
{% block body %}
...
{% endblock body %}
编写子模板
我们使用extends标签声明扩展基模板,它告诉模板引擎当前模板派生自base.html。extends必须是子模板的第一个标签。
{% 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 %}
我们在基模板中定义了四个块,在子模板中,我们可以对父模板中的块执行两种操作:
(1)覆盖内容
当在子模板里创建同名的块时,会使用子块的内容覆盖父块的内容。比如我们在子模板index.html中定义了title块,内容为Home,这会把块中的内容填充到基模板里的title块的位置,最终渲染为<title>Home</title>,content块的效果同理。
(2)追加内容
如果想要向基模板中的块追加内容,需要使用Jinja2提供的super()函数进行声明,这会向父块添加内容。比如,下面的示例向基模板中的styles块追加了一行<style>样式定义:
{% block styles %}
{{ super() }}
<style>
.foo {
color: red;
}
</style>
{% endblock %}
模板进阶实践
空白控制
在实际输出的HTML文件中,模板中的Jinja2语句、表达式和注释会保留移除后的空行。
Jinja2语句中的HTML代码缩进并不是必须的,只是为了增加可读性,在编写大量Jinja2代码时可读性尤其重要。
示例:
{% 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>
如果想在渲染时自动去掉这些空行,解决方法有
1.可以在定界符内侧添加减号
比如,{%-endfor%}会移除该语句前的空白,同理,在右边的定界符内侧添加减号将移除该语句后的空白
2.以使用模板环境对象提供的trim_blocks和lstrip_blocks属性设置,前者用来删除Jinja2语句后的第一个空行,后者则用来删除Jinja2语句所在行之前的空格和制表符(tabs)
app.jinja_env.trim_blocks = True
app.jinja_env.lstrip_blocks = True
trim_blocks中的block指的是使用{%...%}定界符的代码块,与我们前面介绍模板继承中的块无关。
需要注意的是,宏内的空白控制行为不受trim_blocks和lstrip_blocks属性控制,我们需要手动设置,比如:
{% macro qux(amount=1) %}
{% if amount == 1 -%}
I am qux.
{% elif amount > 1 -%}
We are quxs.
{%- endif %}
{% endmacro %}
事实上,我们没有必要严格控制HTML输出,因为多余的空白并不影响浏览器的解析。在部署时,我们甚至可以使用工具来去除HTML响应中所有的空白、空行和换行,这样可以减小文件体积,提高数据传输速度。
加载静态文件
一个Web项目不仅需要HTML模板,还需要许多静态文件,比如CSS、JavaScript文件、图片以及音频等。在Flask程序中,默认我们需要将静态文件存储在与主脚本(包含程序实例的脚本)同级目录的static文件夹中。
为了在HTML文件中引用静态文件,我们需要使用url_for()函数获取静态文件的URL。Flask内置了用于获取静态文件的视图函数,端点值为static,它的默认URL规则为/static/<path:filename>,URL变量filename是相对于static文件夹根目录的文件路径。
<img src="{{ url_for('static', filename='avatar.jpg') }}" width="50">
添加Favicon
favicon.ico文件指的是Favicon(favorite icon,收藏夹头像/网站头像),浏览器在发起请求时,会自动向根目录请求这个文件。
要想为Web项目添加Favicon,你要先有一个Favicon文件,并放置到static目录下。它通常是一个宽高相同的ICO格式文件,命名为favicon.ico。
<link rel="icon" type="image/x-icon" href="{{ url_for('static', filename='favicon.ico') }}">
使用CSS框架
在编写Web程序时,手动编写CSS比较麻烦,更常见的做法是使用CSS框架来为程序添加样式。
CSS框架内置了大量可以直接使用的CSS样式类和JavaScript函数,使用它们可以非常快速地让程序页面变得美观和易用,同时我们也可以定义自己的CSS文件来进行补充和调整。
Bootstrap是最流行的开源前端框架之一,它有浏览器支持广泛、响应式设计等特点。使用它可以快速搭建美观、现代的网页。
通常情况下,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 %}
...
这三个JavaScript文件在引入时要按照jQuery→Popper.js→Boostrap的顺序引入。
从CDN加载是更方便的做法
...
{% 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)
消息闪现
Flask提供了一个非常有用的flash()函数,它可以用来“闪现”需要显示给用户的消息,比如当用户登录成功后显示“欢迎回来!”
使用功能flash()函数发送的消息会存储在session中,我们需要在模板中使用全局函数get_flashed_messages()获取消息并将其显示出来。
通过flash()函数发送的消息会存储在session对象中,所以我们需要为程序设置密钥。可以通过app.secret_key属性或配置变量SECRET_KEY设置
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'))
Jinja2内部使用Unicode,所以你需要向模板传递Unicode对象或只包含ASCII字符的字符串。
Flask提供了get_flashed_message()函数用来在模板里获取消息,因为程序的每一个页面都有可能需要显示消息,我们把获取并显示消息的代码放在基模板中content块的上面,这样就可以在页面主体内容的上面显示消息
<main>
{% for message in get_flashed_messages() %}
<div class="alert">{{ message }}</div>
{% endfor %}
{% block content %}{% endblock %}
</main>
当get_flashed_message()函数被调用时,session中存储的所有消息都会被移除。如果你这时刷新页面,会发现重载后的页面不再出现这条消息。
自定义错误页面
错误处理函数和视图函数很相似,返回值将会作为响应的主体,因此我们首先要创建错误页面的模板文件。
{% 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
JavaScript和CSS中的Jinja2
当程序逐渐变大时,很多时候我们会需要在JavaScript和CSS代码中使用Jinja2提供的变量值,甚至是控制语句。比如,通过传入模板的theme_color变量来为页面设置主题色彩,或是根据用户是否登录来决定是否执行某个JavaScript函数。
首先要明白的是,只有使用render_template()传入的模板文件才会被渲染,如果你把Jinja2代码写在单独的JavaScript或是CSS文件中,尽管你在HTML中引入了它们,但它们包含的Jinja2代码永远也不会被执行。对于这类情况,下面有一些Tips:
1.行内/嵌入式JavaScript/CSS
如果要在JavaScript和CSS文件中使用Jinja2代码,那么就在HTML中使用<style>和<script>标签定义这部分CSS和JavaScript代码。
(不推荐,不利于维护)
2.定义为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')。