YATL 模板语言

py4web 使用两种不同的模板语言来呈现包含 Python 代码的动态 HTML 页面

由于 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 的变量会被转义。如果 xXML 对象,则忽略转义,即使转义设置为 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 语句终止,否则自动缩进处理将失败。

信息工作流程

为了动态修改信息的工作流程,可以使用自定义命令: extendincludeblocksuper 。请注意,它们是特殊的模板指令,而不是 Python 命令。

此外,您可以在模板中使用普通的 Python 函数。

extendinclude

模板可以扩展和包含其他模板,这形成一个树状结构。

例如,我们可以想到一个模板 “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]] 指令,则只会处理最后一个指令。处理过程会递归进行,直到所有 extendinclude 指令都已处理完毕。然后 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” 内的任何地方都可以被使用。

同样值得指出的是,控制器函数返回的变量不仅在函数的主模板中可用,而且在其所有扩展和包含的模板中也可用。

使用变量进行扩展

extendinclude 的参数(即被扩展或被包含的模板名称)可以是 Python 变量(尽管不能是 Python 表达式)。然而,这带来了一个限制——在 extendinclude 语句中使用变量的模板不能进行字节码编译。如上所述,字节码编译的模板包括被扩展和被包含的模板的整个树,因此在编译时必须知道特定的被扩展和被包含的模板,如果模板名称是变量,则不可能知道(其值直到运行时才确定)。因为字节码编译模板可以显著提高速度,如果可能的话,通常应该避免在 extendinclude 中使用变量。

在某些情况下,在 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()]] 。以这种方式定义的函数可以接受参数。

blocksuper

使模板更加模块化的主要方法是使用 [[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 的设计是移动友好的,但当移动设备访问页面时,有时可能需要使用不同的模板。