X
首页 Cheetah技术专栏 使用 Python 和 Cheetah 构建和扩充模板(2)
使用 Python 和 Cheetah 构建和扩充模板(2)

Cheetah 简介

Cheetah 有一个很长的家谱。它从称为 Velocity 的 Java™ 模板系统那里获得了灵感,Velocity 是 Webmacro 模板系统的一个改进版本,试图在 JavaServer Pages 上进行改进。Cheetah 提供了一门简单语言,用来定义提供基本流控制和对象访问构造的模板。它借用了 Velocity 的基本模板语法,但添加了一些特性,为 Cheetah 模板提供对 Python 的便利构造的访问。

以下是一些 Cheetah 代码,这些代码解释了模板定义的“简单”部分 —— 没有流控制的那一部分,Python 的内置模板系统可以处理它:


清单 6. 第一个 Cheetah 示例
from Cheetah.Template import Template
from DummyObjects import dummyUser, dummyOrder
definition = """Hello, $user.firstName.
Your order (#$order.id) has shipped:"""
print Template(definition, searchList=[{'user' : dummyUser,
                                        'order' : dummyOrder}])
 

definition 字符串包含模板定义(电子邮件的静态部分),它可以对外部变量(动态部分)进行引用。Template 构造函数在这里用来将模板定义绑定到名称空间 的 searchList:查找对象的方式对应于定义中使用的变量。例如,模板定义中的 $user 在这里映射到 dummyUser 变量。您还可以提前运行 Template 构造函数,并在准备使用特定对


清单 7. 使用 #for 指令在列表上进行迭代
definition = """Hello, $user.firstName.
Your order (#$order.id) has shipped:
#for $purchased, $quantity in $order.purchased.items():
 $purchased.name: $quantity unit(s)
#end for
Your tracking number is $order.trackingNumber."""
print Template(definition, searchList=[{'user' : dummyUser,
                   'order' : dummyOrder}])
                   
此代码与以前的代码完全相同,但模板定义是不同的。#for 指令启动了一个循环,而 #end for 指令结束了这个循环。因为可以用 Cheetah 生成空白空间很重要的文本(比如电子邮件消息),所以 Cheetah 无法像 Python 所做的那样,将空白空间用作流控制机制,因此它要使用 #end 指令。

#for 迭代的作用类似于使用 Python 的 for 关键字的 Python 迭代。此迭代的作用完全类似于上面所示的手工编写的 Python 迭代:

for purchased, quantity in order.items():
     l.append(purchased.name)
     ...

在手工编写的 Python 代码中,必须将每个输出项手工添加到输出字符串的列表 l 中。Cheetah 使得下列事情变得更轻松:它评估 #for 循环中的代码,并自动将每个迭代的输出附加到模板的输出中。

Cheetah 还提供了一个 #while/#end while 指令,该指令等同于 Python 的 while 构造。

流控制:#if 指令

您已经创建了一个重新生成上述电子邮件的 Cheetah 模板。现在,让我们对它稍微做一下改进。这封电子邮件说您订购了“1 单位”的蓝色装饰品。如果您只订购一件东西或者订购“x 单位”的其他东西,那么更改模板使其说出“1 单位”应该不是很困难。Cheetah 提供了一个 #if 指令,该指令允许您设置 if-then-else 条件。以下是一个 Cheetah 的模板,它试图正确处理复数。Python 代码总是相同的,因此,以下代码只显示了新的模板定义:

清单 8. 使用 #if 指令处理复数
Hello, $user.firstName.
Your order (#$order.id) has shipped:
#for $purchased, $quantity in $order.purchased.items():
 $purchased.name: $quantity unit
#if $quantity != 1
s
#end if
#end for
 
使用这个模板定义的惟一问题是,Cheetah 在单位 中的一个单独行上打印 s:

 Widget, green: 50 unit
s
 Widget, blue: 1 unit
 
可以用两种方法避免这种尴尬的现象:禁止这种令人讨厌的新行,或者提前设置适当的字符串作为变量。

使用 #slurp 指令禁止出现这种新行

Cheetah 的指令 #slurp 告诉 Cheetah 不要在特殊行的结尾处打印新行:


清单 9. 禁止在行的结尾处出现新行
#for $purchased, $quantity in $order.purchased.items():
 $purchased.name: $quantity unit#slurp
#if $quantity != 1
s
#end if
#end for
 
此代码提供了您想要的输出:

 Widget, green: 50 units
 Widget, blue: 1 unit
 

使用 #set 指令设置变量

如果 #slurp 指令对您来说不好用,那么可以使用另一种方法。可以使用 #set 指令在 Cheetah 模板的作用域内创建一个临时变量:


清单 10. 使用 #set 创建一个临时变量
#for $purchased, $quantity in $order.purchased.items():
 #if $quantity == 1
  #set $units = 'unit'
 #else
  #set $units = 'units'
 #end if
 $purchased.name: $quantity $units
#end for
 

在这种情况下,#set 允许您将条件从文本生成中移出,移动到定义变量的代码中。#set 通常是一个有用的指令。您还可以使用它来创建中间值,或者用它来避免多次调用某一昂贵的方法。


生成其他类型的文件

为了简便起见,迄今为止的所有示例生成的都是纯文本的电子邮件,但您不必拘泥于此。Cheetah 最初被设计为生成 HTML 文本,但您可以用它生成任何基于文本的格式:XML、SQL,甚至是 Python 或其他编程语言代码。

以下是一个 Cheetah 模板,它提供了一个呈现定单状态页的 HTML。可以将这个 HTML 转变成称为 OrderStatus.tmpl 的文件。注意,它与相同信息的电子邮件转换的基本相似性:


清单 11. 呈现定单状态信息的 HTML
<html>
<head><title>Status for order #$order.id</title></head>
<body>
<p>[You are logged in as $user.getFullName().]</p>
<p>
#if ($order.hasShipped())
 Your order has shipped. Your tracking number is $order.trackingNumber.
#else
 Your order has not yet shipped.
#end if
</p>
<p>Order #$order.id contains the following items:</p>
<ul>
#for $purchased, $quantity in $order.purchased.items():
 <li>$purchased.name: $quantity unit#slurp
#if ($quantity != 1)
s
#end if
</li>
#end for
</ul>
<hr />
Served by Online Store v1.0
</body>
</html>
 

组合模板

前面定义的 HTML 定单状态页包含某些元素,这些元素对所有在线商店的 Web 应用程序都(应该)很常见:一个带有特定于页面的小标题的 HTML 标题、一个位于页面顶部告诉您您已经进入系统的通知以及一个 HTML 脚注。可能还应该将这些常见页面元素引入某个单独的主模板中。这样,OrderStatus.tmpl 中的模板定义将只包含专门用来显示定单状态的代码,而其他所有模板定义可以使用主模板中创建的代码,不必每次都定义相同的代码。

大多数模板系统(包括 Cheetah 及其 #include 指令)都允许您从另一个模板调用一个模板。您可以使用这项功能,将常见的模板内容移入(例如)Header.tmpl 和 Footer.tmpl 文件中。不过,Cheetah 还允许使用一种更好的方法来解决这个问题:通过使模板进行细分成为可能。

以下是 Skeleton.tmpl,是定义了一个 THML 页的主干的模板定义,但它很明显地留下了两个项没有进行绑定:$title 和 $body:


清单 12. Skeleton.tmpl 的模板定义
<html>
<head><title>$title</title></head>
<body>
<p>[You are logged in as $user.getFullName().]</p>
$body
<hr />
Served by Online Store v1.0.
</body>
</html>
 


回忆一下前面的内容,我们通过在 Skeleton.tmpl 文件上运行 cheetah compile 命令生成了 Skeleton.py,这是一个包含称作 Skeleton 的 Python 类的包,这个包的作用类似于 Skeleton.tmpl。一旦文件准备就绪,就可以编写定单状态模板了。OrderStatusII.tmpl 可以导入生成的 Skeleton 类并对其进行细分。您还可以定义 $title 和 $body 的值,以及主模板中引用的变量:


清单 13. 细分 Skeleton.tmpl
#from Skeleton import Skeleton
#extends Skeleton
#def title
Status for order #$order.id
#end def
#def body
<p>
#if ($order.hasShipped())
 Your order has shipped. Your tracking number is $order.trackingNumber.
#else
 Your order has not yet shipped.
#end if
</p>
<p>Order #$order.id contains the following items:</p>
<ul>
#for $purchased, $quantity in $order.purchased.items():
 <li>$purchased.name: $quantity unit#slurp
#if ($quantity != 1)
s
#end if
 </li>
#end for
</ul>
#end def
 


OrderStatusII.tmpl 使用 #extends 指令来声明其模板是 Skeleton.tmpl 中定义的模板的一个特例。然后,它使用 #extends 指令定义称为 title 和 body 的函数。这些函数对应于主干模板中使用的 $title 和 $body 变量。它们不会在这里 #set(设置)$title 和 $body 的值: #set 指令设置 Python 变量的值,但 title 和 body 则需要对应于 Python 函数。

在 Python 中使用 def 关键字时,还可以使用 Cheetah 的 #def 指令在模板内部定义函数。然后,可以多次调用该模板,从而避免复制和粘贴代码。


结束语

Cheetah 提供了比这里描述的更多的特性。例如,您可以设置一个筛选器,以某种特定的方式修改所有变量引用的输出。还可以使用 #import 指令将任意 Python 模块导入 Cheetah 模板中,并调用它们的函数。实际上,几乎在 Python 中可以所做的所有事情都能在 Cheetah 中实现。

不过,建议您使所有事情尽量简单。请记住,模板系统的目标是将文档的动态部分与其静态部分分离。从将应用程序代码放入 Cheetah 模板开始,您会发现自己遇到同样头疼的问题 —— 迫使编程人员和 UI 设计人员首先选择模板系统。Cheetah 哲学的一个方面是:“Python 适用于后端,Cheetah 适用于前端。”根据这一经验法则,您应该畅通无阻地获取模板系统的好处。