第 6 章:模板优化
这一章我们会继续完善模板,学习几个非常实用的模板编写技巧,为下一章实现创建、编辑电影条目打下基础。

自定义错误页面

为了引出相关知识点,我们首先要为 Watchlist 编写一个错误页面。目前的程序中,如果你访问一个不存在的 URL,比如 /hello,Flask 会自动返回一个 404 错误响应。默认的错误页面非常简陋,如下图所示:
默认的 404 错误页面
在 Flask 程序中自定义错误页面非常简单,我们先编写一个 404 错误页面模板,如下所示:
templates/404.html:404 错误页面模板
1
<!DOCTYPE html>
2
<html lang="en">
3
<head>
4
<meta charset="utf-8">
5
<title>{{ user.name }}'s Watchlist</title>
6
<link rel="icon" href="{{ url_for('static', filename='favicon.ico') }}">
7
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}" type="text/css">
8
</head>
9
<body>
10
<h2>
11
<img alt="Avatar" class="avatar" src="{{ url_for('static', filename='images/avatar.png') }}">
12
{{ user.name }}'s Watchlist
13
</h2>
14
<ul class="movie-list">
15
<li>
16
Page Not Found - 404
17
<span class="float-right">
18
<a href="{{ url_for('index') }}">Go Back</a>
19
</span>
20
</li>
21
</ul>
22
<footer>
23
<small>&copy; 2018 <a href="http://helloflask.com/tutorial">HelloFlask</a></small>
24
</footer>
25
</body>
26
</html>
Copied!
接着使用 app.errorhandler() 装饰器注册一个错误处理函数,它的作用和视图函数类似,当 404 错误发生时,这个函数会被触发,返回值会作为响应主体返回给客户端:
app.py:404 错误处理函数
1
@app.errorhandler(404) # 传入要处理的错误代码
2
def page_not_found(e): # 接受异常对象作为参数
3
user = User.query.first()
4
return render_template('404.html', user=user), 404 # 返回模板和状态码
Copied!
提示 和我们前面编写的视图函数相比,这个函数返回了状态码作为第二个参数,普通的视图函数之所以不用写出状态码,是因为默认会使用 200 状态码,表示成功。
这个视图返回渲染好的错误模板,因为模板中使用了 user 变量,这里也要一并传入。现在访问一个不存在的 URL,会显示我们自定义的错误页面:
自定义 404 错误页面
编写完这部分代码后,你会发现两个问题:
  • 错误页面和主页都需要使用 user 变量,所以在对应的处理函数里都要查询数据库并传入 user 变量。因为每一个页面都需要获取用户名显示在页面顶部,如果有更多的页面,那么每一个对应的视图函数都要重复传入这个变量。
  • 错误页面模板和主页模板有大量重复的代码,比如 <head> 标签的内容,页首的标题,页脚信息等。这种重复不仅带来不必要的工作量,而且会让修改变得更加麻烦。举例来说,如果页脚信息需要更新,那么每个页面都要一一进行修改。
显而易见,这两个问题有更优雅的处理方法,下面我们来一一了解。

模板上下文处理函数

对于多个模板内都需要使用的变量,我们可以使用 app.context_processor 装饰器注册一个模板上下文处理函数,如下所示:
app.py:模板上下文处理函数
1
@app.context_processor
2
def inject_user(): # 函数名可以随意修改
3
user = User.query.first()
4
return dict(user=user) # 需要返回字典,等同于 return {'user': user}
Copied!
这个函数返回的变量(以字典键值对的形式)将会统一注入到每一个模板的上下文环境中,因此可以直接在模板中使用。
现在我们可以删除 404 错误处理函数和主页视图函数中的 user 变量定义,并删除在 render_template() 函数里传入的关键字参数:
1
@app.context_processor
2
def inject_user():
3
user = User.query.first()
4
return dict(user=user)
5
6
7
@app.errorhandler(404)
8
def page_not_found(e):
9
return render_template('404.html'), 404
10
11
12
@app.route('/')
13
def index():
14
movies = Movie.query.all()
15
return render_template('index.html', movies=movies)
Copied!
同样的,后面我们创建的任意一个模板,都可以在模板中直接使用 user 变量。

使用模板继承组织模板

对于模板内容重复的问题,Jinja2 提供了模板继承的支持。这个机制和 Python 类继承非常类似:我们可以定义一个父模板,一般会称之为基模板(base template)。基模板中包含完整的 HTML 结构和导航栏、页首、页脚等通用部分。在子模板里,我们可以使用 extends 标签来声明继承自某个基模板。
基模板中需要在实际的子模板中追加或重写的部分则可以定义成块(block)。块使用 block 标签创建, `
` 作为结束标记。通过在子模板里定义一个同样名称的块,你可以向基模板的对应块位置追加或重写内容。

编写基础模板

下面是新编写的基模板 base.html:
templates/base.html:基模板
1
<!DOCTYPE html>
2
<html lang="en">
3
<head>
4
{% block head %}
5
<meta charset="utf-8">
6
<meta name="viewport" content="width=device-width, initial-scale=1.0">
7
<title>{{ user.name }}'s Watchlist</title>
8
<link rel="icon" href="{{ url_for('static', filename='favicon.ico') }}">
9
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}" type="text/css">
10
{% endblock %}
11
</head>
12
<body>
13
<h2>
14
<img alt="Avatar" class="avatar" src="{{ url_for('static', filename='images/avatar.png') }}">
15
{{ user.name }}'s Watchlist
16
</h2>
17
<nav>
18
<ul>
19
<li><a href="{{ url_for('index') }}">Home</a></li>
20
</ul>
21
</nav>
22
{% block content %}{% endblock %}
23
<footer>
24
<small>&copy; 2018 <a href="http://helloflask.com/tutorial">HelloFlask</a></small>
25
</footer>
26
</body>
27
</html>
Copied!
在基模板里,我们添加了两个块,一个是包含 <head></head> 内容的 head 块,另一个是用来在子模板中插入页面主体内容的 content 块。在复杂的项目里,你可以定义更多的块,方便在子模板中对基模板的各个部分插入内容。另外,块的名字没有特定要求,你可以自由修改。
在编写子模板之前,我们先来看一下基模板中的两处新变化。
第一处,我们添加了一个新的 <meta> 元素,这个元素会设置页面的视口,让页面根据设备的宽度来自动缩放页面,让移动设备拥有更好的浏览体验:
1
<meta name="viewport" content="width=device-width, initial-scale=1.0">
Copied!
第二处,新的页面添加了一个导航栏:
1
<nav>
2
<ul>
3
<li><a href="{{ url_for('index') }}">Home</a></li>
4
</ul>
5
</nav>
Copied!
导航栏对应的 CSS 代码如下所示:
1
nav ul {
2
list-style-type: none;
3
margin: 0;
4
padding: 0;
5
overflow: hidden;
6
background-color: #333;
7
}
8
9
nav li {
10
float: left;
11
}
12
13
nav li a {
14
display: block;
15
color: white;
16
text-align: center;
17
padding: 8px 12px;
18
text-decoration: none;
19
}
20
21
nav li a:hover {
22
background-color: #111;
23
}
Copied!

编写子模板

创建了基模板后,子模板的编写会变得非常简单。下面是新的主页模板(index.html):
templates/index.html:继承基模板的主页模板
1
{% extends 'base.html' %}
2
3
{% block content %}
4
<p>{{ movies|length }} Titles</p>
5
<ul class="movie-list">
6
{% for movie in movies %}
7
<li>{{ movie.title }} - {{ movie.year }}
8
<span class="float-right">
9
<a class="imdb" href="https://www.imdb.com/find?q={{ movie.title }}" target="_blank" title="Find this movie on IMDb">IMDb</a>
10
</span>
11
</li>
12
{% endfor %}
13
</ul>
14
<img alt="Walking Totoro" class="totoro" src="{{ url_for('static', filename='images/totoro.gif') }}" title="to~to~ro~">
15
{% endblock %}
Copied!
第一行使用 extends 标签声明扩展自模板 base.html,可以理解成“这个模板继承自 base.html“。接着我们定义了 content 块,这里的内容会插入到基模板中 content 块的位置。
提示 默认的块重写行为是覆盖,如果你想向父块里追加内容,可以在子块中使用 super() 声明,即 {{ super() }}
404 错误页面的模板类似,如下所示:
templates/404.html:继承基模板的 404 错误页面模板
1
{% extends 'base.html' %}
2
3
{% block content %}
4
<ul class="movie-list">
5
<li>
6
Page Not Found - 404
7
<span class="float-right">
8
<a href="{{ url_for('index') }}">Go Back</a>
9
</span>
10
</li>
11
</ul>
12
{% endblock %}
Copied!

添加 IMDb 链接

在主页模板里,我们还为每一个电影条目右侧添加了一个 IMDb 链接:
1
<span class="float-right">
2
<a class="imdb" href="https://www.imdb.com/find?q={{ movie.title }}" target="_blank" title="Find this movie on IMDb">IMDb</a>
3
</span>
Copied!
这个链接的 href 属性的值为 IMDb 搜索页面的 URL,搜索关键词通过查询参数 q 传入,这里传入了电影的标题。
对应的 CSS 定义如下所示:
1
.float-right {
2
float: right;
3
}
4
5
.imdb {
6
font-size: 12px;
7
font-weight: bold;
8
color: black;
9
text-decoration: none;
10
background: #F5C518;
11
border-radius: 5px;
12
padding: 3px 5px;
13
}
Copied!
现在,我们的程序主页如下所示:
添加导航栏和 IMDb 链接

本章小结

本章我们主要学习了 Jinja2 的模板继承机制,去掉了大量的重复代码,这让后续的模板编写工作变得更加轻松。结束前,让我们提交代码:
1
$ git add .
2
$ git commit -m "Add base template and error template"
3
$ git push
Copied!
提示 你可以在 GitHub 上查看本书示例程序的对应 commit:3bca489

进阶提示

  • 本章介绍的自定义错误页面是为了引出两个重要的知识点,因此并没有着重介绍错误页面本身。这里只为 404 错误编写了自定义错误页面,对于另外两个常见的错误 400 错误和 500 错误,你可以自己试着为它们编写错误处理函数和对应的模板。
  • 因为示例程序的语言和电影标题使用了英文,所以电影网站的搜索链接使用了 IMDb,对于中文,你可以使用豆瓣电影或时光网。以豆瓣电影为例,它的搜索链接为 https://movie.douban.com/subject_search?search_text=关键词,对应的 href 属性即 https://movie.douban.com/subject_search?search_text={{ movie.title }}
  • 因为基模板会被所有其他页面模板继承,如果你在基模板中使用了某个变量,那么这个变量也需要使用模板上下文处理函数注入到模板里。