模板
在之前的章节中,视图函数直接返回文本,而在实际生产环境中其实很少这样用,因为实际的页面大多是带有样式和复杂逻辑的 HTML 代码,这可以让浏览器渲染出非常漂亮和复杂的效果。页面内容应该是可重用的,而且需要执行更高级的功能。首先看一下 Python 自带的模板 string.Template:
In:from string import Template In:s=Template('$who likes$what') In:s.substitute(who='tim', what='kung pao') #按照给定的参数插入到对应的变量上 Out:'tim likes kung pao' In:s=Template("$var is here but$missing is not provided") In:s.safe_substitute(var='tim') Out:'tim is here but$missing is not provided' In:class MyTemplate(Template): ...: delimiter='@' #使用 @为分隔符 ...: idpattern='[a-z]+\.[a-z]+' #符合的模式才会被替换 ...: In:t=MyTemplate('@with.dot@notdoted') In:t.safe_substitute({'with.dot':'replaced', 'notdoted':'not replaced'}) Out:'replaced@notdoted'
自带的模板提供的功能大抵如此,支持很有限:不能写控制语句,无法继承重用。这对于 Web 开发来说远远不够,需要使用第三方的模板系统。目前市面上有非常多的模板系统,其中最知名的就是 Jinja2 和 Mako。本节我们将分别介绍它们。
Jinja2
Jinja 是日本寺庙的意思,并且寺庙的英文 temple 和 template 的发音类似。Jinja2 是 Flask 默认的仿 Django 模板的一个模板引擎,由 Flask 的作者开发。它速度快,被广泛使用,并且提供了可选的沙箱模板来保证执行环境的安全。它有如下优点:
- 让 HTML 设计者和后端 Python 开发工作分离。
- 减少使用 Python 的复杂程度,页面逻辑应该独立于业务逻辑,这样才能开发出易于维护的程序。
- 模板非常灵活、快速和安全,对设计者和开发者会更友好。
- 提供了控制语句、继承等高级功能,减少开发的复杂度。
Jinja2 是 Flask 的一个依赖,因为我们之前已经安装了 Flask,所以 Jinja2 也随之安装了。否则可以单独安装:
> pip install Jinja2
Jinja2 从 2.7 开始已经依赖 MarkupSafe 了,MarkupSafe 的 C 实现要快得多,使用 pip 安装 Jinja2 时会自动安装它。如果不方便升级到新版本的 Jinja2,但当前版本大于 2.5.1,可手动安装 MarkupSafe。
API 的基本使用方式
Jinja2 通过 Template 类创建并渲染模板:
In:from jinja2 import Template In:template=Template('Hello{{name}}!') In:template.render(name='Xiao Ming') Out:u'Hello Xiao Ming!'
是不是和 string.Template 做的事情很像呢?上面的代码片段背后的逻辑大致是这样的:
In:from jinja2 import Environment In:env=Environment() In:template=env.from_string('Hello{{name}}!') In:template.render(name='Xiao Ming') Out:u'Hello Xiao Ming!'
Environment 的实例用于存储配置和全局对象,然后从文件系统或其他位置加载模板:
> echo"Hello{{name}}">templates/chapter3/section2/jinja2/hello.html > touch app.py > ipython In:from jinja2 import Environment, PackageLoader In:env=Environment(loader=PackageLoader('app', 'templates/chapter3/section2/jinja2 ')) In:template=env.get_template('hello.html') In:template.render(name='Xiao Ming') Out:u'Hello Xiao Ming'
通过 Environment 创建了一个模板环境,模板加载器(loader)会在 templates 文件夹中寻找模板。因为这里是测试,所以只是用到 app.py 这个空文件作为包名。由于模板文件在模板目录的子目录下,也可以这样获取:
env=Environment(loader=PackageLoader('app', 'templates')) template=env.get_template('chapter3/section2/jinja2/hello.html')
使用模板加载器的另一个明显的好处就是可以支持模板继承。
Jinja2 的基本语法
模板仅仅是文本文件,它可以使用任何基于文本的格式(HTML、XML、CSV、LaTex 等),它并没有特定的扩展名,通常使用.html 作为后缀名。模板包含“变量”或“表达式”,这两者在模板求值的时候会被替换为值。模板中还有标签和控制语句。
下面是一个简单的模板(simple.html):
1 <!DOCTYPE html> 2 <html lang="en"> 3 <head> 4 <title>Simple Page</title> 5 </head> 6 <body> 7 {# This is a Comment #} 8 <ul id="navigation"> 9 {%for item in items%} 10 <li><a href="{{item.href}}">{{item['caption']}}</a></li> 11 {%endfor%} 12 </ul> 13 14 <h1>{{title|trim}}</h1> 15 <p>{{content}}</p> 16 17 </body> 18 </html>
我们来解析下这个模板的语法。
- 第 1 行,声明文档类型是 HTML 5。
- 第 7、9、10 行,这是三种分隔符,每种分隔符都包含开始标记和结束标记。
- {# ... #}:模板注释。它不会出现在渲染的页面里。
- {%...%}:用于执行诸如 for 循环或赋值的语句。
- {{...}}:用于把表达式的结果输出到模板上。
- 第 9 行,出现了“for 循环”这种控制结构。语法是{%for X in Y%}...{%endfor%},控制语句都需要以 endxxx 作为结束。
- 第 10 行,应用把变量传递到模板,可以使用点(.)来访问变量的属性,也可以使用中括号语法([])。下面的两行效果几乎是一样的:
{{item.href}} {{item['href']}}
- 第 14 行,trim 是一个过滤器,在模板中通过管道符号(|)把变量和过滤器分开。也可以使用多个过滤器,如{{title|trim|striptags}},striptags 也是一个过滤器。Jinja2 内置了非常多的过滤器,全部过滤器可以在 http://bit.ly/29RZ1fK 找到,一定要熟悉这些过滤器,它们大多都很常用。
模板继承
合理使用模板继承,让模板能重用,能提高工作效率和代码质量。
首先定义一个基础的“骨架”模板(base.html):
<!DOCTYPE html> <html lang="en"> <head> {%block head%} <link rel="stylesheet"href="style.css"/> <title>{%block title%}{%endblock%}-My Webpage</title> {%endblock%} </head> <body> <div id="content"> {%block content%} {%endblock%} </div> <div id="footer"> {%block footer%} {%endblock%} </div> </body> </html>
这个模板有如下细节:
- “{%block XXX%}...{%endblock%}”是一个代码块,可以在子模板重载。
- head 的代码块有默认内容,而 content 和 footer 都是没有内容的。这 3 个块需要在子模板中被重载,如果子模板没有重载,就用这个基类模板的定义显示默认内容。
接着看子模板(index.html):
{%extends"base.html"%} {%block title%}Index{%endblock%} {%block head%} {{super()}} <style type="text/css"> .important{color:#336699;} </style> {%endblock%} {%block content%} <h1>Index</h1> <p class="important"> Welcome on my awesome homepage. </p> {%endblock content%}
子模板有如下细节:
- index.html 继承了 base.html 里面的内容。extends 标签应该在模板中一开始就使用。
- 标题被重载,换成了“Index”。
- head 块被重载。但是首先使用了 super(),表示先使用 base.html 的 head 块的内容,再基于此添加 CSS 样式。
- content 块被重载,其中在块的结束标签中加入了名称,这样可以改善模板的可读性。
- footer 没有被重载,什么都不显示。
如果你想要多次使用一个块,可以使用特殊的“self”变量并调用与块同名的函数:
<title>{%block title%}{%endblock%}</title> <h1>{{self.title()}}</h1>
宏
宏类似常规编程语言中的函数。它用于把常用行为抽象成可重用的函数:
In:Template(''' ...:{%macro hello(name)%} ...:Hello{{name}} ...:{%endmacro%} ...:<p>{{hello('world')}}</p> ...:''').render() Out:u'\n\n<p>\n Hello world\n</p>'
可以像函数一样调用宏。
赋值
在代码块中,你也可以为变量赋值。赋值使用 set 标签,并且可以为多个变量赋值:
In:print Template(''' ...:{%set a=1%} ...:{%set b, c=range(2)%} ...:<p>{{a}}{{b}}{{c}}</p> ...:''').render() Out:u'\n\n\n<p>1 0 1</p>'
include
include 语句用于包含一个模板,渲染时会在 include 语句的对应位置添加被包含的模板内容:
{% include 'header.html'%} Body {% include 'footer.html'%}
include 可以使用“ignore missing”标记,如果模板不存在,Jinja 会忽略这条语句:
{% include"sidebar.html"ignore missing%}
import
Jinja2 支持在不同的模板中导入宏并使用,与 Python 中的 import 语句类似。有两种方式来导入模板:可以把整个模板导入到一个变量(import xx)或从其中导入特定的宏(from xx import yy)。
现在有一个宏模板(macro.html):
{%macro hello(name)%} Hello{{name}} {%endmacro%} {%macro strftime(time, fmt='%Y-%m-%d%H:%M:%S')%} {{time.strftime(fmt)}} {%endmacro%}
引用并调用宏的方法如下(hello_macro.html):
{%import 'macro.html' as macro%} {%from 'macro.html' import hello as_hello, strftime%} <p>{{macro.hello('world')}}</p> <p>{{strftime(time)}}</p>
看一下渲染的效果:
In:from jinja2 import FileSystemLoader, Environment In:from datetime import datetime In:loader=FileSystemLoader('/home/ubuntu/web_develop/templates/chapter3/section2/ jinja2') In:template=Environment(loader=loader).get_template('hello_macro.html') In:print template.render(time=datetime.now()) <p> Hello world </p> <p> 2016-05-27 04:37:11 </p>
Mako
Mako 是另一个知名模板语言。它从 Django、Jinja2、Genshi 等模板借鉴了很多语法和 API。它有如下优点:
- 性能和 Jinja2 相近,这一点 Jinja2 也承认(http://bit.ly/28RvFiM )。
- 有大型网站在使用,有质量保证。Reddit 在 2011 年的月 PV 就达到 10 亿,豆瓣几乎全部用户产品都使用 Mako 模板,所以不需要担心没有大公司使用的案例。
- 有知名 Web 框架支持。Pylons 和 Pyramid 这两个 Web 框架内置 Mako,而且把它作为默认模板。
- 支持在模板中写几乎原生的 Python 语法的代码,对 Python 工程师友好。
- 自带完整的缓存系统。Mako 提供非常好的扩展接口,很容易切换成其他的缓存系统。
Jinja2 和 Mako 的设计哲学有一点不同:Jinja2 认为应该尽可能把逻辑从模板中移除,界限清晰,不允许在模板内写 Python 代码,也不支持全部的 Python 内置函数(只提供了很有限、最常用的一部分);而 Mako 正好相反,它最后会编译成 Python 代码以达到性能最优,在模板里面可以自由写后端逻辑,不需要传递就可以使用 Python 自带的数据结构和内置类。Jinja2 带来的好处是模板引擎易于维护,并且模板有更好的可读性;而 Mako 是一个对 Python 工程师非常友好的语言,限制很少,完成模板开发工作时更有效率,整个项目的代码可维护性更好。
我们先安装它:
> pip install Mako
基本 API 的使用
Mako 也可以通过 Template 类创建一个模板实例并渲染它:
In:from mako.template import Template In:template=Template('Hello${name}!') In:template.render(name='Xiao Ming') Out:u'Hello Xiao Ming!'
Mako 的变量使用了“${...}”的风格。
模板文件后缀不强制以“.mako”结尾,使用“.html”甚至“.txt”都是可以接受的。
In:Template(filename='templates/chapter3/section2/mako/hello.mako').render(name=' XiaoMing') Out:u'Hello XiaoMing\n'
可以优化一下性能,保存编译后的模板,下次有参数相同的调用就能使用缓存的结果:
In:Template(filename='templates/chapter3/section2/mako/hello.mako', module_directory ='/tmp/mako_cache').render(name='XiaoMing') Out:u'Hello XiaoMing\n'
看一下生成的 Mako 文件:
> tree/tmp/mako_cache /tmp/mako_cache └──templates └──chapter3 └──section2 └──mako ├──hello.mako.py └──hello.mako.pyc
缓存的文件是按照模板路径的结构保存的。
使用 TemplateLookup
上面的例子只是单个模板文件的渲染。试想下如果有模板继承,或者模板引用了其他模板,如何告诉 Mako 模板的搜索路径都有哪些呢?TemplateLookup 就是做这件事的:
In:from mako.lookup import TemplateLookup In:mylookup=TemplateLookup(directories=['templates/chapter3/section2/mako']) In:template=Template('<%include file="hello.mako"/>', lookup=mylookup) In:template.render(name='XiaoMing') Out:u'Hello XiaoMing\n'
和 Jinja2 的用法更相近的方式是:
In:mylookup.get_template('hello.mako').render(name='XiaoMing') Out:u'Hello XiaoMing\n'
TemplateLookup 也支持 module_directory 参数:
mylookup=TemplateLookup(directories=['templates/chapter3/section2/mako'], module_directory='/tmp/mako_modules')
渲染模板还能使用相对路径:
In:mylookup.get_template('/hello.mako').render(name='XiaoMing') Out:u'Hello XiaoMing\n'
模板名字使用“/”开头,这是因为搜索目录包含了 templates/chapter3/section2/mako,这个“/”只是表示相对的路径。
Mako 的基本语法
下面是一个简单的模板(simple.html):
1 <%! 2 from datetime import datetime 3 from itertools import repeat 4 %> 5 6 <%inherit file="base.html"/> 7 <%namespace name="utils"file="/utils.html"/> 8 9 <% 10 rows=repeat(range(10), 10) 11 %> 12 13 <%include file="/nav.html"/> 14 15 16 <%def name="main()"> 17 ## this is a comment 18 <h2>${utils.strftime(datetime.now())}</h2> 19 20 <table> 21 %for row in rows: 22 ${makerow(row)} 23 %endfor 24 </table> 25 </%def> 26 27 <%def name="makerow(row)"> 28 <tr> 29 %for name in row: 30 <td>${name}</td> 31 %endfor 32 </tr> 33 </%def>
此模板有如下细节:
- 第 1~4 行,“<%! ...%>”是模块级别的块元素,常用来引入模块、声明全局变量和全局函数等。
- 第 6 行,表示这个模板继承了 base.html。
- 第 7 行,表示把/utils.html 当作一个模块一样声明,使用“utils”这个命令空间。之后可以直接调用 utils 里面的函数、变量等。
- 第 9~11 行,“<%...%>”表示 Python 代码块,在里面可以自由地写 Python 代码,但是它不是全局的,建议在函数内使用,保证在调用模板函数的时候可以访问这些定义的代码。
- 第 13 行,表示直接把“/nav.html”这个模板的内容插入到当前位置。
- 第 16、27 行,“<%def name=”main()”>”和“<%def name=”makerow(row)”>”是两个函数。区别是 makerow 需要传入参数。
- 第 17 行,以“##”开头的行表示一个注释。如果有多行注释,也可以使用“<%doc>...</%doc>”的方式。
- 第 18 行,“${...}”中要执行的可以是模板中的函数,也可以是 Python 代码,执行结果就直接输出到 HTML 页面上了。
- 第 21 行,使用“%”标签就可以直接写 Python 语法的控制语句了。“%for row in rows:”是一个 for 循环语句,它用 Python 代码风格生成 HTML 代码。
<%page>
上面例子中的“<%include ...>”并没有带参数,nav.html 里面只要用“<%page/>”即可,当需要传输参数的时候这样使用:
<%page args="x, y, z='default'"/>
插入模板的语法如下:
<%namespace name="utils"file="/utils.html"args="1, 2, z='z'"/>
“<%page>”还可以指定缓存方式:
<%page cached="True"cache_type="memory"/>
<%block>
“%block”和“%def”很像,它受 Jinja2 的 block 启发,在定义的地方被渲染,无须像“%def”那样当需要调用时才会被渲染。“%block”也可以接收缓存、过滤器的参数:
<html> <body> <%block cached="True"cache_timeout="60"> This content will be cached for 60 seconds. </%block> </body> </html>
我们也可以给块加个名字以便重复调用:
<div name="page"> <%block name="pagecontrol"> <a href="">previous page</a>| <a href="">next page</a> </%block> <table> ## some content </table> ${pagecontrol()} </div>
pagecontrol 共渲染了两次。
<%namespace>
“<%namespace>”的作用很像 Python 的 import,可以把其他模板当成 Python 模块一样引用进来:
<%namespace file="/utils.html"import="strftime"/>
这样就可以直接使用 strftime 了:
<h2>${strftime(datetime.now())}</h2>
import 支持“*”操作符(可能会影响性能,建议采用显式的 import):
<%namespace file="/utils.html"import="*"/>
file 参数还可以接收表达式,动态地传入文件名:
<%namespace name="dyn"file="${context['namespace_name']}"/>
除了上面通过“utils.strftime”的方式调用,还可用以下两种方式调用:
<%utils:strftime args='${datetime.now()}'/> <%call expr='utils.strftime()' args='${datetime.now()}'></%call>
过滤器
Mako 模板中同样使用管道符号(|)把过滤器和变量分隔开,但需要注意的是,多个过滤器是用逗号隔开的:
${"this is some text"|u} ${"<tag>some value</tag>"|h,trim}
Mako 中的常用过滤器包含如下 4 种。
- u:URL 的转换,等价于“urllib.quote_plus(string.encode('utf-8'))”。
In:Template('Hello${name|u}!').render(name=u'中文') Out:u'Hello%E4%B8%AD%E6%96%87!'
- h:HTML 转换,等价于“markupsafe.escape(string)”。如果没有安装 markupsafe 模块的话,等价于“cgi.escape(string, True)”。
In:Template('Hello${name|h}!').render(name='<div>n</div>') Out:u'Hello<div>n</div>!'
- trim:过滤行首和行尾的空格,实际上是“string.strip()”。
In:Template('Hello${name|trim}!').render(name=' a ') Out:u'Hello a!'
- n:禁用默认的过滤器。
在 Mako 中定义一个过滤器非常简单(my_filters.html):
<%! def div(text): return"<div>"+text+"</div>" %> Here's a div:${"text"|div}
函数 div 需要输入一个参数,参数就是过滤之前的文本,也就是文本:”text”。
可以使用 default_filters 参数指定全局的设置。如果不指定,在 Python 2 中,默认设置是“['unicode']”,在 Python 3 中是“['str']”:
mylookup=TemplateLookup(directories=['templates/chapter3/section2/mako'], default_filters=['unicode', 'h', 'decode.utf8'])
这样就不需要在页面指定“expression_filter='h'”了。如果想要全局开启自定义的过滤器,需要使用如下方式:
mylookup=TemplateLookup( directories=['templates/chapter3/section2/mako'], default_filters=['str', 'myfilter'], imports=['from mypackage import myfilter'])
Mako 模板编译完成后会生成这样的的代码(截取相关的一部分):
from mypackage import myfilter def render_body(context,**pageargs): name=context.get('name', UNDEFINED) __M_writer=context.writer() __M_writer(u'Hello ') __M_writer(myfilter(str(name))) __M_writer(u'\n')
render_body 是每个 Mako 模板编译完成之后生成的 Python 代码的主函数,渲染的时候就是通过调用它生成 HTML 代码的。过滤器的执行顺序和当时指定的顺序是一样的。
模板继承
我们来使用 Mako 的模板继承,生成和 Jinja2 一样的 HTML 页面。首先看一下骨架模板(base.html):
<!DOCTYPE HTML> <html lang="en"> <head> <%block name="head"> <link rel="stylesheet"href="style.css"/> <title>${title()}-My Webpage</title> </%block> </head> <body> <div id="content"> <%block name="content"/> </div> <div id="footer"> <%block name="footer"/> </div> </body> </html> <%def name="title()"> </%def>
和 Jinja2 的模板相比,只是语法不同。再看一下子模板(index.html):
<%inherit file="base.html"/> <%def name="title()">Index</%def> <%block name="head"> ${parent.head()} <style type="text/css"> .important{color:#336699;} </style> </%block> <%block name="content"> <h1>Index</h1> <p class="important"> Welcome on my awesome homepage. </p> </%block>
title 当然也可以使用“%block”来实现。模板中也能多次调用同一个函数,使用“self.<defname>”的方式,比如 title 可以使用“${self.title()}”。
Mako 排错
Mako 有一个让人非常迷惑的地方,即出现 NameError(NameError:Undefined),尤其在使用了模板多继承的情况下。举个例子,不小心在模板中使用了一个没有被定义的变量,渲染页面就会失败,出现类似如下的错误:
... File"/home/ubuntu/r/venv/lib/python2.7/site-packages/mako/runtime.py", line 892, in _exec_template callable_(context,*args,**kwargs) File"activity_index_html", line 224, in render_body File"_activity_widgets_widget_html", line 1373, in render_widget File"_activity_widgets_filmawards_html", line 117, in render_main File"/home/ubuntu/r/venv/lib/python2.7/site-packages/mako/filters.py", line 78, in decode return decode(str(x)) File"/home/ubuntu/r/venv/lib/python2.7/site-packages/mako/runtime.py", line 226, in __str__ raise NameError("Undefined") NameError:Undefined
这个例子跨了多个模板文件,_activity_widgets_widget_html、_activity_widgets_filmawards_html 这样的模板并不是真实存在的模板文件,它们都是由 Mako 编译而成的临时结果,即便使用 werkzeug.debug.DebuggedApplication 也无法定位报错的行数。
这个时候可以找到 Mako 的源码中的 template.py(https://github.com/zzzeek/mako/blob/master/mako/template.py ),在_compile(https://github.com/zzzeek/mako/blob/master/mako/template.py# L651 )这个函数结尾处添加两行打印输出:
def_compile(template, text, filename, generate_magic_comment): ... for index, s in enumerate(source.splitlines()): print index, s return source, lexer
这样,在终端就可以看到编译后的模板的内容,从而定位错误原因了。
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论