Part2 模板(Template)

在 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来描述用户帖子,每个列表项是字典类型项,拥有 authorbody 字段。如果我要实现真正的用户和帖子功能时,我将尽可能保持这些字段名称,这样一来直到我介绍真的用户和帖子,我现在使用虚拟数据设计和测试首页模板做的工作将一直有效。
在模板这一边,我要解决一个新的问题。帖子列表可能有任意数量的列表项,这将由视图函数决定有多少篇帖子将展示在页面当中。模板判断不了会有多少帖子,所以需要准备把所有视图发送给他的所有帖子都显示出来。
对于这种问题,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当中。 这两个模板都拥有一致命名为contentblock声明,因此,Jinja2就知道如何将这两个模板合并成一个。现在如果我需要创建应用程序的其他页面,我就可以通过继承base.html模板的方式来派生之——这也是我可以创建众多具备统一外观页面而不觉得重复工作的原因。

模板继承

庆祝一下吧,模板部分完美收工!

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,271评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,275评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,151评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,550评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,553评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,559评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,924评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,580评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,826评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,578评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,661评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,363评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,940评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,926评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,156评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,872评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,391评论 2 342

推荐阅读更多精彩内容