返回介绍

模板

发布于 2025-04-20 18:52:14 字数 17912 浏览 0 评论 0 收藏

在之前的章节中,视图函数直接返回文本,而在实际生产环境中其实很少这样用,因为实际的页面大多是带有样式和复杂逻辑的 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&lt;div&gt;n&lt;/div&gt;!'
  • 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 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。