在完成第1章之后,你将拥有一个具有以下文件结构的可正常运行且简单的Web应用程序:

microblog\
  venv\
  app\
    __init__.py
    routes.py
  microblog.py

要运行该应用程序,请在终端会话中设置FLASK_APP = microblog.py,然后执行flask run。 这将使用该应用程序启动Web服务器,你可以通过在Web浏览器的地址栏中键入http://localhost:5000/ URL来打开该服务器。

在本章中,你将继续使用同一应用程序,特别是,你将学习如何生成具有复杂结构和许多动态组件的更精致的网页。 如果到目前为止尚不清楚有关应用程序或开发工作流程的任何内容,请在继续之前再次阅读第1章。

本章的GitHub链接是:浏览压缩差异

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)

为了表示用户帖子,我使用了一个列表,其中每个元素都是具有authorbody字段的字典。 当我真正实现用户和博客文章时,我将尝试尽可能保留这些字段名称,以便使用这些假对象设计和测试主页模板的所有工作将继续进行。 在介绍真实用户和帖子时有效。

在模板方面,我必须解决一个新问题。 帖子列表可以包含任意数量的元素,由查看功能决定页面中将显示多少帖子。 该模板不能对有多少个帖子做出任何假设,因此需要准备好呈现与视图以通用方式发送的一样多的帖子。

对于此类问题,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中。 这两个模板具有匹配的带有名称contentblock语句,这就是Jinja2知道如何将两个模板组合为一个的方式。 现在,如果需要为该应用程序创建其他页面,则可以将它们创建为来自同一base.html模板的派生模板,这就是我可以使应用程序的所有页面共享相同外观的方式,而无需重复。