在 Flask Mega-Tutorial 系列的第二部分,我将介绍如何使用模板(templates)。
在完成了 第一章 之后,你应该完成了一个虽简单但能正常工作的应用程序,其结构如下:
microblog\
venv\
app\
__init__.py
routes.py
microblog.py
要运行此程序,你需要先在终端设定FLASK_APP=microblog.py
,然后执行 flask run
。这就通过服务器启动了应用程序,然后你可以在浏览器地址栏里输入 http://localhost:5000/ 这一URL来查看其响应。
本章,你将继续改进这个程序。特别是,你将学习如何生成更多页面,使其具有精心组织的结构和动态部件。如果你有任何关于程序或开发流程方面的疑惑,请在开始之前回一次炉^_^ 点我复习 第一章 。
啥叫模板?
我希望我的微博程序的首页上有个欢迎用户的天顶区。 现在,先忽略我们还没有神马用户的概念,这个将来我们才会谈到。作为替代,我使用一个 mock 用户( 模拟数据,虚假数据
),用Python字典的形式实现,如下:
user = {'username': 'Miguel'}
创建虚假对象是个很有用的技术,这可以允许你无需操心程序尚未实现的其他部分而只专心于一部分程序。我希望设计好程序的首页,但不希望目前还没有用户系统的事实分散我的注意力,因此我就虚构一个用户对象来保证继续开发。
目前视图函数只返回了一个简单的字符串。我希望做的就是把这个字符串扩展成一个完整的HTML页面,那么或许应该这样:
app/routes.py: 从视图函数返回HTML
from app import app
@app.route('/')
@app.route('/index')
def index():
user = {'username': 'Miguel'}
return '''
<html>
<head>
<title>Home Page - Microblog</title>
</head>
<body>
<h1>Hello, ''' + user['username'] + '''!</h1>
</body>
</html>'''
如果你不熟悉HTML,我建议你去读一下维基百科上关于它的简介吧 HTML Markup 。
更新你的视图函数如上,然后刷新浏览器查看最新的效果。
我希望你应该也认同:上面这种直接输出HTML给浏览器的方式很糟糕。考虑一下,当我们要处理来自用户各种多变的内容发布时,这里的视图函数的代码将会多么的复杂。 而程序本身也会拥有越来越多的视图函数,关联各种URLs,假设有一天我要更改这个程序的布局,那就不得不去修改每个视图函数代码。很明显,这并不是个能随程序发展而自如调整的好选项。
若你能坚持把你的程序逻辑与页面布局分离开,那么就会好组织的多,你觉得呢?你甚至可以雇佣一个web设计师创建个杀手级别的酷站,自己仅仅使用Python编辑应用程序的逻辑处理部分。
模板帮助你实现了布局和商业逻辑分离。在Flask中,模板被单独写在文件中,存储在应用包所在文件夹的templates 子文件夹下。因此,要确认你处在 microblog 文件夹中,然后创建存储模板的这个文件夹:
(venv) $ mkdir app/templates
接下来,我们要创建爱你地一个模板,在功能上与上面index()
函数返回的HTML页面类似。在 app/templates/index.html中编写代码:
app/templates/index.html: 主页模板
<html>
<head>
<title>{{ title }} - Microblog</title>
</head>
<body>
<h1>Hello, {{ user.username }}!</h1>
</body>
</html>
这几乎是标准的,非常简洁的HTML页面。唯一有趣的东东就是页面当中有两个动态内容占位符,就是被 {{ ... }}
包括起来的那部分。这些占位符代表页面中不确定的内容,只有在运行时才会明确下来。
现在,页面描述已经与卸载到HTML模板中了,视图函数就简单多了:
app/routes.py: 使用 render_template() 功能
from flask import render_template
from app import app
@app.route('/')
@app.route('/index')
def index():
user = {'username': 'Miguel'}
return render_template('index.html', title='Home', user=user)
这样看着好多了,对吧? 试用一下新版的程序看看模板是如何工作的。在浏览器中加载本页面之后,你或许会查看HTML源码与原来的模板进行比较。
把模板转换成纯HTML页面被称为渲染 rendering。要渲染模板,我必须导入Flask框架自带的功能: render_template()
。该函数获取模板文件名和变量列表作为参数然后返回同名模板,但模板中的所有占位符都会被替换成实际的值。
render_template()
函数调用Flask框架自带的 Jinja2 模板引擎 Jinja2 会用 render_template()
中提供的参数值替换相应的 {{ ... }}
块。
条件语句
你已经明白Jinja2在渲染过程中用值替换了占位符,但这只是Jinja2在模板文件中支持的强大操作中的一个。举例来说,模板也支持包括在 {% ... %}
中的控制语句。新一版的 index.html 模板添加了条件语句:
app/templates/index.html: 模板中的条件语句
<html>
<head>
{% if title %}
<title>{{ title }} - Microblog</title>
{% else %}
<title>Welcome to Microblog!</title>
{% endif %}
</head>
<body>
<h1>Hello, {{ user.username }}!</h1>
</body>
</html>
现在,模板变得更聪明了。如果视图函数忘记传递占位符title
的值,模板会用一个默认值替代空白赋值给title。你可以尝试在试图函数中去除title
参数来检验这一条件控制是如何工作的。
循环
已经登录的用户希望在首页上看到自己关注的用户最近发表的帖子,所以我们要扩展程序来支持这一功能。
我会再一次弄虚作假一下,用模拟数据来实现一些用户和帖子来展示:
app/routes.py: 视图函数中的假帖子
from flask import render_template
from app import app
@app.route('/')
@app.route('/index')
def index():
user = {'username': 'Miguel'}
posts = [
{
'author': {'username': 'John'},
'body': 'Beautiful day in Portland!'
},
{
'author': {'username': 'Susan'},
'body': 'The Avengers movie was so cool!'
}
]
return render_template('index.html', title='Home', user=user, posts=posts)
我用列表list
来描述用户帖子,每个列表项是字典类型项,拥有 author
和 body
字段。如果我要实现真正的用户和帖子功能时,我将尽可能保持这些字段名称,这样一来直到我介绍真的用户和帖子,我现在使用虚拟数据设计和测试首页模板做的工作将一直有效。
在模板这一边,我要解决一个新的问题。帖子列表可能有任意数量的列表项,这将由视图函数决定有多少篇帖子将展示在页面当中。模板判断不了会有多少帖子,所以需要准备把所有视图发送给他的所有帖子都显示出来。
对于这种问题,Jinja2提供了 for
控制结构:
app/templates/index.html: 模板中的for循环 for-loop
<html>
<head>
{% if title %}
<title>{{ title }} - Microblog</title>
{% else %}
<title>Welcome to Microblog</title>
{% endif %}
</head>
<body>
<h1>Hi, {{ user.username }}!</h1>
{% for post in posts %}
<div><p>{{ post.author.username }} says: <b>{{ post.body }}</b></p></div>
{% endfor %}
</body>
</html>
简单不?测试一下新版的程序,确认给帖子列表增添了更多内容,而模板是如何适应并把视图函数传递来的所有帖子都显示出来的。
模板继承
当今大部分web应用都拥有一个顶部导航栏,包含一些诸如修改个人信息登录退出之类的常用链接。我们可以在index.html
模板上添加HTML来加入导航栏,但随着程序的扩展增长,我需要不断在其他页面上做同样的添加工作。我本人是真心不希望在众多的HTML模板中维护这么多的复制代码。因此,如果可能,就不要自我重复(DRY:Don‘t repeat yourself
)是个很好的做法。
Jinja2 的模板继承功能专治这一问题。实际上,你所做的就是把布局中通用的部分转移到一个基础模板中,其他模板都从这个“母版”派生出来。
因此,我现在要定义一个基础模板,将其命名为base.html
,其中包含一个简单的导航栏和我们先前实现的标题处理逻辑。你需要编写模板代码文件app/templates/base.html如下 :
app/templates/base.html: 带有导航栏的基础模板
<html>
<head>
{% if title %}
<title>{{ title }} - Microblog</title>
{% else %}
<title>Welcome to Microblog</title>
{% endif %}
</head>
<body>
<div>Microblog: <a href="/index">Home</a></div>
<hr>
{% block content %}{% endblock %}
</body>
</html>
我在此模板中使用了 block
控制语句来定义区域,以供继承模板插入自身内容。 块(Blocks
) 被需具备一个不重复的名字,这样继承模板就知道应该把自己的内容添加到什么地方。
基础模板就位了,我就可以利用继承base.html的方式来简化 index.html :
app/templates/index.html: 继承自base.html
{% extends "base.html" %}
{% block content %}
<h1>Hi, {{ user.username }}!</h1>
{% for post in posts %}
<div><p>{{ post.author.username }} says: <b>{{ post.body }}</b></p></div>
{% endfor %}
{% endblock %}
由于 base.html 模板解决了通用结构,我就可以去除 index.html 中的其他元素而只保留内容部分。 通过extends
声明在两个模板间建立继承关系,因此Jinja2在收到渲染index.html
的请求时,它就会将其嵌入到base.html
当中。 这两个模板都拥有一致命名为content
的block
声明,因此,Jinja2就知道如何将这两个模板合并成一个。现在如果我需要创建应用程序的其他页面,我就可以通过继承base.html
模板的方式来派生之——这也是我可以创建众多具备统一外观页面而不觉得重复工作的原因。
庆祝一下吧,模板部分完美收工!