基础篇 第三章 Flask模板
前言
在动态Web程序中,视图函数返回的HTML数据往往需要根据相应的变量(比如查询参数)动态生成。当HTML代码保存到单独的文件中时,我们没法再使用字符串格式化或拼接字符串的方式来在HTML代码中插入变量,这时我们需要使用模板引擎(template engine)。借助模板引擎,我们可以在HTML文件中使用特殊的语法来标记出变量,这类包含固定内容和动态部分的可重用文件称为模板(template)。
模板引擎的作用就是读取并执行模板中的特殊语法标记,并根据传入的数据将变量替换为实际值,输出最终的HTML页面,这个过程被称为渲染(rendering)。Flask默认使用的模板引擎是Jinja2,它是一个功能齐全的Python模板引擎,除了设置变量,还允许我们在模板中添加if判断,执行for迭代,调用函数等,以各种方式控制模板的输出。对于Jinja2来说,模板可以是任何格式的纯文本文件,比如HTML、XML、CSV、LaTeX等。在这一章,我们会学习Jinja2模板引擎的基本用法和一些常用技巧。
3. Flask模板
3.1 模板基本用法
使用Jinja2创建HTML模板,并在视图函数中渲染模板,最终实现HTML响应的动态化。
3.1.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'},
]
template/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 %}
{# Below is the movie list (this is comment) #}
<h5>{{ user.username }}'s Watchlist ({{ movies|length }}):</h5>
<ul>
{% for movie in movies %}
<li>{{ movie.name }} - {{ movie.year }}</li>
{% endfor %}
</ul>
</body>
</html>
这里创建了一个基础的HTML文档结构,关于HTML5的结构组成,可以访问https://www.w3.org/wiki/HTML_structural_elements了解.Jinja2里常见的三种定界符:
- (1) 语句
{% ... %}
- (2) 表达式
{{ ... }}
- (3) 注释
{# ... #}
Jinja2支持使用“.”获取变量的属性,比如user字典中的username键值通过“.”获取,即user.username
,在效果上等同于user['username']
。
3.1.2 模板语法
利用Jinja2这样的模板引擎,我们可以将一部分的程序逻辑放到模板中去。简单地说,我们可以在模板中使用Python语句和表达式来操作数据的输出。但需要注意的是,Jinja2并不支持所有Python语法。而且出于效率和代码组织等方面的考虑,我们应该适度使用模板,仅把和输出控制有关的逻辑操作放到模板中。
Jinja2允许你在模板中使用大部分Python对象,比如字符串、列表、字典、元组、整型、浮点型、布尔值。它支持基本的运算符号(+、-、*、/等)、比较符号(比如==、!=等)、逻辑符号(and、or、not和括号)以及in、is、None和布尔值(True、False)。
在Jinja2里,语句使用{%...%}
标识,尤其需要注意的是,在语句结束的地方,我们必须添加结束标签:
常用的Jinja2 for循环特殊变量:
完整的for循环变量列表请访问http://jinja.pocoo.org/docs/2.10/templates/#for查看。
3.1.3 渲染模板
渲染一个模板,就是执行模板中的代码,并传入所有在模板中使用的变量,渲染后的结果就是我们要返回给客户端的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)
首先传入模板的文件名作为参数。以,关键字参数的形式传入了模板中使用的变量值,以user为例:左边的user表示传入模板的变量名称,右边的user则是要传入的对象。
其他类型的变量通过相同的方式传入。传入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>
3.2 模板辅助工具
除了基本语法,Jinja2还提供了许多方便的工具,这些工具可以让你更方便地控制模板的输出。为了方便测试,我们在示例程序的templates目录下创建了一个根页面模板index.html。返回主页的index视图和watchlist视图类似.
from flask import render_template
@app.route('/')
def index():
return render_template('index.html')
3.2.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 %}
1.内置上下文变量
Flask在模板上下文中提供了一些内置变量,可以在模板中直接使用
2.自定义上下文
如果多个模板都需要使用同一变量,那么比起在多个视图函数中重复传入,更好的方法是能够设置一个模板全局变量。Flask提供了一个
app.context_processor
装饰器,可以用来注册模板上下文处理函数,它可以帮我们完成统一传入变量的工作。模板上下文处理函数需要返回一个包含变量键值对的字典
@app.context_processor
def inject_foo():
foo = 'I am foo.'
return dict(foo=foo) # 等同于return {'foo': foo}
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.'))
3.2.2 全局对象
全局对象是指在所有的模板中都可以直接使用的对象,包括在模板中导入的模板,后面我们会详细介绍导入的概念。
1.内置全局函数
这里只列出了部分常用的全局函数,完整的全局函数列表请访问
http://jinja.pocoo.org/docs/2.10/templates/#list-of-global-functions查看。
Flask 中内置全局函数:
Flask除了把g、session、config、request对象注册为上下文变量,也将它们设为全局变量,因此可以全局使用。
<a href="{{ url_for('index') }}">← Return</a>
2.自定义全局函数
@app.template_global()
def bar():
return 'I am bar.'
app.add_template_global(your_global_function) # 另一种方式
3.2.3 过滤器
过滤器(filter)是一些可以用来修改和过滤变量值的特殊函数,过滤器和变量用一个竖线(管道符号)隔开,需要参数的过滤器可以像函数一样使用括号传递。下面是一个对name变量使用title过滤器的例子:
{{ name|title }}
{{ movies|length }}
{% filter upper %}
This text becomes uppercase.
{% endfilter %}
1.内置过滤器
Jinja2提供了许多内置过滤器,常用的过滤器如下:
这里只列出了一部分常用的过滤器,完整的列表请访问http://jinja.pocoo.org/docs/2.10/templates/#builtin-filters查看.
<h1>Hello, {{ name|default('陌生人')|title }}!</h1>
一种将文本标记为安全的方法是在渲染前将变量转换为Markup对象:
from flask import Markup
@app.route('/hello')
def hello():
text = Markup('<h1>Hello, Flask!</h1>')
return render_template('index.html', text=text)
2.自定义过滤器
如果内置的过滤器不能满足你的需要,还可以添加自定义过滤器。使用app.template_filter()装饰器可以注册自定义过滤器,代码清单3-5注册了一个musical过滤器。
from flask import Markup
@app.template_filter()
def musical(s):
return s + Markup(' ♫')
因为音符通过HTML实体♫
;表示,我们使用Markup类将它标记为安全字符。在使用时和其他过滤器用法相同:
{{ name|musical }}
3.2.4 测试器
在Jinja2中,测试器(Test)是一些用来测试变量或表达式,返回布尔值(True或False)的特殊函数。比如,number测试器用来判断一个变量或表达式是否是数字,我们使用is连接变量和测试器:
{% if age is number %}
{{ age * 365 }}
{% else %}
无效的数字。
{% endif %}
1.内置测试器
Jinja2内置了许多测试器,常用的测试器及用法说明如下表:
这里只列出了一部分常用的测试器,完整的内置测试器列表请访问
http://jinja.pocoo.org/docs/2.10/templates/#list-of-builtin-tests查看。
{% if foo is sameas(bar) %}...
{% if foo is sameas bar %}...
2.自定义测试器
和过滤器类似,我们可以使用Flask提供的app.template_test()装饰器来注册一个自定义测试器。在示例程序中,我们创建了一个没有意义的baz过滤器,仅用来验证被测值是否为baz:
@app.template_test()
def baz(n):
if n == 'baz':
return True
return False
app.add_template_test(your_test_function)
3.2.5 模板环境对象
在Jinja2中,渲染行为由jinja2.Enviroment
类控制,所有的配置选项、上下文变量、全局函数、过滤器和测试器都存储在Enviroment实例上。当与Flask结合后,我们并不单独创建Enviroment对象,而是使用Flask创建的Enviroment对象,它存储在app.jinja_env
属性上。
1.添加自定义全局对象
和app.template_global()装饰器不同,直接操作globals字典允许我们传入任意Python对象,而不仅仅是函数,类似于上下文处理函数的作用。下面的代码使用app.jinja_env.globals分别向模板中添加全局函数bar和全局变量foo:
def bar():
return 'I am bar.'
foo = 'I am foo.'
app.jinja_env.globals['bar'] = bar
app.jinja_env.globals['foo'] = foo
2.添加自定义过滤器
下面的代码使用app.jinja_env.filters
向模板中添加自定义过滤器smiling
:
def smiling(s):
return s + ' :)'
app.jinja_env.filters['smiling'] = smiling
3.添加自定义测试器
下面的代码使用app.jinja_env.tests
向模板中添加自定义测试器baz
:
def baz(n):
if n == 'baz':
return True
return False
app.jinja_env.tests['baz'] = baz
访问http://jinja.pocoo.org/docs/latest/api/#jinja2.Environment查看Enviroment类的所有属性及用法说明。
3.3 模板结构组织
除了使用函数、过滤器等工具控制模板的输出外,Jinja2还提供了一些工具来在宏观上组织模板内容。借助这些技术,我们可以更好地实践DRY(Don’t Repeat Yourself)原则。
3.3.1 局部模板
在Web程序中,我们通常会为每一类页面编写一个独立的模板。比如主页模板、用户资料页模板、设置页模板等。这些模板可以直接在视图函数中渲染并作为HTML响应主体。除了这类模板,我们还会用到另一类非独立模板,这类模板通常被称为局部模板或次模板,因为它们仅包含部分代码,所以我们不会在视图函数中直接渲染它,而是插入到其他独立模板中。
当程序中的某个视图用来处理AJAX请求时,返回的数据不需要包含完整的HTML结构,这时就可以返回渲染后的局部模板。
]
我们使用include标签来插入一个局部模板,这会把局部模板的全部内容插在使用include标签的位置。比如,在其他模板中,我们可以在任意位置使用下面的代码插入_banner.html的内容:
{% include '_banner.html' %}
为了和普通模板区分开,局部模板的命名通常以一个下划线开始。
3.3.2 宏
宏(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) }}
关于宏的编写,更多的细节请访问http://jinja.pocoo.org/docs/latest/templates/#macros查看。
3.3.3 模板继承
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>
<link rel="icon" type="image/x-icon" href="{{ url_for('static', filename='favicon.ico') }}">
{% block styles %}
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='style.css' ) }}">
{% endblock %}
{% endblock %}
</head>
<body>
<nav>
<ul><li><a href="{{ url_for('index') }}">Home</a></li></ul>
</nav>
<main>
{% for message in get_flashed_messages() %}
<div class="alert">{{ message }}</div>
{% endfor %}
{% block content %}{% endblock %}
</main>
<footer>
{% block footer %}
<small> © 2018 <a href="http://greyli.com" title="Written by Grey Li">Grey Li</a> /
<a href="https://github.com/greyli/helloflask" title="Fork me on GitHub">GitHub</a> /
<a href="http://helloflask.com" title="A HelloFlask project">HelloFlask</a>
</small>
{% 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代码。
这里的块名称可以随意指定,而且并不是必须的。你可以按照需要设置块,如果你只需要让子模板添加主体内容,那么仅定义一个content块就足够了。
以content块为例:
为了避免块的混乱,块的结束标签可以指明块名,同时要确保前后名称一致。比如:
{% block body %}
...
{% endblock body %}
2.编写子模板
因为基模板中定义了HTML的基本结构,而且包含了页脚等固定信息,在子模板中我们不再需要定义这些内容,只需要对特定的块进行修改。这时我们可以修改前面创建的电影清单模板watchlist.html
和主页模板index.html
,将这些子模板的通用部分合并到基模板中,并在子模板中定义块来组织内容,以便在渲染时将块中的内容插入到基模板的对应位置。以index.html
为例: template/templates/index.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=1) }}</li>
<li><a href="{{ url_for('watchlist_with_static') }}">Watchlist with image and styles.</a></li>
<li><a href="{{ url_for('just_flash') }}">Flash something</a></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 %}
当子模板被渲染时,它会继承基模板的所有内容,然后根据我们定义的块进行覆盖或追加操作,渲染子模板index.html的结果如下所示:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Template - HelloFlask</title>
<link rel="icon" type="image/x-icon" href="/static/favicon.ico">
<link rel="stylesheet" type="text/css" href="/static/style.css">
</head>
<body>
<nav>
<ul><li><a href="/">Home</a></li></ul>
</nav>
<main>
<h1>Template</h1>
<ul>
<li><a href="/watchlist">Watchlist</a></li>
<li>Filter: I am foo. ♫</li>
<li>Global: I am bar.</li>
<li>Test: I am baz.</li>
<li>Macro: I am qux.</li>
<li><a href="/watchlist2">Watchlist with image and styles.</a></li>
<li><a href="/flash">Flash something</a></li>
</ul>
</main>
<footer>
<small> © 2018 <a href="http://greyli.com" title="Written by Grey Li">Grey Li</a> /
<a href="https://github.com/greyli/helloflask" title="Fork me on GitHub">GitHub</a> /
<a href="http://helloflask.com" title="A HelloFlask project">HelloFlask</a>
</small>
</footer>
</body>
</html>
3.4 模板进阶实战
介绍模板在Flask程序中的常见应用,其中主要包括加载静态文件和自定义错误页面。
3.4.1 空白控制
如果想在渲染时自动去掉这些空行,可以在定界符内侧添加减号。比如,{%-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>
你可以访问http://jinja.pocoo.org/docs/latest/templates/#whitespacecontrol查看更多细节。
除了在模板中使用减号来控制空白外,我们也可以使用模板环境对象提供的trim_blocks和lstrip_blocks属性设置,前者用来删除Jinja2语句后的第一个空行,后者则用来删除Jinja2语句所在行之前的空格和制表符(tabs):
app.jinja_env.trim_blocks = True
app.jinja_env.lstrip_blocks = True
需要注意的是,宏内的空白控制行为不受trim_blocks和lstrip_blocks属性控制,我们需要手动设置,比如
{% macro qux(amount=1) %}
{% if amount == 1 -%}
I am qux.
{% elif amount > 1 -%}
We are quxs.
{%- endif %}
{% endmacro %}
事实上,我们没有必要严格控制HTML输出,因为多余的空白并不影响浏览器的解析。在部署时,我们甚至可以使用工具来去除HTML响应中所有的空白、空行和换行,这样可以减小文件体积,提高数据传输速度。所以,编写模板时应以可读性为先,在后面的示例程序中,我们将不再添加空白控制的代码,并且对Jinja2语句中的HTML代码进行必要的缩进来增加可读性。
3.4.2 加载静态文件
一个Web项目不仅需要HTML模板,还需要许多静态文件,比如CSS、JavaScript文件、图片以及音频等。在Flask程序中,默认我们需要将静态文件存储在与主脚本(包含程序实例的脚本)同级目录的static文件夹中。
为了在HTML文件中引用静态文件,我们需要使用url_for()函数获取静态文件的URL。Flask内置了用于获取静态文件的视图函数,端点值为static,它的默认URL规则为/static/<path:filename>
,URL变量filename是相对于static文件夹根目录的文件路径。
在示例程序的static目录下保存了一个头像图片avatar.jpg,我们可以通过url_for(‘static’,filename=‘avatar.jpg’)获取这个文件的URL,这个函数调用生成的URL为/static/avatar.jpg,在浏览器中输
入http://localhost:5000/static/avatar.jpg即可访问这个图片。在模板
watchlist2.html里,我们在用户名的左侧添加了这个图片,使用url_for()函数生成图片src属性所需的图片URL,如下所示:
<img src="{{ url_for('static', filename='avatar.jpg') }}" width="50">
另外,还可以创建了一个存储CSS规则的styles.css文件,我们使用下面的方式在模板中加载这个文件:
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='style.css' ) }}">
在浏览器中访问http://localhost:5000/watchlist2可以看到添加了头像图片并加载了CSS规则的电影清单页面:
1.添加Favicon
Flask中静态文件的默认路径为/static/filename,为了正确返回Favicon,我们可以显式地在HTML页面中声明Favicon的路径。首先可以在<head>
部分添加一个元素,然后将rel属性设置为icon:
<link rel="icon" type="image/x-icon" href="{{ url_for('static', filename='favicon.ico') }}">
2.使用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" type="text/css" href="{{ url_for('static', filename='style.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.{% 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 %}
...
3.使用宏加载静态资源
为了方便加载静态资源,我们可以创建一个专门用于加载静态资的宏, template/templates/macros.html:用于加载静态资源的宏:
{% 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" type="image/x-icon" href="{{ filename_or_url }}" }}">
{%- endif %}
{% endmacro %}
在模板中导入宏后,只需在调用时传入静态资源的类别和文件路径就会获得完整的资源加载语句。在template/templates/base.html
中,使用它加载CSS和js文件的示例如下:
{% from 'macros.html' import static_file %}
<head>
{% block head %}
<meta charset="utf-8">
<title>{% block title %}Template - HelloFlask{% endblock %}</title>
{{ static_file('icon', 'favicon.ico') }}
{% block styles %}
{{ static_file('css', 'style.css') }}
{% endblock %}
{% endblock %}
</head>
...
{% block scripts %}
{{ static_file('js','js/jquery.min.js') }}
{{ static_file('js','js/popper.min.js') }}
{{ static_file('js','js/bootstrap.min.js') }}
{% endblock %}
...
使用它也可以从CDN加载资源,只需要将关键字参数local设为False,然后传入资源的URL即可:
{{ static_file('css', 'https://maxcdn.../css/bootstrap.min.css', local=False) }}
3.4.3 消息闪现
Flask提供了一个非常有用的flash()函数,它可以用来“闪现”需要显示给用户的消息,比如当用户登录成功后显示“欢迎回来!”。在视图函数调用flash()函数,传入消息内容即可“闪现”一条消息。当然,它并不是我们想象的,能够立刻在用户的浏览器弹出一条消息。实际上,使用功能flash()函数发送的消息会存储在session中,我们需要在模板中使用全局函数get_flashed_messages()
获取消息并将其显示出来, 该函数用来在模板里获取消息,因为程序的每一个页面都有可能需要显示消息,我们把获取并显示消息的代码放在基模板中content块的上面,这样就可以在页面主体内容的上面显示消息:
<main>
{% for message in get_flashed_messages() %}
<div class="alert">{{ message }}</div>
{% endfor %}
{% block content %}{% endblock %}
</main>
因为同一个页面可能包含多条要显示的消息,所以这里使用for循环迭代get_flashed_message()
返回的消息列表。另外,我们还为消息定义了一些CSS规则,你可以在示例程序中的static/styles.css
文件中查看。现在访问http://localhost:5000
打开示例程序的主页,如果你单击页面上的Flash something链接(指向/flash),页面重载后就会显示一条消息:
当get_flashed_message()
函数被调用时,session中存储的所有消息都会被移除。如果你这时刷新页面,会发现重载后的页面不再出现这条消息。
3.4.4 自定义错误页面
当程序返回错误响应时,会渲染一个默认的错误页面,默认的错误页面太简单了,我们可以注册错误处理函数来自定义错误页面。
我们首先要创建错误页面的模板文件。为了和普通模板区分开来,我们在模板文件夹templates里为错误页面创建了一个errors子文件夹,并在其中为最常见的404和500错误创建了模板文件template/templates/errors/404.html
:404页面模板如下:
{% 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
这时如果访问一个错误的URL(即未在程序中定义的URL),比如http://localhost:5000/nothing:
3.4.5 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代码。
避免把大量JavaScript代码留在HTML中的办法就是尽量将要使用的Jinja2变量值在HTML模板中定义为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')
。
在HTML中,“data-*”被称为自定义数据属性(custom dataattribute),我们可以用它来存储自定义的数据供JavaScript获取。在后面的其他程序中,我们也会频繁使用这种方式来传递数据。
对于需要全局使用的数据,则可以在页面中使用嵌入式JavaScript定义变量,如果没法定义为JavaScript变量,那就考虑定义为函数,比如:
<script type="text/javascript">
var foo = '{{ foo_variable }}';
</script>
当你在JavaScript中插入了太多Jinja2语法时,或许这时你该考虑将程序转变为Web API,然后专心使用JavaScript来编写客户端,在本书的第二部分我们会介绍如何编写Web API。
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);
}
3.5 本章代码
# -*- coding: utf-8 -*-
"""
:author: Grey Li (李辉)
:url: http://greyli.com
:copyright: © 2018 Grey Li
:license: MIT, see LICENSE for more details.
"""
import os
from flask import Flask, render_template, flash, redirect, url_for, Markup
app = Flask(__name__)
app.secret_key = os.getenv('SECRET_KEY', 'secret string')
app.jinja_env.trim_blocks = True
app.jinja_env.lstrip_blocks = True
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('/')
def index():
return render_template('index.html')
# register template context handler
@app.context_processor
def inject_info():
foo = 'I am foo.'
return dict(foo=foo) # equal to: return {'foo': foo}
# register template global function
@app.template_global()
def bar():
return 'I am bar.'
# reigster template filter
@app.template_filter()
def musical(s):
return s + Markup(' ♫')
# register template test
@app.template_test()
def baz(n):
if n == 'baz':
return True
return False
@app.route('/watchlist2')
def watchlist_with_static():
return render_template('watchlist_with_static.html', user=user, movies=movies)
# message flashing
@app.route('/flash')
def just_flash():
flash('I am flash, who is looking for me?')
return redirect(url_for('index'))
# 404 error handler
@app.errorhandler(404)
def page_not_found(e):
return render_template('errors/404.html'), 404
# 500 error handler
@app.errorhandler(500)
def internal_server_error(e):
return render_template('errors/500.html'), 500