Flask Web Development 第三章读书笔记 模板

第三章 模板

为什么要分离

易于维护的代码,关键在于保持简单的结构。
而我们之前编写的hello.py虽然简单,却混合了两种不同的部分:
如何生成页面数据、如何显示。
将这两部分混合在一起会带来复杂性。

注意hello.py中的return '<h1>Hello, world</h1>'
第一部分是生成数据:生成Hello,world,第二部分是如何显示:<h1>一级标题。

为什么需要模板

分离后我们将把hello,world!这样的html页面单独存放。
这样当用户打开一个网址时,服务器立即把对应的html页面发送给他。
可事情这么简单就好了,很多html页面中的东西并不是一成不变的。
我们需要一种技术,让页面中的一部分可以动态变化:渲染。
为了渲染模板,Flask使用了Jinja2.

3.1 Jinja2模板引擎

一个简单的静态模板:templates/index.html
<h1>Hello world!</h1>

增加动态部分的形式 :templates/user.html
<h1>Hello ,{{ name }}!</h1>

第一个模板为静态,不需要Jinja2也能支持。
而Jinja2会对动态的部分进行替换,如第二个模板中的动态部分。
此时用html直接解析会出现错误,必须有类似Jinja2的模板引擎。

为了简化程序,不需要在Flask程序里显示调用Jinja2。

3.1.1 渲染模板

如何使用模板

在Flask程序里调用render_template函数后,就使用了模板。

示例 3-3 hello.py: 渲染模板  
from flask import Flask, render_template
 
# ...
  
@app.route('/')
def index():
    return render_template('index.html')
  
@app.route('/user/<name>')
def user(name):
    return render_template('user.html', name=name)

模板参数和搜索路径

在例3-3中,render_template有两个参数。

第一个是模板文件名,以html结尾,默认在程序/templates文件夹下搜寻。
还记得刚开始我们讲Flask类的参数(__name__)的作用吗。

第二个是动态参数的值,如name=name:
前一个name就是模板中的{{ name }}(见序的第二个文件),即模板中的动态部分;
后一个name是动态部分要在页面显示的值,在这里是函数user的参数,它会被传给模板渲染。

3.1.2 变量

什么是变量

模板中使用的两个大括号包裹的{{ name }}表示一个变量,
它的值由Flask程序提供(渲染)。
Jinja2能识别python中所有类型的变量,例如列表、字典和对象。

一些在模板中使用变量的示例:

<p>引用字典的一个值: {{ mydict['key'] }}.</p>
# <p>是html标签,表示段落(paragraph)
<p>引用列表的一个值: {{ mylist[3] }}.</p>
<p>引用列表中的一个值,键为变量: {{ mylist[myintvar] }}.</p>
<p>引用对象的一个方法: {{ myobj.somemethod() }}.</p>

什么是过滤器

有时候出于安全考虑或对变量进行形式变更,
这时候需要用到过滤器。
例如以首字母大写形式显示变量 name 的值:
Hello, {{ name|capitalize }}
使用过滤器,在变量名后加竖线和过滤器名即可。

常见的过滤器

capitalize  # 把值的首字母转换成大写,其他字母转换成小写
safe  # 渲染值时不转义
lower  # 把值转换成小写形式
upper  # 把值转换成大写形式
title  # 把值中每个单词的首字母都转换成大写
trim  # 把值的首尾空格去掉
striptags  # 渲染之前把值中所有的 HTML 标签都删掉

特别的safe过滤器

默认情况下,出于安全考虑, Jinja2 会转义所有变量。
例如,如果一个变量的值为 '<h1>Hello</h1>'
Jinja2 会将其渲染成 '<h1>Hello</ h1>'
浏览器能显示这个 h1 元素,但不会进行解释。
很多情况下需要显示变量中存储的 HTML 代码,
这时就可使用 safe 过滤器。

千万别在不可信的值上使用 safe 过滤器,
例如用户在表单中输入的文本。
完整的过滤器列表可在
Jinja2 文档查看。

3.1.3 控制结构

if条件控制

{% if user %}
    Hello, {{ user }}!
{% else %}
    Hello, Stranger!
{% endif %}

控制语句以{% 关键字%}开始,
注意有一个结束语句endif。

for循环

一个常见需求是在模板中渲染一组元素。

<ul>
    {% for comment in comments %}
        <li>{{ comment }}</li> 
    {% endfor %}
</ul>

同样有endfor,Jinja2中的结束关键字是end+开始关键字,中间没有空格。

<li>用来定义列表的条目,可以用在无序列表<ul>和有序列表<ol>中。

宏类似于函数,例如:

{% macro render(w) %}
    <li>{{ w }}</li>
{% endmacro %}
 
 
<ul>
    {% for w in word %}
        {{ render(w) }}
    {% endfor %}
</ul>

第二段for循环里的render就是前面定义的宏,
作用是对序列word中的每一项进行渲染。

为了重复使用宏,我们可以将其保存在单独的文件中,
然后在需要使用的模板中导入。
例如我们把若干宏保存到macros.html后,可以用类似python包的形式导入。

{% import 'macros.html' as macros %}
<ul>
    {% for w in word %}
        {{ macros.render(w) }}
    {% endfor %}
</ul>

模板继承

一种方式是使用include:模板插入。
需要在多处重复使用的模板代码片段可以写入单独的文件,
再包含在模板的某一位置中,以避免重复:
{% include 'common.html' %}

另一种是使用extends:模板继承。
它类似于 Python 代码中的类继承,具有更强的功能。
首先,创建一个名为 base.html 的基模板:

<head>
    {% block head %}
    <title>{% block title %}{% endblock %} - My Application</title>
    {% endblock %}
</head>

使用base.html的模板叫子模板,base是它的父模板。子模板可以修改block元素。

{% extends "base.html" %}
{% block title %}Index{% endblock %}
{% block head %}
    {{ super() }}
   <style>
   </style>
{% endblock %}

extends 声明子模板继承自base.html,
extends必须放在第一行。
子模板重新定义了base.html中的2个block,
在子模板的block中,如果想包含在父模板block的内容,
使用{{ super() }},
这会获取对应block的内容。

这种方式和include的不同在于,include写好的片段不再修改,只是简单插入,
最终由调用include的模板进行渲染。
而extends继承,是在base.html上做扩展修改,
最终由extends所在的模板进行渲染。

3.2 Flask-Bootstrap

Bootstrap是什么

Bootstrap是Twitter开发的一个开源框架,
可用来快速开发美观的网页外观。
它不但兼容所有的现代web浏览器,
而且一份代码,便可以支持手机、电脑、平板的浏览。

Bootstrap是客户端框架,因此不会直接涉及服务器。
服务器需要的只是提供使用了Bootstrap和JavaScript的代码,
并且在这些代码中实例化所需组件。
这些操作最理想的执行场所就是模板。

为什么需要Flask-Bootstrap

要想在程序中使用Bootstrap,
显然原来的模板要进行必要的改动来适应它。
不过有人已经开发了Flask-Bootstrap,
用来简化这些改动。

如何安装使用Flask-Bootstrap

Flask-Bootstrap使用pip安装:
(venv) $ pip install flask-bootstrap
类似Flask-Script,Flask扩展一般都在创建程序实例时初始化。
下面是Flask-Bootstrap的初始化方法。

from flask_bootstrap import Bootstrap
# ...
bootstrap = Bootstrap(app)

初始化 Flask-Bootstrap 之后,
就可以在程序中使用一个包含所有 Bootstrap 文件的基模板。
这个模板利用 Jinja2 的模板继承机制,
让程序扩展一个具有基本页面结构的基模板,
其中就有用来引入 Bootstrap 的元素。

使用Flask-Bootstrap的模板

{% extends "bootstrap/base.html" %}
 
{% block title %}Flasky{% endblock %}
 
{% block navbar %}
<div class="navbar navbar-inverse" role="navigation">
    <div class="container">
        <div class="navbar-header">
            <button type="button" class="navbar-toggle"
            data-toggle="collapse" data-target=".navbar-collapse">
                <span class="sr-only">Toggle navigation</span>
                <span class="icon-bar"></span>
                <span class="icon-bar"></span>
                <span class="icon-bar"></span>
            </button>
            <a class="navbar-brand" href="/">Flasky</a>
        </div>
        <div class="navbar-collapse collapse">
            <ul class="nav navbar-nav">
                <li><a href="/">Home</a></li>
            </ul>
        </div>
    </div>
</div>
{% endblock %}
 
 
{% block content %}
<div class="container">
    <div class="page-header">
        <h1>Hello, {{ name }}!</h1>
    </div>
</div>
{% endblock %}

它和Flask-Script一样,初始化了Flask的实例app,
但不同的是,Bootstrap实例没有run方法,
还是像以前一样运行app.run()。

对这个模板的简单说明

现在你看到的模板比较复杂,我们现在只需要知道,
整个模板定义了3个块,title,navbar和content。
title很明显是网页标签bar上出现的网站信息,
navbar就是导航条,而content就是网页的主体部分。
其中的Flasky,Home,Hello,你也可以自己替换成中文。

注意extends后面,在当前程序下并没有这个bootstrap目录,
也就是说,我们写的bootstrap实际上是表明了我们引用了
bootstrap的模板,模板名叫base.html,
具体路径是C:\Python34\Lib\site-packages\flask_bootstrap\templates\bootstrap,
根据你的python版本不同,路径中的python34可能是python27或python36等等。
你可以到这个路径下看看还有什么模板。

Flask-Bootstrap基模板中定义的块

块名 说明
doc 整个html文档
html_attribs <html>标签的属性
html <html>标签中的内容
head <head>标签中的内容
title <title>标签中的内容
metas 一组<meta>标签
styles 层叠样式表定义
body_attribs <body>标签的属性
body <body>标签中的内容
navbar 用户定义的导航条
content 用户定义的页面内容
scripts 文档底部的JavaScript声明

表 3-2 中的很多块都是 Flask-Bootstrap 自用的,
如果直接重定义可能会导致一些问题。
例如,Bootstrap 所需的文件在 styles 和 scripts 块中声明。
如果程序需要向已经有内容的块中添加新内容,
必须使用 Jinja2 提供的 super() 函数。
例如,如果要在衍生模板中添加新
的 JavaScript 文件,需要这么定义 scripts 块:

{% block scripts %}
    {{ super() }}
    <script type="text/javascript" src="my-script.js"></script>
{% endblock %}

3.3 自定义错误页面

为什么需要自定义错误页面

现在你在地址栏输入一个不存在的地址,
出来的404页面会显示很多英文,
并且与使用了Bootstrap的页面不一致,
这或许不是你想要的。

有哪些错误页面

Flask允许程序使用基于模板的自定义错误页面。
最常见的错误代码有两个:

  • 404, 客户端请求未知页面或路由时显示;
  • 500,有未处理的异常时显示。

自定义错误的示例

@app.errorhandler(404)
def page_not_found(e):
    return render_template('404.html'), 404
 
@app.errorhandler(500)
def internal_server_error(e):
    return render_template('500.html'), 500

装饰器后出现了新的方法:errorhandler。
此方法指定了它想改变的错误页面。
函数接受了一个e的错误,
这个值在这并没有被使用。
注意返回渲染的模板后,还加上了错误代码。
我们稍后可以试试不加会怎样。

布局一致的要求

我们在上例中告诉Flask去寻找404.html和500.html这两个模板,
而现在templates目录下没有这两个模板,
也不符合extends bootstrap的情况,
如果你无法忍受默认的错误页面,
那这两个模板必须我们自己编写。
这些模板应该和常规页面使用相同的布局,
因此要有一个导航条和显示错误消息的页面头部。

编写错误页面的笨方法

编写这些模板最直观的方法是复制 templates/user.html,
分别创建 templates/404.html 和
templates/500.html,
然后把这两个文件中的页面头部元素改为相应的错误消息。
但这种方法会带来很多重复劳动。

更灵活的方式

Jinja2 的模板继承机制可以帮助我们解决这一问题。
Flask-Bootstrap 提供了一个具有页面基本布局的基模板,
同样,程序可以定义一个具有更完整页面布局的基模板,
其中包含导航条,
而页面内容则可留到衍生模板中定义。
这个模板本身也可作为其他模板的基模板,
例如 templates/user.html、 templates/404.html 和 templates/500.html。

修改后的基模板

{% extends "bootstrap/base.html" %}
 
{% block title %}Flasky{% endblock %}
 
{% block navbar %}
<div class="navbar navbar-inverse" role="navigation">
    <div class="container">
        <div class="navbar-header">
            <button type="button" class="navbar-toggle"
            data-toggle="collapse" data-target=".navbar-collapse">
                <span class="sr-only">Toggle navigation</span>
                <span class="icon-bar"></span>
                <span class="icon-bar"></span>
                <span class="icon-bar"></span>
            </button>
            <a class="navbar-brand" href="/">Flasky</a>
        </div>
        <div class="navbar-collapse collapse">
            <ul class="nav navbar-nav">
                <li><a href="/">Home</a></li>
            </ul>
        </div>
    </div>
</div>
{% endblock %}
 
{% block content %}
<div class="container">
    {% block page_content %}{% endblock %}
</div>
{% endblock %}

这个模板的 content 块中只有一个 <div> 容器,
其中包含了一个名为 page_content 的新的空块,
块中的内容由衍生模板定义。

如何编写子模板

现在,程序使用的模板继承自这个模板,
而不直接继承自 Flask-Bootstrap 的基模板。
通过继承 templates/base.html 模板编写自定义的 404 错误页面很简单,

{% extends "base.html" %}
 
{% block title %}Flasky - Page Not Found{% endblock %}
 
{% block page_content %}
<div class="page-header">
    <h1>Not Found</h1>
</div>
{% endblock %}

注意基模板定义的title块我们也可以重新修改,
也就是尽量利用父模板的代码,
但可以进行必要的修改。
现在你可以试试修改index、500和user模板。

3.4 链接辅助函数

为什么需要链接辅助

任何具有多个路由的程序都需要可以连接不同页面的链接,例如导航条。
在模板中直接编写简单路由的 URL 链接不难,
但写死的URL不具有灵活性,
如果其中有可变部分,
重新定义路由后,模板中的链接可能会失效。

url_for()的简单用法

为了避免这些问题, Flask 提供了 url_for() 辅助函数,
它使用 URL 映射中的信息生成 URL。

url_for() 函数最简单的用法是以视图函数名作为参数,
返回对应的 URL。
例如,在当前版本的 hello.py 程序中调用 url_for('index') 得到的结果是 /。
调用 url_for('index', _external=True) 返回的则是绝对地址,
在这个示例中是 http://localhost:5000/

生成连接程序内不同路由的链接时,使用相对地址就足够了。
如果要生成在浏览器之外使用的链接,
则必须使用绝对地址,
例如在电子邮件中发送的链接。

url_for的更多用法

使用 url_for() 生成动态地址时,将动态部分作为关键字参数传入。
例如, url_for('user', name='john', _external=True) 的返回结果是 http://localhost:5000/user/john

传入 url_for() 的关键字参数不仅限于动态路由中的参数。
函数能将任何额外参数添加到查询字符串中。
例如, url_for('index', page=2) 的返回结果是 /?page=2。

在hello.py的最后注释掉if __name__app.run两行。
添加以下2行代码并运行:

with app.test_request_context():
    print(url_for('index'))

test_request_context是Flask实例的一个属性,
用于在没有运行Flask实例时的上下文测试,
因为一旦app.run()运行实例,那个print语句就不会起作用。

3.5 静态文件

什么是静态文件

Web 程序不是仅由 Python 代码和模板组成。
大多数程序还会使用静态文件,
例如 HTML代码中引用的图片、 JavaScript 源码文件和 CSS。

静态路由

你可能还记得在第 2 章中检查 hello.py 程序的 URL 映射时,
其中有一个 static 路由。
这是因为对静态文件的引用被当成一个特殊的路由,
即 /static/<filename>。
例如, 调用url_for('static', filename='css/styles.css', _external=True)
得 到 的 结 果 是 http://localhost:5000/static/css/styles.css

默认设置下, Flask 在程序根目录中名为 static 的子目录中寻找静态文件。
如果需要,可在static 文件夹中使用子文件夹存放文件。
服务器收到前面那个 URL 后,
会生成一个响应,
包含文件系统中 static/css/styles.css 文件的内容。

注意filename文件名是完整路径

定义收藏夹图标的示例

{% block head %}
    {{ super() }}
    <link rel="shortcut icon" href="{{ url_for('static', filename = 'favicon.ico') }}"type="image/x-icon">
    <link rel="icon" href="{{ url_for('static', filename = 'favicon.ico') }}"type="image/x-icon">
{% endblock %}

图标的声明会插入 block head的末尾。
注意如何使用 super() 保留基模板中定义的块的原始内容。

3.6 Flask-Moment:本地化时间

为什么要本地化时间

如果 Web 程序的用户来自世界各地,
那么处理日期和时间可不是一个简单的任务。

服务器需要统一时间单位,
这和用户所在的地理位置无关,
所以一般使用协调世界时( Coordinated Universal Time, UTC)。
不过用户看到 UTC 格式的时间会感到困惑,
他们更希望看到当地时间,
而且采用当地惯用的格式。

如何本地化时间

要想在服务器上只使用 UTC 时间,
一个优雅的解决方案是,
把时间单位发送给 Web 浏览器,
转换成当地时间, 然后渲染。
Web 浏览器可以更好地完成这一任务,
因为它能获取用户电脑中的时区和区域设置。

Flask-Moment 是什么

有一个使用 JavaScript 开发的优秀客户端开源代码库,
名为 moment.js( http://momentjs.com/),
它可以在浏览器中渲染日期和时间。
Flask-Moment 是一个 Flask 程序扩展,
能把moment.js 集成到 Jinja2 模板中。

如何安装和初始化

Flask-Moment 可以使用 pip 安装:

$ pip install flask-moment
from flask_moment import Moment
moment = Moment(app)

除了 moment.js, Flask-Moment 还依赖 jquery.js。
要在 HTML 文档的某个地方引入这两个库,
可以直接引入,这样可以选择使用哪个版本,
也可使用扩展提供的辅助函数,
从内容分发网络( Content Delivery Network,
CDN)中引入通过测试的版本。
Bootstrap 已经引入了 jquery.js,
因此只需引入 moment.js 即可。

下面的例子展示了如何在基模板的 scripts 块中引入这个库。

{% block scripts %}
    {{ super() }}
    {{ moment.include_moment() }}  # 引入moment.js库
{% endblock %}

如何使用Flask-Moment

为了处理时间戳, Flask-Moment 向模板开放了 moment类。

  • hello.py:加入一个datetime变量。
from datetime import datetime
 
@app.route('/')
def index():
    return render_template('index.html',current_time=datetime.utcnow())
  • templates/index.html:使用 Flask-Moment 渲染时间戳
<p>The local date and time is {{ moment(current_time).format('LLL') }}.</p>
<p>That was {{ moment(current_time).fromNow(refresh=True) }}</p>

format('LLL') 根据客户端电脑中的时区和区域设置渲染日期和时间。
参数决定了渲染的方式,
'L' 到 'LLLL' 分别对应不同的复杂度。
format() 函数还可接受自定义的格式说明符。

第二行中的 fromNow() 渲染相对时间戳,
而且会随着时间的推移自动刷新显示的时间。
这个时间戳最开始显示为“ a few seconds ago”,
但指定 refresh 参数后,
其内容会随着时间的推移而更新。 如果一直待在这个页面,
几分钟后,会看到显示的文本变成“ a minuteago”“ 2 minutes ago”等。

其他功能

Flask-Moment 渲染的时间戳可实现多种语言的本地化。
语言可在模板中选择,把语言代码
传给 lang() 函数即可:
{{ moment.lang("zh_cn") }}
Flask-Moment 实现了 moment.js 中的 format()、 fromNow()、 fromTime()、 calendar()、 valueOf()和 unix() 方法。
你可查阅文档 http://momentjs.com/docs/#/displaying/
学习 moment.js 提供的全部格式化选项。

Flask-Monet 假定服务器端程序处理的时间戳是“纯正的” datetime 对象,
且使用 UTC 表示。
关于纯正和细致的日期和时间对象 1 的说明,
请阅读标准库中 datetime 包的文档
https://docs.python.org/2/library/datetime.html

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

推荐阅读更多精彩内容