YATL 模板语言
py4web 使用两种不同的模板语言来呈现包含 Python 代码的动态 HTML 页面
yatl (Yet Another Template Language) ,被认为是原始的参考实现
Renoir ,是 yatl 的更新、更快的实现,具有额外的功能
由于 Renoir 不包括 HTML 助手(见下一章),py4web 默认使用 Renoir 模块渲染模板,使用 yatl 模块渲染助手,再加上一些小技巧使它们无缝协作。
py4web 还使用双方括号 [[ ... ]] 转义嵌入在 HTML 中的 Python 代码,除非另有说明。
使用方括号而不是尖括号的优点是,它对所有常见的 HTML 编辑器都是透明的(即没有特殊含义)。这允许开发人员使用这些编辑器创建 py4web 模板。
警告
请小心,不要将 Python 代码方括号与其他方括号混合使用!例如,你很快就会看到这样的语法:
[[items = ['a', 'b', 'c']]] # this gives "Internal Server Error" [[items = ['a', 'b', 'c'] ]] # this works
必须在第一个方括号后添加一个空格,将列表与 Python 代码方括号隔开。
因为开发人员将 Python 代码嵌入 HTML 中,所以文档应根据 HTML 规则而不是Python 规则缩进。因此,我们允许在 [[ ... ]] 标签内使用未缩进的 Python 代码。但由于 Python 通常使用缩进来分隔代码块,我们需要一种不同的方式来分隔它们;这就是 py4web 模板语言使用 Python 关键字 pass 的原因。
代码块(code block) 以冒号结尾的行开始,以 pass 开头的行结束。当代码块的结尾从上下文中显而易见时,不再必须有关键字 pass 。
以下是一个示例:
[[
if i == 0:
response.write('i is 0')
else:
response.write('i is not 0')
pass
]]
请注意, pass 是 Python 关键字,而不是 py4web 关键字。一些 Python 编辑器,如 Emacs,使用关键字 pass 表示块的划分,并使用它自动重新生成缩进的代码。
py4web 模板语言的功能完全相同。当它发现类似以下内容时:
<html><body>
[[for x in range(10):]][[=x]] hello <br />[[pass]]
</body></html>
py4web 模板语言将其转换为程序:
response.write("""<html><body>""", escape=False)
for x in range(10):
response.write(x)
response.write(""" hello <br />""", escape=False)
response.write("""</body></html>""", escape=False)
response.write 将内容写入响应正文中。
当 py4web 模板中出现错误时,错误报告会显示生成的模板代码,而不是开发人员编写的实际模板。这有助于开发人员通过突出显示实际执行的代码来调试代码(可以使用 HTML 编辑器或浏览器的 DOM 检查器进行调试)。
另请注意:
[[=x]]
生成
response.write(x)
默认情况下,以这种方式注入 HTML 的变量会被转义。如果 x 是 XML 对象,则忽略转义,即使转义设置为 True (有关详细信息,请稍后参阅 XML)。
下面是一个示范 H1 helper 的示例
[[=H1(i)]]
这被转换为:
response.write(H1(i))
在评估后,H1 对象及其组件被递归序列化、转义并写入响应体。 H1 和内部 HTML 生成的标签不会转义。此机制保证网页上显示的所有文本(仅文本)始终被转义,从而防止 XSS 漏洞。同时,代码简单,易于调试。
方法 response.write(obj, escape=True) 有两个参数,要写入的对象和是否必须转义(默认设置为 True )。如果 obj 有一个 .xml() 方法,则调用该方法并将结果写入响应体(忽略 escape 参数的设置)。否则,它使用对象的 __str__ 方法对其进行序列化,如果 escape 参数为 True,则再对其进行转义。所有内置的 helper 对象(如示例中的 H1 )都是知道如何通过 .xml() 方法进行序列化它们自身内容的对象。
这一切都是透明完成的(无需额外干预)。
备注
虽然控制器内使用的响应对象是一个完整的 bottle.response 对象,但在 yatl 模板内,它被一个虚拟对象( yatl.template.DummyResponse )替换。这个对象非常不同,也简单得多:它只有一个写方法!此外,您永远不需要(也不应该)显式调用 response.write 方法。
基本语法
py4web 模板语言支持所有 Python 控制结构。在这里,我们为每一个提供了一些例子。它们可以根据实际的编程情况被嵌套。您可以通过复制 _scaffold 应用程序(请参阅 复制 _scaffold 应用 ),然后编辑文件 new_app/template/index.html 来轻松地进行测试。
for...in
在模板中,你可以遍历任何可迭代对象:
[[items = ['a', 'b', 'c'] ]]
<ul>
[[for item in items:]]<li>[[=item]]</li>[[pass]]
</ul>
被生成为:
<ul>
<li>a</li>
<li>b</li>
<li>c</li>
</ul>
这里地 items 是任意的可迭代对象,例如 Python 中的 list、tuple、 Rows 或者任何其它被实现为 iterator 的对象。显示的元素首先被序列化,然后再被转义。
while
您可以使用 while 关键字创建循环:
[[k = 3]]
<ul>
[[while k > 0:]]<li>[[=k]][[k = k - 1]]</li>[[pass]]
</ul>
被生成为:
<ul>
<li>3</li>
<li>2</li>
<li>1</li>
</ul>
if...elif...else
您可以使用条件子句:
[[
import random
k = random.randint(0, 100)
]]
<h2>
[[=k]]
[[if k % 2:]]is odd[[else:]]is even[[pass]]
</h2>
被生成为:
<h2>
45 is odd
</h2>
因为 else 显而易见地结束了紧跟 if 的第一个语句块,所以这里不再需要 pass 语句,如果用了将会导致错误。无论如何,你必须使用一个 pass 明确地结束紧跟 else 的语句块。
回想一下,在 Python 中被写成 elif 的 “else if”,如下例所示:
[[
import random
k = random.randint(0, 100)
]]
<h2>
[[=k]]
[[if k % 4 == 0:]]is divisible by 4
[[elif k % 2 == 0:]]is even
[[else:]]is odd
[[pass]]
</h2>
由它会生成:
<h2>
64 is divisible by 4
</h2>
try...except...else...finally
在模板中,也可以使用 try...except 语句,但有一个警告。考虑以下示例:
[[try:]]
Hello [[= 1 / 0]]
[[except:]]
division by zero
[[else:]]
no division by zero
[[finally:]]
<br />
[[pass]]
由它将生成如下输出:
Hello division by zero
<br />
此示例说明了在 try 块内异常发生之前生成的所有输出(包括异常之前的输出)都会被呈现出来。“Hello” 被输出了,是因为它位于异常之前。
def...return
py4web 模板语言允许开发人员定义和实现可以返回任何 Python 对象或 text/html 格式字符串的函数。这里我们有两个例子:
[[def itemize1(link): return LI(A(link, _href="http://" + link))]]
<ul>
[[=itemize1('www.google.com')]]
</ul>
会生成下面的输出:
<ul>
<li><a href="http://www.google.com">www.google.com</a></li>
</ul>
itemize1 函数返回了一个 helper 对象,其位置放在了在函数被调用的地方。
现在,看下面的代码:
[[def itemize2(link):]]
<li><a href="http://[[=link]]">[[=link]]</a></li>
[[return]]
<ul>
[[itemize2('www.google.com')]]
</ul>
它产生与上述完全相同的输出。在这种情况下,函数 itemize2 表示一段 HTML ,它将替换调用该函数的 py4web 标签。请注意,在调用 itemize2 之前没有 “=” ,因为该函数不返回文本,而是将其直接写入响应中。
有一个警告:即使没有要返回的内容,模板内定义的函数也必须以 return 语句终止,否则自动缩进处理将失败。
信息工作流程
为了动态修改信息的工作流程,可以使用自定义命令: extend 、 include 、 block 和 super 。请注意,它们是特殊的模板指令,而不是 Python 命令。
此外,您可以在模板中使用普通的 Python 函数。
extend 和 include
模板可以扩展和包含其他模板,这形成一个树状结构。
例如,我们可以想到一个模板 “index.html” ,它扩展了 “layout.html” 并包含了 “body.html” 。同时,“layout.html” 可能包括 “header.html” 和 “footer.html” 。
树状结构的根就是我们所说的 layout template 。与任何其他 HTML 模板文件一样,您可以从命令行或使用 py4web Dashboard 对其进行编辑。文件名 “layout.html” 只是一个约定。
这有一个很简单的页面,它扩展了 “layout.html” 模板,并包含了 “page.html” 模板:
<!--minimalist_page.html-->
[[extend 'layout.html']]
<h1>Hello World</h1>
[[include 'page.html']]
被扩展的布局文件必须包含 [[include]] 指令,类似于:
<!--layout.html-->
<html>
<head>
<title>Page Title</title>
</head>
<body>
[[include]]
</body>
</html>
调用模板时,将加载被扩展(布局)模板,调用模板的内容将替换布局中的 [[include]] 指令。如果你没有在布局中写入 [[include]] 指令,那么它将默认地被包含在文件的开头。此外,如果您使用多个 [[extend]] 指令,则只会处理最后一个指令。处理过程会递归进行,直到所有 extend 和 include 指令都已处理完毕。然后 py4web 将生成的模板翻译成 Python 代码。
请注意,当对应用程序进行字节码编译时,编译的是翻译后的 Python 代码,而不是原始模板文件本身。因此,给定模板的字节码编译版本是一个.pyc文件,其中不仅包括原始模板文件的 Python 代码,还包括其整个被扩展的和被包含的模板的树状结构的 Python 代码。
[[extend ...]] 指令 之前 的任何内容或代码都将在被扩展的模板的内容/代码开始之前插入(并因此执行)。虽然这通常不用于在扩展模板的内容之前插入实际的 HTML 内容,但它可以作为定义要提供给扩展模板的变量或函数的一种手段。例如,考虑一个模板 “index.html” :
<!--index.html-->
[[sidebar_enabled=True]]
[[extend 'layout.html']]
<h1>Home Page</h1>
以及 “layout.html” 的部分代码片段:
<!--layout.html-->
[[include]]
[[if sidebar_enabled:]]
<div id="sidebar">
Sidebar Content
</div>
[[pass]]
在 “index.html” 中,因为 sidebar_enabled 的声明在 extend 之前,所以那一行将在 “layout.html” 开始之前被插入执行,这使得 sidebar_enabled 在 “layout.html” 内的任何地方都可以被使用。
同样值得指出的是,控制器函数返回的变量不仅在函数的主模板中可用,而且在其所有扩展和包含的模板中也可用。
使用变量进行扩展
extend 或 include 的参数(即被扩展或被包含的模板名称)可以是 Python 变量(尽管不能是 Python 表达式)。然而,这带来了一个限制——在 extend 或 include 语句中使用变量的模板不能进行字节码编译。如上所述,字节码编译的模板包括被扩展和被包含的模板的整个树,因此在编译时必须知道特定的被扩展和被包含的模板,如果模板名称是变量,则不可能知道(其值直到运行时才确定)。因为字节码编译模板可以显著提高速度,如果可能的话,通常应该避免在 extend 和 include 中使用变量。
在某些情况下,在 include 中使用变量的另一种方法是将常规的 [[include ...]] 指令放在 if...else 块中。
[[if some_condition:]]
[[include 'this_template.html']]
[[else:]]
[[include 'that_template.html']]
[[pass]]
上述代码对字节码编译没有任何问题,因为它不涉及变量。但是请注意,字节码编译的模板实际上将包含 “this_template.html” 和 “that_template.html” 的 Python 代码,尽管根据 some_condition 的值,只会执行其中一个模板的代码。
请记住,这只适用于 include —— 你不能在 if...else 块内放置 [[extend ...]] 指令。
布局用于封装页面通用性(页眉、页脚、菜单),虽然它们不是强制性的,但它们将使您的应用程序更容易编写和维护。
模板中定义函数(Template Functions)
考虑一下这个 “layout.html” :
<!--layout.html-->
<html>
<body>
[[include]]
<div class="sidebar">
[[if 'mysidebar' in globals():]][[mysidebar()]][[else:]]
my default sidebar
[[pass]]
</div>
</body>
</html>
和下面这个扩展模板:
[[def mysidebar():]]
my new sidebar!!!
[[return]]
[[extend 'layout.html']]
Hello World!!!
请注意,函数是在 [[extend...]] 语句之前被定义的——这导致函数是在执行 “layout.html” 代码之前创建的,因此函数可以在 “layout.html” 中的任何地方调用,甚至在 [[include]] 之前。还要注意,该函数包含在扩展模板中,但是没有前缀 = 。
该代码生成以下输出:
<html>
<body>
Hello World!!!
<div class="sidebar">
my new sidebar!!!
</div>
</body>
</html>
请注意,该函数是在 HTML 中被定义的(尽管它也可以包含 Python 代码),因此使用 response.write 来输出其内容(该函数不返回内容)。这就是为什么布局使用 [[mysidebar()]] 而不是 [[=mysidebar()]] 。以这种方式定义的函数可以接受参数。
block 和 super
使模板更加模块化的主要方法是使用 [[block ...]] ,这种机制是上一节讨论的机制的替代方案。
要了解这是如何工作的,请考虑基于脚手架应用程序 welcome 的应用程序,它有一个模板 layout.html 。此布局模板由模板 default/index.html 通过 [[extend 'layout.html']] 被扩展。layout.html 的内容预先定义了具有某些默认内容的某些块,因此这些块被包含在 default/index.html 中。
您可以通过 将新内容包含在相同的块名称中来覆盖这些默认内容块 。layout.html 中块的位置没有改变,但内容发生了变化。
这是一个简化版本。想象一下这是 “layout.html” :
<html>
<body>
[[include]]
<div class="sidebar">
[[block mysidebar]]
my default sidebar (this content to be replaced)
[[end]]
</div>
</body>
</html>
而这是一个简单的包含扩展的模板 default/index.html :
[[extend 'layout.html']]
Hello World!!!
[[block mysidebar]]
my new sidebar!!!
[[end]]
这生成下面的输出,其内容由在包含扩展指令的模板中相同名称的覆盖看提供,而包含内容的 DIV 和 class 样式来自 layout.html。这允许模板之间的一致性:
<html>
<body>
Hello World!!!
<div class="sidebar">
my new sidebar!!!
</div>
</body>
</html>
实际的 layout.html 定义了许多有用的块,您可以轻松添加更多块来匹配您想要的布局。
您可以有许多块,如果被扩展的模板中存在块,但包含扩展指令的模板中没有块,则使用被扩展的模板中的内容。此外,请注意,与函数不同,不需要在 [[extend ...]] 之前定义块——即使在 extend 之后定义,它们也可以用于在被扩展的模板中的任何位置进行替换。
在块内,可以使用表达式 [[super]] 来包含被扩展的模板中同名块内原有的内容。例如,如果我们将以上包含扩展指令的模板内容替换为:
[[extend 'layout.html']]
Hello World!!!
[[block mysidebar]]
[[super]]
my new sidebar!!!
[[end]]
将得到:
<html>
<body>
Hello World!!!
<div class="sidebar">
my default sidebar
my new sidebar!
</div>
</body>
</html>
页面布局的标准结构
默认的页面布局
当前 py4web 提供的 _scaffold 应用程序附带的 “templates/layout.html” 非常复杂,但它具有以下结构:
1 <!DOCTYPE html>
2 <html>
3 <head>
4 <base href="[[=URL('static')]]/">
5 <meta name="viewport" content="width=device-width, initial-scale=1">
6 <link rel="shortcut icon" href="data:image/x-icon;base64,AAABAAEAAQEAAAEAIAAwAAAAFgAAACgAAAABAAAAAgAAAAEAIAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAPAAAAAA=="/>
7 <link rel="stylesheet" href="css/no.css">
8 <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.14.0/css/all.min.css" integrity="sha512-1PKOgIY59xJ8Co8+NE6FZ+LOAZKjy+KY8iq0G4B3CyeY6wYHN3yt9PW0XpSriVlkMXe40PTKnXrLnZ9+fkDaog==" crossorigin="anonymous" />
9 <style>.py4web-validation-error{margin-top:-16px; font-size:0.8em;color:red}</style>
10 [[block page_head]]<!-- individual pages can customize header here -->[[end]]
11 </head>
12 <body>
13 <header>
14 <!-- Navigation bar -->
15 <nav class="black">
16 <!-- Logo -->
17 <a href="[[=URL('index')]]">
18 <b>py4web <script>document.write(window.location.href.split('/')[3]);</script></b>
19 </a>
20 <!-- Do not touch this -->
21 <label for="hamburger">☰</label>
22 <input type="checkbox" id="hamburger">
23 <!-- Left menu ul/li -->
24 [[block page_left_menu]][[end]]
25 <!-- Right menu ul/li -->
26 <ul>
27 [[if globals().get('user'):]]
28 <li>
29 <a class="navbar-link is-primary">
30 [[=globals().get('user',{}).get('email')]]
31 </a>
32 <ul>
33 <li><a href="[[=URL('auth/profile')]]">Edit Profile</a></li>
34 <li><a href="[[=URL('auth/change_password')]]">Change Password</a></li>
35 <li><a href="[[=URL('auth/logout')]]">Logout</a></li>
36 </ul>
37 </li>
38 [[else:]]
39 <li>
40 Login
41 <ul>
42 <li><a href="[[=URL('auth/register')]]">Sign up</a></li>
43 <li><a href="[[=URL('auth/login')]]">Log in</a></li>
44 </ul>
45 </li>
46 [[pass]]
47 </ul>
48 </nav>
49 </header>
50 <!-- beginning of HTML inserted by extending template -->
51 <center>
52 <div>
53 <!-- Flash alert messages, first optional one in data-alert -->
54 <flash-alerts class="padded" data-alert="[[=globals().get('flash','')]]"></flash-alerts>
55 </div>
56 <main class="padded">
57 <!-- contect injected by extending page -->
58 [[include]]
59 </main>
60 </center>
61 <!-- end of HTML inserted by extending template -->
62 <footer class="black padded">
63 <p>
64 Made with py4web
65 </p>
66 </footer>
67 </body>
68 <!-- You've gotta have utils.js -->
69 <script src="js/utils.js"></script>
70 [[block page_scripts]]<!-- individual pages can add scripts here -->[[end]]
71 </html>
此默认布局的一些特点,使其非常易于使用和自定义:
使用 HTML5 编写
在第 7 行,使用了
no.css样式表,请参见 这里当页面呈现时,第 58 行的
[[include]]被包含扩展指令的模板的内容替换包含以下块:page_head、page_left_menu、page_scripts
在第 30 行,它检查用户是否已登录,并相应地更改菜单
在第 54 行,它检查 flash alert 消息
当然,也可以用你自己编写的内容彻底替换 “layout.html” 和样式表。
移动开发
虽然默认 layout.html 的设计是移动友好的,但当移动设备访问页面时,有时可能需要使用不同的模板。