第三章 模板(Templates)
编写易于维护的程序的要点在于书写干净、良好结构的代码。你以前所见的代码都过于简单无法演示这一点。但Flask试图函数把两个完全独立的愿望混淆成一个,创建一个问题。
视图函数的任务就是为请求生成响应,就先前面第二章你看到的那样。对于简单请求来说足够了,但通常情况下,一个请求往往会触发程序状态的改变,视图函数也是改变发生地地方。
举例来说,一个用户在网站上注册一个新帐户。该用户在表单中输入一个email地址和密码,点击提交按钮。在服务器端,来自用户的含有数据的请求到达,Flask将请求分配给以注册的视图函数进行处理。这个试图函数需要与数据库进行交互:添加一个新帐号,生成操作的响应并发送给浏览器。这两类任务形式上分别被称为商业逻辑和表现逻辑。
如果将商业逻辑和表现逻辑混合起来,就会导致代码难以理解和维护。想象一下,你不得不使用HTML标记符号关联相关从数据库中取出的巨大的数据集,然后创建表格……把表现逻辑转移到模板当中,有助于改进程序的可维护性。
一个模板是一个包含响应文本的文件,他通过仅在请求上下文中可见的占位符变量来替换动态部分。用实际值替换占位变量并返回最终响应字符串,这一过程被称之为渲染(rendering)。Flask采用了被称之为Jinjia2的模板引擎来完成这一渲染过程。
JInjia2模板引擎
在最简单的表单中,JInjia2模板就是一个包含了一个相应字符串的文件。3-1示例展示了匹配2-1例子中index()视图函数的响应的模板
3-1 teplates/index.html:jinjia2 template
<h1>Hello World!</h1>
下面的就是2-2例子中动态返回由变量提供的用户名的视图函数,与之对应的模板形式。
3-2. templates/user.html: Jinja2 template
<h1>Hello, {{ name }}!</h1>
渲染模板
Flask默认在应用程序的tempplates子文件夹里查找模板文件。在下面版本的hello.py中,你需要把提前定义好的模板文件index.html和user.html保存在一个新建的templates文件夹中。
如例子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)
函数render_template是由Flask集成的扩展Jinja模板引擎提供的。它以模板的文件名作为自己的第一个参数,附加参数以键/值对的形式为在模板文件中引用的变量赋值。在本例中,第二个参数就是模板接收的name变量。
如果第一次使用,上例中的像name=name这样的键值对参数可能难以理解。左侧的name提供了参数名,也就是在模板中的占位符。右侧的name是当前范围的变量,它给同名的参数提供值。
变量
在例子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>
变量还可以通过filters(过滤器)进行修改,在变量名称后面添加竖线作为分割符。例如:下列模板代码展示了对name变量进行首字母大写:
Hello, {{name|capitalize}}
表格3-1列出了Jinja2中常用过滤器(filters)
Table 3-1. Jinja2 变量过滤器
| 过滤器名 | 描述 |
| safe | 返回未经转换的值 |
| capitalize | 将值的首个字符大写其他小写 |
| lower | 将值转换为小写字符 |
| upper | 将值转换成大写字符 |
| title | 值得每个单词都首字母大写 |
| strim | 从值中去掉头尾的空白字符 |
| striptags | 在渲染前从值中去除所有的html标记|
safe过滤器是值得注意的一个。出于安全考虑,Jinja2会默认对所有变量进行转码(escapes)。例如:如果一个变量的值被设置为'<h1>Hello</h1>'
,Jinja2将会把这个字符串渲染成 '<h1>Hello</h1>'
,这将把h1元素标记显示成普通字符,而不是被浏览器解释后显示为html格式。大多数时候,如果需要把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页支持"宏",你可以使用Python代码实现类似上例的功能,例如:
{% macro render_comment(comment) %}
<li>{{ comment }}</li>
{% endmacro %}
<ul>
{% for comment in comments %}
{{ render_comment(comment) }}
{% endfor %}
</ul>
为了尽可能的复用宏,你可以把它存储在独立文件中,在需要的时候再导入(imported)到模板中。
{% import 'macros.html' as macros %}
<ul>
{% for comment in comments %}
{{ macros.render_comment(comment) }}
{% endfor %}
</ul>
需要在多处重复使用的模板代码可以部件化后保存到独立文件中,需要的时候使用再包括进来(included):
{% 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派生而来。接下来在指定位置重新定义了基础模板中的三个块。注意,对于head块的新定义,原来基础模板已经有这一定义了,用了super()来更改原始内容(父类)。
稍后,在实际使用中,本节的所有控制结构都有展示,你有机会观察它们是如何工作的。
使用Flask-Bootstrap集成Twitter Bootstrap
Bootstrap是Twitter提供的一个开源框架,通过它提供的用户接口部件我们可以创建干净、引人注目的网页,且被所有现代浏览器兼容。
Bootstrap是个客户端框架,所以在服务器端并不直接涉及它。服务器只是提供一个html响应,该响应包含了bootstrap的层叠样式表、javascript并通过html,css和js代码实例化各个部件。完成这一切的的理想之处就是模板。
把Bootstrap集成进程序的最常见做法是在模板里完成所有更改。最简单的一个方案就是使用Flask扩展——Flask-Bootstrap。你可以通过pip安装它:
(venv)$pip install flask-bootstrap
Flask扩展实例一般是随程序实例创建而同时初始化的。示例3-4展示了Flask-Bootstrap的初始化:
from flask.ext.bootstrap import Bootstrap
# ...
bootstrap = Bootstrap(app)
就像在第2章中的Flask-Script扩展一样,从Flask.ext命名空间中导入flask-bootstrap,然后将程序实例传递给它的构造函数从而完成初始化。
一旦初始化完成,程序就可以使用包含所有Bootstrap文件的基础模板。这个模板利用Jinja2模板的继承,扩展包含了从导入的bootstrap元素生成的基本页面结构。例子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扩展从flask-Bootstrap引用Bootstrap/base.html实现模板继承。Flask-Bootstrap的基础模板提供了包含所有bootstrap的css和js文件的网页骨架。
基础模板定义了可以被继承模板子类覆盖的“块”。block和endblock指令定义的内容将被添加到基础模板中。
上面的user.html模板定义了三个块,title,navar,content。他们有基础模板定义,并供继承模板重写、重定义。title块中的内容江出现在渲染后的html文档的title标记中。navbar和content块 提供了页面导航和主体部分的内容。
在这个模板中,navbar块利用bootstrap部件定义了一个简单的导航栏。content块包含<div>标记的页头部分。老版本的问候语现在放到页头(page header)部分了。图3-1展示了程序外观的变化。
Flask-Bootstrap的base.html还定义了其他一些可以被派生模板使用的块,表格3-2是可用块的完整列表:
块名 说明
doc 整个HTML文档
html_attribs <html> 标记的属性
html <html>标记内的内容
head <head>标记内的内容
title <title>标记内的内容
metas <meta>标记内的列表项
styles 层叠样式表定义
body_attribs <body>标记的属性
body <body>标记内的内容
navbar 自定义导航栏
content 自定义页面内容
scripts 文档底部的JavaScript 声明
上表中很多块是 Flask-bootstrap自己使用的,所以如果直接覆盖它们将导致报错。例如:styles和scripts两个块声明了bootstrap文件位置,如果程序需要向已存在的内容中添加新内容,必须使用Jinja2的super()函数。下面的例子显示了在派生模板中如何重写scripts块来添加新的js文件:
{% block scripts %}
{{ super() }}
<script type="text/javascript" src="my-script.js"></script>
{% endblock %}
定制错误页面
当浏览器试图访问无效地址时,你就会看到一个代码为404的错误页面目前这个错误页面太干净了,不吸引人。并且样式跟其他使用了bootstrap的页面不一致(根本没有用上)。
Flask允许程序自己定义基于基础模板的错误页面,就像常规路由一样。有两个最常见的错误代码分别是404和500:404会在客户端请求一个不存在的页面或路由是触发;500错误则是出现了未被捕捉处理的错误是触发。例子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
错误处理器就像视图函数一样返回一个响应。同时也返回了与错误代码一致的数字代码。
错误处理器引用的模板还没有编写。这两个模板应该跟其他常规页面布局一样,所以在此例中同样有导航栏和显示错误信息的页头。
你可以直接了当的复制一下templates/user.html
重命名为templates/404.html
和 templates/500.html
。然后修改其中的page header元素以显示错误信息。但这样还会产生很多重复。
Jinja2的模板继承能够解决这一点。同样,Flask-bootstrap提供了一个带有基本页面布局的基础模板,程序可以定义自己更完成布局的基础模板,包含导航栏并留下页面内容供派生类自定义。例子3-7展示了templates/base.html
一个新的继承自bootstrap/base.html
的带有导航栏的基础模板,这样templates/user.html,templates/404,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的base.html,而是从这个模板继承。例子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 %}
图3-2显示在浏览器中的404页面
templates/user.html
模板现在也可以从这个基础模板来继承,简单修改如下例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 %}
连接
任何一个程序都不止一个路由,所以需要像在导航栏中那样包含不同连接来在不同页面间跳转。
直接在模板中硬编码URL连接,对简单路由来说太繁琐了,而对带变量参数的动态路由来说,要想写对更为困难。并且直接写url会在路由定义代码中创建一个不希望的依赖。如果对路由进行了更改,模板中的连接就会失效。
为了避免这些问题,Flask提供了一个url_for()辅助函数,可以借此根据url映射中的url信息来生成连接。
最简单的用法,该函数获取视图函数名称 (或者是app.add_url_route()定义的路由结束点(endpoint)名称) 作为其唯一参数并返回其url。例如:在当前版本的hello.py中,调用url_for('index')
将返回 /
。调用url_for('index',_external=True)
则将返回一个绝对路径,那就是http://localhost:5000
提醒
通常在程序不同路由间使用相对URl就可以,而对需要通过浏览器进行外部调用连接必须使用绝对URL路径来生成,如通过email发送的连接。
动态URL可以通过给url_for函数传递一个动态键值对作为参数来创建。如:url_for('user',name='john',_external=True)
将返回http://localhost:5000/user/john
传递给url_for()的键值对参数无需局限于动态路由使用的参数。这个函数可以接受任意的扩展参数作为查询子符串。如:url_for('index',page=2)
将返回/?page=2
。
静态文件
web应用不仅仅有python代码和模板构成,大多数程序还要在html中用到一些静态文件,诸如图片,js脚本和css样式表等。
你可以再次调出第二章的hello.py程序的URL映射进行观察。其中有一个叫做static的条目。这是因为对静态文件的引用被当作一个特殊的路由定义 /static/<filename>
。如:url_for('static',fielname='css/styles.css',_external=True)
将返回 http://localhost:5000/static/css/styles.css
。
在默认配置中,Flask从程序根文件夹下的名为static的子文件夹中查找静态文件。如果需要,还可以在此文件夹下再新建文件夹中存放静态文件。在上例中服务器接受到url后将生成包含static/css/styles.css
文件内容的一个响应。
例子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 %}
图标引用声明被插入在head块的末尾。注意,此处使用super()来修改基础模板中定义的原始内容。
使用Flask-Moment本地化时间和日期
当你的用户遍及全球时,在web应用中处理日期和时间就不是一件小事情了。
服务器需要把各个独立不同的地域用户的时间统一规范,所以一般使用具有代表性的协调世界时(UTC)。但对用户来说,要是看见的都用UTC格式的时间就会懵圈啦——用户希望看到以自己所在国家/地域格式化的本地日期和时间。
简洁的解决方案就是:允许服务器仅以UTC格式工作并发送给浏览器,由浏览器把他们转换成本地时间和日期。因为浏览器能访问本地时区和用户的计算机设置,所以可以很漂亮的完成这个活。
开源的客户端js脚本库moment.js就是干这个的。而Flask-Moment扩展可以轻松将它集成到Jinja2模板中。你可以通过pip安装:
(venv)$pip install flask-moment
例子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添加进来就行了。例子3-12展示了在基础模板中通过script加载这个库。
Example 3-12. templates/base.html: Import moment.js library
{% block scripts %}
{{ super() }}
{{ moment.include_moment() }}
{% endblock %}
在模板中你可以访问moment类,以便于处理时间戳(timestamp)数据。例子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())
例子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'详细程度不同(different levels of verbosity?)。format()函数还可以自定义格式化参数。
fromNow()函数渲染结果显示在第二行,它显示一个相对化的时间,并根据时间刷新。初始显示为"刚刚(a few seconds aog)",刷新功能将保持它随时间更新,所以如果你离开页面几分钟后在回来,你将看到文本就变成了"一会前(a minute ago)",或者 2minutes ago之类的。
Flask_Moment实现了format(),fromNow(),fromTime(),calendar(),valueOf()和unix()这几个moment.js方法。你可以从moment.js的文档中学习更多的格式化选项。
提醒
Flask-Moment 假设服务器端处理的时间戳是不包含时区信息的 naive datetime对象。你可以查看标准库datetime中关于naive和waare两种日期时间对象的文档。
通过Flask-Moment可以将时间戳本地化渲染成多种语言格式。可以在模板中将语言代码传递给lang()函数来选择一种语言:
{{moment.lang('es')}}
通过学习本章设计的知识,你可以创建一个现代化的用户友好的web页面了。在下一章我们将深入研究模板:如何通过表单与用户交互。
<<第二章 基本程序结构 第四章 Web表单>>