在完成第1章之后,你将拥有一个具有以下文件结构的可正常运行且简单的Web应用程序:
microblog\
venv\
app\
__init__.py
routes.py
microblog.py
要运行该应用程序,请在终端会话中设置FLASK_APP = microblog.py
,然后执行flask run
。 这将使用该应用程序启动Web服务器,你可以通过在Web浏览器的地址栏中键入http://localhost:5000/
URL来打开该服务器。
在本章中,你将继续使用同一应用程序,特别是,你将学习如何生成具有复杂结构和许多动态组件的更精致的网页。 如果到目前为止尚不清楚有关应用程序或开发工作流程的任何内容,请在继续之前再次阅读第1章。
What Are Templates?
我希望我的微博应用程序的首页具有欢迎用户的标题。 目前,我将忽略以下事实:该应用程序还没有用户概念,因为这将在以后出现。 相反,我将使用模拟用户,该用户将作为Python字典实现,如下所示:
user = {'username': 'Miguel'}
创建模拟对象是一种有用的技术,它使你可以专注于应用程序的一部分,而不必担心系统中尚不存在的其他部分。 我想设计应用程序的主页,也不希望没有适当的用户系统来分散我的注意力,所以我只是组成了一个用户对象,以便继续前进。
应用程序中的view函数返回一个简单的字符串。 我现在想做的是将返回的字符串扩展为完整的HTML页面,也许是这样的:
#app/routes.py: Return complete HTML page from view function
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,建议你阅读Wikipedia上的HTML标记以作简要介绍。
如上所述更新视图功能,并尝试在浏览器中查看应用程序的外观。
我希望你同意我的观点,以上将HTML传输到浏览器的解决方案不是很好。考虑一下当我收到来自用户的博客文章时,此视图功能中的代码将变得多么复杂,并且这些博客文章将不断变化。该应用程序还将具有更多的视图功能,这些视图功能将与其他URL关联,因此想象一下,如果有一天我决定更改此应用程序的布局,并且必须在每个视图功能中更新HTML。显然,这不是随应用程序的增长而扩展的选项。
如果你可以将应用程序的逻辑与网页的布局或表示形式分开,那么事情就会井井有条,不是吗?在用Python编写应用程序逻辑时,你甚至可以雇用一名Web设计人员来创建一个杀手级网站。
模板有助于实现表示和业务逻辑之间的分离。在Flask中,模板被写为单独的文件,存储在应用程序包内的templates
文件夹中。因此,在确保你位于microblog
目录中之后,创建用于存储模板的目录:
(venv) $ mkdir app/templates
在下面,你可以看到你的第一个模板,其功能与上面的index()
视图函数返回的HTML页面相似。 将此文件写入app/templates/index.html
中:
<!--app/templates/index.html: Main page template/-->
<html>
<head>
<title>{{ title }} - Microblog</title>
</head>
<body>
<h1>Hello, {{ user.username }}!</h1>
</body>
</html>
这是一个非常标准的HTML页面。 此页面上唯一有趣的事情是,在{{...}}
部分中包含了几个用于动态内容的占位符。 这些占位符代表页面的可变部分,只有在运行时才知道。
现在,页面的显示已被卸载到HTML模板,可以简化视图功能:
#app/routes.py: Use render\_template() function
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()
调用中提供的参数给出。
Conditional Statements
你已经看到Jinja2如何在渲染过程中用实际值替换占位符,但这只是Jinja2在模板文件中支持的许多强大操作之一。 例如,模板还支持在{%...%}
块内给出的控制语句。 下一个index.html
模板版本添加了一条条件语句:
<!--app/templates/index.html: Conditional statement in template-->
<html>
<head>
{% if title %}
<title>{{ title }} - Microblog</title>
{% else %}
<title>Welcome to Microblog!</title>
{% endif %}
</head>
<body>
<h1>Hello, {{ user.username }}!</h1>
</body>
</html>
现在,模板更加智能。 如果view函数忘记传递title
占位符变量的值,则模板将显示默认标题,而不是显示空标题。 你可以通过在视图函数的render_template()
调用中删除title
参数来尝试此条件的工作方式。
Loops
登录的用户可能希望在主页中查看已连接用户的最新帖子,因此我现在要做的就是扩展应用程序以支持该操作。
再一次,我将依靠方便的假对象技巧来创建一些用户和一些帖子来显示:
#app/routes.py: Fake posts in view function
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)
为了表示用户帖子,我使用了一个列表,其中每个元素都是具有author
和body
字段的字典。 当我真正实现用户和博客文章时,我将尝试尽可能保留这些字段名称,以便使用这些假对象设计和测试主页模板的所有工作将继续进行。 在介绍真实用户和帖子时有效。
在模板方面,我必须解决一个新问题。 帖子列表可以包含任意数量的元素,由查看功能决定页面中将显示多少帖子。 该模板不能对有多少个帖子做出任何假设,因此需要准备好呈现与视图以通用方式发送的一样多的帖子。
对于此类问题,Jinja2提供了for
控件结构:
<!--app/templates/index.html: for-loop in template-->
<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>
简单吧? 请尝试使用该新版本的应用程序,并确保将更多内容添加到帖子列表中,以查看模板如何适应并始终呈现视图功能发送的所有帖子。
Template Inheritance
如今,大多数Web应用程序在页面顶部都有一个导航栏,其中包含一些常用链接,例如用于编辑个人资料,登录,注销等的链接。我可以轻松地将导航栏添加到index.html
。 带有更多HTML的模板,但是随着应用程序的增长,我将在其他页面中需要相同的导航栏。 我并不是很想在许多HTML模板中维护导航栏的多个副本,如果可能的话,最好不要重复自己的做法。
Jinja2具有专门解决此问题的模板继承功能。 本质上,你可以做的是将页面布局中所有模板共有的部分移动到基本模板,所有其他模板都从该基本模板中派生。
因此,我现在要做的是定义一个名为base.html
的基本模板,该模板包括一个简单的导航栏以及我之前实现的标题逻辑。 你需要在文件app/templates/base.html
中编写以下模板:
<!--app/templates/base.html: Base template with navigation bar-->
<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
控制语句来定义派生模板可插入自身的位置。 块具有唯一的名称,派生的模板在提供其内容时可以引用这些名称。
有了基本模板之后,我现在可以通过使其继承自base.html
来简化index.html
:
<!--app/templates/index.html: Inherit from base template-->
{% 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
中删除了所有这些元素,仅保留了内容部分。 extend
语句在两个模板之间建立继承链接,因此Jinja2知道当要求呈现index.html
时,需要将其嵌入base.html
中。 这两个模板具有匹配的带有名称content
的block
语句,这就是Jinja2知道如何将两个模板组合为一个的方式。 现在,如果需要为该应用程序创建其他页面,则可以将它们创建为来自同一base.html
模板的派生模板,这就是我可以使应用程序的所有页面共享相同外观的方式,而无需重复。