学习Flask之三、模板
书写易于维护的应用的关键是书写整洁和良构的代码。到目前为止你所见的例子过于简单而不能体现这点。把两个目的完全独立的Flask view 函数当作一个来写,会产生问题。view函数的一个显然的任务是对请求作出响应,如前面的例子所示。对于简单的请求,这是足够的。但是通常请求会触发应用状态的改变,view函数是产生改变的来源地。
例如,考虑一下用户注册一个新的网站帐户。用户在网页的表单里输入email地址和密码,并点击提交按钮。在服务端,包含用户数据的请求到达,Flask把它分派给处理注册请求的view函数。这个view函数需要与数据库对话以添加新的用户然后产生响应送回浏览器。这两种类型的任务正规的称为业务逻辑和呈现逻辑。将业务逻辑和呈现逻辑混合会使代码难于理解和维护。将呈现逻辑转移到模板(templates)有助于改进应用的可维护性。模板是包含响应文本的文件,带有表示动态部分的占位符变量,动态部只有请求上下文才知道。用真实值代替变量并返回最终响应字符的过程称为宣染(rendering)。对于宣染模板的任务,Flask提供了一个强大的模板引擎称为Jinja2。
Jinja2模板引擎
最简单的形式,Jinja2模板是一个包含响应文本的文件。
Example 3-1展示了一个与Example 2-1中的index() view函数相匹配的Jinja2模板。
Example 3-1. templates/index.html: Jinja2 template
<h1>Hello World!</h1>
Example 2-2的view函数user()返回的响应有动态成份,用变量表示。
Example 3-2展示完成这种响应的模板。
Example 3-2. templates/user.html: Jinja2 template
<h1>Hello, {{ name }}!</h1>
宣染模板
黙认情况下,Flask从应用文件夹的templates子文件夹中查找模板。在下一版的hello.py里,你需要将前面的模板放在新的templates文件夹并命名为index.html和user.html。
需要修改应用的view函数以宣染模板。
Example 3-3展示这些更新。
Example 3-3. hello.py: Rendering a template
from flask import Flask, render_template
# ...
@app.route('/index')
def index():
return render_template('index.html')
@app.route('/user/<name>')
def user(name):
return render_template('user.html', name=name)
Flask提供的render_template集成Jinja2模板引擎。这个函数以模板的文件名作为第一个参数。任何其它的参数包括 key/value对,表示模板中引用的变量的实际值。本例中,第二个模板接收一个name 变量。
如果你不使用它们的话,前面的类似name=name的关键字参数很常见但是看起来很混淆而且难于理解。左边的“name”表示参数名,在模板文件中用作占位符。右边的“name” 是个当前范围的变量,提供同名参数的值。
变量
Example 3-2模板中使用的{{ name }}结构引用一个变量,一种特殊的占位符,告诉模板引擎那个位置的值应该来自从模板宣染时提供的数据。Jinja2可以识别任意类型的变量,即便是复杂的列表,字典和对象。如下是模板中使用变量额外的例子:
<p>A value from a dictionary: {{ mydict['key'] }}.</p>
<p>A value from a list: {{ mylist[3] }}.</p>
<p>A value from a list, with a variable index: {{ mylist[myintvar] }}.</p>
<p>A value from an object's method: {{ myobj.somemethod() }}.</p>
变量可以用过滤器修饰,它放在变量名后,用管道符分隔。例如,下面的模板显示首字符大写的name变量。
Hello, {{ name|capitalize }}
Table 3-1 列出了一些Jinja2里常用的过滤器。见:
http://www.aluoyun.cn/details.php?article_id=192
safe很有意思,值得强调。 黙认情况下出于安全目的Jinja2去除所有变量的tags。例如,如果变量设定值为 '<h1>Hello</h1>', Jinja2 会宣染值为'<h1>Hello</h1>',这会使得h1元素显示而不是被浏览器解释。很多时候有必要显示存贮于变量的HTML代码,这时候 safe过滤器派上用场。
对于不信任的值不要使用safe,例如用户在表单输入的文本。
关于过滤器的完全列表可以参见Jinja2官方文档。
控制结构
Jinja2 提供了多个控制结构可以用来改变模板。
本节介绍一些最有用的例子 。
下面的例子显示如何在模板里使用条件语句:
{% if user %}
Hello, {{ user }}!
{% else %}
Hello, Stranger!
{% endif %}
模板里另一个常见需求是宣染元素的列表。本例展示如何用for循环来完成这个任务:
<ul>
{% for comment in comments %}
<li>{{ comment }}</li>
{% endfor %}
</ul>
Jinja2也支持macros,这与Python代码里的函数相似。例如:
{% macro render_comment(comment) %}
<li>{{ comment }}</li>
{% endmacro %}
<ul>
{% for comment in comments %}
{{ render_comment(comment) }}
{% endfor %}
</ul>
为了使macros可复用,可以把它存贮于单一的文件然后引入到需要它们的模板中:
{% import 'macros.html' as macros %}
<ul>
{% for comment in comments %}
{{ macros.render_comment(comment) }}
{% endfor %}
</ul>
需要在不同的地方重复的部分模板代码也可以存贮于单独的文件并在模板中包含它以免重复:
{% include 'common.html' %}
另一种强大的方法是通过模板继承,这与Python代码里的类的继承相似。首先创建基模板,命名为base.html:
<html>
<head>
{% block head %}
<title>{% block title %}{% endblock %} - My Application</title>
{% endblock %}
</head>
<body>
{% block body %}
{% endblock %}
</body>
</html>
这里block标记定义派生模板可以改变的元素。本例中,有head,title, 和body块;注意title包含于head。下面的模板是由基模板派生的:
{% extends "base.html" %}
{% block title %}Index{% endblock %}
{% block head %}
{{ super() }}
<style>
</style>
{% endblock %}
{% block body %}
<h1>Hello, World!</h1>
{% endblock %}
extends指令声明模板从base.html派生。这个指令跟着3个新的块定义,分别在合适的地方插入。注意head块的新的定义,它在基类中不是空的,所以用 super()保留原始的内容。本节中呈显的所有的控制结构的实际使用将在后面展示,所以你有机会看到它们是如何工作的。
用Flask-Bootstrap集成Twitter Bootstrap
Bootstrap是来自Twitter的开源框架,提供了用户接口来创建简洁而有吸引力的兼容所有现代浏览器的网页。
Bootstrap是客户端的框架,所以服务器不会直接参与它。服务器需要做的是提供HTML响应,它引用 Bootstrap的CSS和JavaScript文件并通过HTML,CSS,和JavaScript代码实例化所需要的元素。完成所有这些的理想场所是在模板里。
很显然的方法是在应用中集成Bootstrap以对模板作必要的修改。更简单 的方法是使用Flask的扩展,称为Flask-Bootstrap以简化集成工作。可以用pip安装Flask-Bootstrap:
(venv) $ pip install flask-bootstrap
Flask扩展通常在应用实例创建的同时初始化。
Example 3-4 shows the initialization of Flask-Bootstrap.
Example 3-4. hello.py: Flask-Bootstrap initialization
from flask.ext.bootstrap import Bootstrap
# ...
bootstrap = Bootstrap(app)
如第二章的Flask-Script,Flask-Bootstrap 从flask_名字空间引入,并通过传入应用实例初始化。
一旦Flask-Bootstrap初始化,包含所有Bootstrap文件的基模板就可以被就用使用。
这个模板使用了Jinja2的模板继承;这个应用扩展具有通用网页结构的基模板包括引入 Bootstrap的元素。 Example 3-5展示新版本user.html作为衍生模板。
Example 3-5. templates/user.html: Template that uses 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 %}
Jinja2的extends指令通过引用Flask-Bootstrap的bootstrap/base.html实施模板继承。来自Flask-Bootstrap的基模板提供了骨架网页以包含所有Bootstrap CSS和JavaScript 文件。基模板定义了可以被派生模板重写的块。block和endblock指令定义添加到基模板的块的内容。前面的user.html模板定义三个块称为title,navbar,和content。
这些是基模块中被派生模板导出的所有的块。title非常直接,它的内容在HTML文件的header的<title>标记内。保留navbar和 content块用作网页的导航栏和主内容。在这个模板里,navbar块定义一个简单的导航栏,使用Bootstrap元素。
content有一个容器<div>其中有网页的header。第一个版本的欢迎行现在在网页的header里了。
Flask-Bootstrap的base.html模板定义了多个可以在派生模板中使用的其它块。Table 3-2 显示了所有的可用的块的列表。见:
http://www.aluoyun.cn/details.php?article_id=193
Table 3-2里的许多块被Flask-Bootstrap本身使用,所以直接重写它们会出现问题。例如,styles和scripts块是声明Bootstrap文件的地方。如果应用需要添加内容到一个已有内容的块,则必须使用Jinja2的super()函数。例如,这里是如何在派生模板里书写scripts块来添加新的JavaScript文件到文档中:
{% block scripts %}
{{ super() }}
<script type="text/javascript" src="my-script.js"></script>
{% endblock %}
定义错误页
当你在浏览器的地址栏中输入无效的路由时,你会得到代码404的错误页。错误页太过直白不具吸引力,它也与使用Bootstrap的网页没有联系。Flask 允许应用自定义错误页,它可以基于模板,就像正常的路由一样。有两个最常见的错误代码是404,当客户端请求一个未知的网页或路由时触发,当出现未处理的意外时触发500代码。
Example 3-6展示如何为这两个错误提供自定义的处理器。
Example 3-6. hello.py: Custom error pages
@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
错误处理器返回响应,像view函数一样。它们也返回数值状态码与错误相应。错误处理器中引用的模板需要重写。这些模板要遵循一般网页的布局,这种情况下它们有导航栏和网页的header显示错误信息。书写这些模块的直接的方法是拷贝templates/user.html 到templates/404.html和 templates/500.html,然后改变网页的 header元素以适应错误信息。但是这样会产生在量的重复。Jinja2的模板继承可以帮助这一点。同样Flask-Bootstrap 提供了带有网页的基础布局的基模板,应用可以定义它自已的基模板使它具有更完整的页面布局包括导航栏和预留的网页内容以在派生模板中定义。
Example 3-7展示templates/base.html,一个派生自bootstrap/base.html的新的模板并定义导航栏,但它本身是其它模板如templates/user.html, templates/404.html, 和templates/500.html的基文件。
Example 3-7. templates/base.html: Base application template with navigation bar
{% 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。
Example 3-8 显示自templates/base.html构建一个自定义的404错误页是多么的简单。
Example 3-8. templates/404.html: Custom code 404 error page using template inheritance
{% extends "base.html" %}
{% block title %}Flasky - Page Not Found{% endblock %}
{% block page_content %}
<div class="page-header">
<h1>Not Found</h1>
</div>
{% endblock %}
templates/user.html模板现在可以简单的继承自基模板,如Example 3-9所示。
Example 3-9. templates/user.html: Simplified page template using template inheritance
{% extends "base.html" %}
{% block title %}Flasky{% endblock %}
{% block page_content %}
<div class="page-header">
<h1>Hello, {{ name }}!</h1>
</div>
{% endblock %}
链接
任何有一个以上路由的应用都需要包含链接来连接不同的网页,例如导航栏。对于简单的路由来说在模板里书写URLs作为链接并不重要,但是对于有可变部分的动态路由来说在模板里构建URLs会变得复杂。而且,直接书写URLs会导致不想要的对于代码里定义的路由的依赖。如果路由是可识别的,模板里的链接会破裂。为了避免这些问题,Flask提供了url_for()函数,它产生URLs自贮存于应用的URL map的信息。
最简单的使用,这些函数取view函数名(或者 endpoint名作为app.add_url_route()定义的路由)作为单一参数并返回它的URL。
例如,当前版本的hello.py调用 url_for('index')将返回/。而调用url_for('index', _external=True) 将返回绝对的URL,本例为http://localhost:5000/。
在应用中连接不同的路由使用相对URLs已经足够了。在网页浏览器外有必要使用绝到路径,例如通过email发送链接。动态的URLs可以通过给url_for()传递动态部分作为关键字参数来产生。例如,url_for('user', name='john', _external=True)将返回http://localhost:5000/user/john。
发送到url_for()的关键字参数不限于动态路由使用的参数。这个函数可以增加任意参数到查询字符串。例如url_for('index', page=2)将返回/?page=2。
静态文件
网络应用不只是由Python代码和模板构成。许多的应用也用静态文件如images, JavaScript源文件,自HTML代码引用的CSS。
你可能记得当第2章的hello.py 应用的URL被检查时,它里面有静态的入口。这是因为引用静态文件被看作特殊的路由,用/static/<filename>定义。例如,调用url_for('static', filename='css/styles.css', _external=True)将返回http://localhost:5000/static/css/styles.css。
在黙认配置里,Flask从位于应用根目录的static子目录查找静态文件。如果有必要可以将文件放在这个目录的子目录里。当服务器从前面的例子接收URL时,它产生响应包含static/css/styles.css的内容。
Example 3-10 展示应用如何在基模板中包含favicon.ico以在浏览器的地址栏中显示。
Example 3-10. templates/base.html: favicon definition
{% 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 %}
icon声明放在head块的后面。注意如何使用super()来保留基模板中定义的原始内容。
用Flask-Moment来本地化Dates和Times
在网络应用中处理日期和时间并不简单当用户在不同的地方工作当。服务器需要统一时间单位,它不依赖于用户的位置,所以通常使用Coordinated Universal Time (UTC)。然而,对于用户,看 UTC时间很困惑,因为用户总是想看当地的日期和时间并与当地的格式对应。一个优雅的方案是发送时间单位到浏览器,然后转换为当地时间再宣染。对于这种任务浏览器可以很好的工作因为它们可以访问时区并在用户的计算机里本地化。
有一个优秀的客户端开源 JavaScript库在浏览器里宣染日期和时间,称为moment.js。Flask-Moment是Flask应用集成moment.js到 Jinja2模板的扩展。用pip安装Flask-Moment。
(venv) $ pip install flask-moment
这个扩展的初始化见Example 3-11。
Example 3-11. hello.py: Initialize Flask-Moment
from flask.ext.moment import Moment
moment = Moment(app)
Flask-Moment依赖于jquery.js 和 moment.js。这两个库需要在HTML文档的适当地方放置,可以直接放置,也可以通过扩展的帮助函数放置。它可以从CDN引用这些库的测试版。因为Bootstrap已经包含jquery.js,只有moment.js需要添加。 Example 3-12展示如何在其模板的scripts里加载这些库。
Example 3-12. templates/base.html: Import moment.js library
{% block scripts %}
{{ super() }}
{{ moment.include_moment() }}
{% endblock %}
要使用timestamps,Flask-Moment产生moment类给模板使用。
Example 3-13传递变量称为current_time给模板进行宣染。
Example 3-13. hello.py: Add a datetime variable
from datetime import datetime
@app.route('/')
def index():
return render_template('index.html',
current_time=datetime.utcnow())
Example 3-14 展示current_time是如何在模板里宣染的。
Example 3-14. templates/index.html: Timestamp rendering with 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,” 但是刷新项会让它更新,所以你打开网页几分钟你会看到内容变为“a minute ago,” “2 minutes ago,” 等等。
Flask-Moment实施format(), fromNow(), fromTime(), calendar(),
valueOf(), 和 unix() 方法自moment.js。更多的格式选项请见官方文档。
Flask-Moment假定服务端处理的时间截是原生的UTC表示的datetime对象。更详细的内容请见 datetime包的官方文档。 Flask-Moment宣染的时间截可以本地化到许多语言。可以给lang()函数传递语言选项:
{{ moment.lang('es') }}
通过上面讨论的技术,你可以构建用户友好的现代网页。下一章将会讨论如何通过表单与用户交互。