QWeb模板
QWeb是Odoo中主要使用的模板引擎,它是一个XML模板引擎,主要用于生成HTML片段和页面。
模板指令通过XML属性指定,并以t-
作为前缀,例如用于条件判断的t-if
,其中元素和其他属性会被直接渲染输出。
为了防止元素被渲染,这里还提供了一个占位符元素。该元素执行其指令但本身不会生成任何输出内容,可以表示为:<t>
<t t-if="condition"> <p>Test</p></t>
将会输出:
<p>Test</p>
如果为condition条件为 true,则:
<div t-if="condition"> <p>Test</p></div>
将会输出:
<div> <p>Test</p></div>
数据输出
QWeb的输出指令会自动对输入内容进行HTML转义,这样在显示用户提供的内容时可以有效限制跨站脚本(XSS)风险。
out指令接受一个表达式,计算其结果并将该结果注入到文档中。
<p><t t-out="value"/></p>
在value设置为42的情况下呈现:
<p>42</p>
参见“高级输出”以了解更多高级主题(例如注入原始HTML等内容)。
条件
QWeb提供了一个条件指令t-if,该指令会计算其属性值中给定的表达式:
<div> <t t-if="condition"> <p>ok</p> </t></div>
如果条件为 true,则呈现该元素:
<div> <p>ok</p></div>
但是,如果条件为 false,则会从结果中删除它:
<div></div>
条件呈现适用于指令的持有者,该指令的持有者确实如此 不一定是:
<div> <p t-if="condition">ok</p></div>
这将产生与前一个示例相同的结果。
另外还提供了额外的条件分支指令t-elif和t-else。
<div> <p t-if="user.birthday == today()">Happy birthday!</p> <p t-elif="user.login == 'root'">Welcome master!</p> <p t-else="">Welcome!</p></div>
循环
QWeb提供了一个迭代指令t-foreach,该指令接受一个返回待遍历集合的表达式作为参数,并且需要第二个参数来指定在迭代过程中“当前项”所使用的名称: t-foreach与t-as搭配使用。
<t t-foreach="[1, 2, 3]" t-as="i"> <p><t t-out="i"/></p></t>
将呈现为:
<p>1</p><p>2</p><p>3</p>
与条件指令类似,t-foreach作用于带有该指令属性的元素上,并且对指定集合进行迭代。
<p t-foreach="[1, 2, 3]" t-as="i"> <t t-out="i"/></p>
这与前面的示例效果相同。
t-foreach可以对数组进行迭代(当前项将是当前值),也可以对映射进行迭代(当前项将是当前键)。虽然仍支持对整数进行迭代(等同于在0(包含)和提供的整数(不包含)之间的数组上迭代),但这一做法已过时并被弃用。
除了通过t-as传递的名称外,t-foreach还为各种数据点提供了几个其他变量。
$as_all(已弃用)警告
$as将会被传递给t-as的名称替换。
被迭代的对象
注意
此变量仅在 JavaScript QWeb 上可用,而不适用于 Python。
$as_value
当前迭代值,与 for lists 和 integers 相同, 但对于映射,它提供值(其中提供键) a s as asas
$as_index
当前迭代索引(迭代的第一项索引为 0)
$as_size
集合的大小(如果可用)
a s f i r s t 当前项是否为迭代中的第一个项(等效于 as_first 当前项是否为迭代中的第一个项(等效于 asfirst当前项是否为迭代中的第一个项(等效于as_index == 0)
a s l a s t 当前项是否为迭代的最后一个(等效于),要求迭代对象的大小为可用 as_last 当前项是否为迭代的最后一个(等效于 ),要求迭代对象的大小为 可用 aslast当前项是否为迭代的最后一个(等效于),要求迭代对象的大小为可用as_index + 1 == $as_size
$as_parity(已弃用)
either 或 ,当前迭代轮次的奇偶校验"even"“odd”
$as_even(已弃用)
一个布尔标志,指示当前迭代轮处于偶数状态 指数
$as_odd(已弃用)
一个布尔标志,指示当前迭代轮处于奇数状态 指数
这些额外提供的变量以及在foreach内部创建的所有新变量仅在foreach的范围内可用。如果该变量在foreach外部上下文中已存在,则在foreach结束时,其值会被复制到全局上下文中。
<t t-set="existing_variable" t-value="False"/><!-- existing_variable now False --><p t-foreach="[1, 2, 3]" t-as="i"> <t t-set="existing_variable" t-value="True"/> <t t-set="new_variable" t-value="True"/> <!-- existing_variable and new_variable now True --></p><!-- existing_variable always True --><!-- new_variable undefined -->
属性
QWeb支持动态计算属性值,并将计算结果应用于输出节点。这一功能通过t-att(属性)指令实现,该指令有3种不同形式。
t-att-$name创建调用的属性,计算属性值 结果被设置为属性的值:$name
<div t-att-a="42"/>
将会被呈现为:
<div a="42"></div>
t-attf-$name与前面相同,但参数是一个格式化字符串而非仅仅是表达式,这种方式常用于混合字面量字符串和非字面量字符串(例如类名):
<t t-foreach="[1, 2, 3]" t-as="item"> <li t-attf-class="row {{ (item_index % 2 === 0) ? 'even' : 'odd' }}"> <t t-out="item"/> </li></t>
将会被呈现为:
<li class="row even">1</li><li class="row odd">2</li><li class="row even">3</li>
t-att属性映射提示
在QWeb模板中,有以下格式化字符串的两种等效语法:(也称为Jinja风格)和(也称为Ruby风格)。“plain_text {{code}}”
“plain_text #{code}”
如果参数是一个映射(字典),则每个键值对将生成一个新属性及其对应的值:
<div t-att="{'a': 1, 'b': 2}"/>
将被渲染为:
<div a="1" b="2"></div>
t-att属性对如果参数是一对(元组或包含两个元素的数组),则该对的第一个元素作为属性名,第二个元素作为属性值:
<div t-att="['a', 'b']"/>
将被渲染为:
<div a="b"></div>
设置变量
QWeb允许在模板内部创建变量,用于存储计算结果(以便多次使用)、给数据片段赋予更清晰的名称等。这通过t-set
指令完成,该指令接受要创建的变量名。变量的赋值可以通过两种方式提供:
t-value
属性包含一个表达式,其计算结果将作为变量的值: <t t-set="foo" t-value="2 + 1"/><t t-out="foo"/>
这段代码将会输出3。
如果没有t-value
属性,则节点的内容会被渲染并作为变量的值: <t t-set="foo"> <li>ok</li></t><t t-out="foo"/>
调用子模板
QWeb模板不仅可以用于顶级渲染,还可以从另一个模板内部使用,以避免重复或命名模板的部分内容。这通过t-call
指令实现:
<t t-call="other-template"/>
这将调用具有父级执行上下文的名为“other-template”的模板。如果有定义如下:
<p><t t-value="var"/></p>
上面的调用将呈现为空,但是:
<t t-set="var" t-value="1"/><t t-call="other-template"/>
将呈现为 <p>1</p>
。
另外,使用t-call
时,可以在指令体中设置变量,这些变量会在调用子模板之前进行评估,并且可以更改本地上下文:
<t t-call="other-template"> <t t-set="var" t-value="1"/></t><!-- 在这里 "var" 不再存在 -->
t-call
指令的主体可以是任意复杂的结构(不仅仅是简单的指令)。它的呈现结果将以魔术变量set0
的形式在被调用模板中可用:
<div> This template was called with content: <t t-out="0"/></div>
因此,当这样调用时:
<t t-call="other-template"> <em>content</em></t>
将导致:
<div> This template was called with content: <em>content</em></div>
高级输出
默认情况下,通过t-out
指令输出的内容会自动进行HTML转义以防止XSS攻击。不需要转义的内容会原样注入文档,并可能成为实际HTML标记的一部分。
唯一跨平台安全的内容包括来自t-call
输出的内容以及与t-set
一起使用的“body”内容(而非t-value
或f
)。
Python
通常情况下,您无需过多关注:那些有道理应该生成“安全”内容的API会自动做到这一点,并且事情应当透明地运行。
然而,在某些需要更明确的情况中,以下API会输出默认在注入到模板时不会(重新)转义的安全内容:
HTML字段。
html_escape()
和 escape()
(它们是别名,不会有双重转义的风险)。
markupsafe.escape()
html_sanitize()
。
markupsafe.Markup
类及其实例。
警告:
markupsafe.Markup
是一种不安全的API,它表明您希望内容为标记安全的,但实际上无法检查这一点,因此应当小心使用。
to_text()
不会将内容标记为安全,但也不会从已安全的内容中剥离安全性信息。 强制双层转义
如果内容被标记为安全,但由于某些原因仍需要转义(例如,要打印HTML字段的原始标记),可以将其转换回普通字符串以“去除”安全标志。在Python中和JavaScript中分别可以这样做:
python
str(content)
javascript
String(content)
这样操作后,即使原本的内容被认为安全而不会被自动转义,在将其转换为普通字符串后,再次注入到模板或其他地方时将会按照常规进行转义处理。
注意:
由于Markup
类型比普通字符串更丰富,在一些操作中,Markup
类型的某些操作会剥离安全信息,而普通字符串则不会。例如,在Python中使用字符串连接操作(+
)会导致其中一个操作数被正确转义后的str
类型,而在JavaScript中,+
运算符将得到一个未转义前就被连接起来的结果。
废弃的输出指令
esc
: 是out
的一个别名,原本会对其输入进行HTML转义。尚未正式废弃,因为out
和esc
之间的唯一区别在于后者含义不太清晰/不准确。raw
: 是out
的一个版本,永不转义其内容。无论内容是否安全,都会原样输出。 废弃自15.0版本起:请改用带有markupsafe.Markup
值的out
。
Python
专享指令
资产包
智能记录字段格式化
t-field
指令只能在对“智能”记录(方法的结果)执行字段访问(如a.b
)时使用。它可以基于字段类型自动格式化,并且与网站的富文本编辑功能集成。
t-options
可用于定制字段,最常见的选项是widget
,其他选项取决于特定的字段或小部件。
调试
t-debug
当
t-debug
指令的值为空时,会调用内置函数breakpoint()
,通常会触发调试器(默认为pdb)。此行为可以通过PYTHONBREAKPOINT
环境变量或sys.breakpointhook()
进行配置。 渲染缓存:
t-cache="key_cache"
标记要在渲染时缓存的模板部分。 每个子指令将仅在第一次渲染期间调用。这意味着 在呈现这些子指令期间超出的 SQL 查询 也只做一次。
t-nocache="documentation"
标记每次要渲染的模板部分。 内容只能使用根值。
为什么以及何时使用t-cache?
该指令用于通过缓存最终文档的部分内容来加速渲染,这可能会节省对数据库的查询。然而,应谨慎使用t-cache
,因为它不可避免地会使模板变得复杂(例如对其理解造成困难)。
在实际节约数据库查询时,可能需要以惰性求值的方式渲染模板。如果这些惰性值被用在了缓存部分中,在缓存中有这部分内容时,它们将不会被评估。
t-cache
指令对于那些使用依赖于有限数据集的值的模板部分非常有用。我们建议通过启用“添加qweb指令上下文”选项分析模板的渲染情况。在控制器中向渲染传递惰性值有助于显示使用这些值的指令,并触发相应的查询。
使用此类缓存的一个关注点是如何使其适用于不同用户(不同的用户应该以相同的方式渲染缓存部分)。另一个潜在问题是适时使缓存失效。为此,应当明智地选择键表达式。例如,使用记录集的write_date
可以使得缓存键过期,而无需显式从缓存中移除它。
同时需要注意的是,缓存部分中的值具有作用域限制。这意味着,如果模板这部分包含t-set
指令,那么其后的渲染结果可能会与没有t-set
指令时有所不同。
如果在t-cache内部还有一个t-cache指令?
这些组件已经被缓存,其中每个仅包含各自渲染后的字符串表示。这样一来,内部的部分很可能访问频率较低,其对应的缓存键也可能不会经常用到。倘若确实存在这样的情况,即内部内容应当独立于外部环境而不受其影响,则你可能需要在同一节点或者其父节点上添加一个t-cachet-nocache标签来禁用这部分内容的缓存功能。
t-nocache是做什么用的?
如果你想使用t-cache
缓存模板的一部分,但其中一小部分内容需要保持动态并在缓存时重新计算。然而,在t-cache
内的t-nocache
部分将无法访问到模板中的变量值。这里仅能访问由控制器提供的值。例如,菜单项总是相同且渲染耗时较长(可以利用性能开发工具配合qweb上下文进行分析),因此将其整体缓存是合理的。但在菜单中,我们希望电商购物车始终显示最新的状态。所以在这里使用了t-nocache
来确保这部分内容始终保持动态,并且可以利用t-set
指令设置和更新购物车的相关数据。
t-cache的基础
指令允许你存储模板渲染的结果。键表达式(例如 42: t-cache=“42”)会被当作Python表达式进行计算。这个表达式将用于生成缓存键,因此对于同一个模板部分,可以根据不同的键表达式值拥有不同的缓存值(已缓存的渲染部分)。如果键表达式返回一个元组或列表,那么在生成缓存键时会搜索这些内容。如果键表达式返回了一个或多个记录集,那么模型、ID及其对应的write_date(写入日期)将会被用来生成缓存键。
特殊情况:如果键表达式返回了Falsy值(即假值),那么内容将不会被缓存。
示例:
<div t-cache="record,bool(condition)"> <span t-if="condition" t-field="record.partner_id.name"> <span t-else="" t-field="record.partner_id" t-options-widget="contact"></div>
在这种情况下,对于已经根据真条件返回的每条记录以及假条件,缓存中都可能存在相应的值(字符串)。如果某个模块修改了记录,则被修改的记录的write_date
会发生变化,此时已缓存的值会被丢弃。
t-cache 与作用域内的值(如 t-set 和 t-foreach)
在t-cache区域内的值具有作用域限制,这意味着是否在其父节点之一上使用了t-cache会改变其行为表现。务必记住,Odoo大量使用了模板和视图继承机制。因此,在某个地方添加一个t-cache可能会修改你在编辑时并未直接看到的其他模板的渲染效果。(这就像在每个迭代中使用的for each循环一样,会影响到范围内的变量状态)
示例:
<div> <t t-set="a" t-value="1"/> <inside> <t t-set="a" t-value="2"/> <t t-out="a"/> </inside> <outside t-out="a"/> <t t-set="b" t-value="1"/> <inside t-cache="True"> <t t-set="b" t-value="2"/> <t t-out="b"/> </inside> <outside t-out="b"/></div>
将呈现:
<div> <inside>2</inside> <outside>2</inside> <inside>2</inside> <outside>1</inside></div>
t-nocache的基础
带有t-nocache
属性的节点所包含的模板部分不会被缓存。因此,这部分内容是动态的,并且每次都会被系统地重新渲染。然而,在这个区域内可访问的值仅限于由控制器提供的那些值(在调用render方法时)。
示例:
<section> <article t-cache="record"> <title><t t-out="record.name"/> <i t-nocache="">(views: <t t-out="counter"/>)</i></titlle> <content t-out="record.description"/> </article></section>
将呈现 (counter = 1):
<section> <article> <title>The record name <i>(views: 1)</i></titlle> <content>Record description</content> </article></section>
在此,包含<i>标签的容器部分将始终被渲染。而其他部分作为一个整体字符串存储在缓存中。
t-nocache
与作用域根值(如 t-set 和 t-foreach) t-nocache 标签内的内容可用于文档说明,并解释为何添加此指令。该标签内的值具有局部作用域限制,这些值仅限于根级别的值(由控制器提供的值和/或调用 ir.qweb.render 方法时传递的值)。可以在 t-nocache 标签内的模板部分执行 t-set 操作,但这些设置的变量值不会在其他地方可用。
例:
<section> <t t-set="counter" t-value="counter * 10"/> <header t-nocache=""> <t t-set="counter" t-value="counter + 5"/> (views: <t t-out="counter"/>) </header> <article t-cache="record"> <title><t t-out="record.name"/> <i t-nocache="">(views: <t t-out="counter"/>)</i></titlle> <content t-out="record.description"/> </article> <footer>(views: <t t-out="counter"/>)</footer></section>
将呈现 (counter = 1):
<section> <header> (views: 6) </header> <article> <title>The record name <i>(views: 1)</i></titlle> <content>Record description</content> </article> <footer>(views: 10)</footer></section>
在此例中,包含<i>
标签的容器部分将始终进行渲染。而其余部分则作为一个单一字符串存储在缓存中。计数器不会因为<i>
标签外部的t-set
指令更新。
t-nocache-*
在缓存中添加一些基元值
为了能够使用模板中生成的值,可以 缓存它们。该指令用作 where is 所选值的名称和 Python 表达式,因此结果将 被缓存。缓存的值必须是基元类型。t-nocache-*=“expr”*expr
例:
<section t-cache="records"> <article t-foreach="records" t-as="record"> <header> <title t-field="record.get_method_title()"/> </header> <footer t-nocache="This part has a dynamic counter and must be rendered all the time." t-nocache-cached_value="record.get_base_counter()"> <span t-out="counter + cached_value"/> </footer> </article></section>
该值随着t-cache="records"所缓存的模板部分一起被缓存,并且每次都会添加到局部作用域内的根级变量中。
帮助
基于请求
QWeb
在 Python
端的大多数使用场景是在控制器中(以及HTTP请求期间),在这种情况下,存储在数据库中的模板(作为视图)可以通过调用 odoo.http.HttpRequest.render()
方法轻松地进行渲染。
response = http.request.render('my-template', { 'context_value': 42})
这会自动创建一个Response对象,可以 从控制器返回(或进一步定制以适合)。
基于视图
在之前的辅助方法更深层次上,有ir.qweb
对象上的_render
方法(使用可迭代的数据集)以及公共模块方法render
(不直接使用数据库):
_render(id[, values])
通过数据库ID或外部ID渲染一个QWeb视图/模板。模板会自动从记录中加载。
_prepare_environment
方法会在渲染环境上下文中设置多个预设的默认值,其中包含了附加组件(addons)及其所需的一些基本默认值。如果你想不采用这些默认值,可以像在公开的方法 http_routing 中那样使用 minimal_qcontext=False 参数来调用渲染函数:
request
当前Request对象(如果有)
debug
当前请求(如果有)是否处于模式debug
quote_plus
url 编码实用程序函数
json
对应的标准库模块
time
对应的标准库模块
datetime
对应的标准库模块
relativedelta
relativedelta 是一个来自 Python 的 dateutil 库中的类,用于处理日期和时间的相对偏移量。
keep_query
Helper 函数keep_query
render(template_name, values, load, **options)
load(ref)()返回 etree object ref
Javascript
专享指令
定义模板
指令只能被放置在模板文件的顶级(作为文档根元素的直接子元素):t-name
<templates> <t t-name="template-name"> <!-- template code --> </t></templates>
该指令不接受其他参数,但可以与元素或其他任何元素一起使用。当与元素结合使用时,应确保<t>元素仅有一个子元素。
模板名称是一个任意字符串,但在多个模板之间存在关联关系(例如调用子模板)时,通常会采用点分隔的方式来命名,以表示层级关系。
模板继承
模板继承用于:
就地更改现有模板,例如向模板添加信息
由其他模块创建。
从给定的父模板创建新模板模板继承通过使用两个指令来执行:
t-inherit
这是要从中继承的模板的名称, -t-inherit-mode
指令决定了继承的行为方式:它可以设置为 primary
,以从父模板创建一个新的子模板;或者设置为 extension
,以便在原地修改父模板内容。
此外,还可以选择性地指定一个 t-name
指令。在“primary”模式下使用时,它将作为新创建模板的名称;而在其他情况下,它会被添加为转换后模板上的注释,以帮助追溯继承关系。
对于模板本身的继承操作,更改是通过xpath指令完成的。有关所有可用指令的完整列表,请参阅XPath文档。
主要(子)模板继承示例:
<t t-name="child.template" t-inherit="base.template" t-inherit-mode="primary"> <xpath expr="//ul" position="inside"> <li>new element</li> </xpath></t>
扩展继承(就地转换):
<t t-inherit="base.template" t-inherit-mode="extension"> <xpath expr="//tr[1]" position="after"> <tr><td>new cell</td></tr> </xpath></t>
指令接受一个CSS选择器。该选择器应用于扩展的模板,用于选择将指定操作应用到的目标上下文节点:t-jqueryt-operation
append:将节点内容追加到目标上下文节点的末尾(即在上下文节点最后一个子节点之后)
prepend:将节点内容插入到目标上下文节点之前(即在上下文节点第一个子节点之前)
before:将节点内容直接插入到目标上下文节点之前
after:将节点内容直接插入到目标上下文节点之后
inner:用节点内容替换目标上下文节点的所有子节点
replace:使用节点内容替换目标上下文节点自身
attributes:节点内容应包含任意数量的元素,每个元素都有一个attributename
属性和一些文本内容,目标上下文节点的相应名称属性会被设置为指定的值(如果已存在则替换,否则添加)
无操作
如果未指定 t-operation,则模板内容会被解释为JavaScript代码,并以上下文节点作为 this 来执行。
警告: 尽管这种模式相比其他操作拥有更强大的功能,但调试和维护起来也更为困难。因此建议尽量避免使用该模式。
调试
javascript QWeb 实现提供了一些调试钩子:
t-log
指令接受一个表达式参数,在渲染过程中评估该表达式,并使用 console.log 将其结果记录到控制台。
<t t-set="foo" t-value="42"/><t t-log="foo"/>
将打印到控制台42
t-debug
在模板呈现期间触发调试器断点:
<t t-if="a_test"> <t t-debug=""/></t>
当调试处于激活状态时,将会停止执行(确切的停止条件取决于浏览器及其开发工具)。
t-js
该节点的内容是将在模板渲染期间执行的JavaScript代码。它接受一个名为
context
的参数,该参数指定了在t-js
标签体中可访问的渲染上下文名称。 <t t-set="foo" t-value="42"/><t t-js="ctx"> console.log("Foo is", ctx.foo);</t>
帮助
core.qweb(
core
指代模块)web.coreQWeb2.Engine()
的一个实例,其中已经加载了所有由模块定义的模板文件,并且包含了对标准辅助对象(如 underscore
)、翻译函数(如 _t
)和 JSON 对象的引用。 可以使用 core.qweb.render
来轻松地渲染基本的模块模板。
API
class QWeb2.Engine()
QWeb“渲染器”负责处理QWeb的大部分逻辑,包括加载、解析、编译和渲染模板。
在Odoo Web中,核心模块为用户实例化了一个QWeb,并将其导出到core.qweb
。同时,它还会将各个模块的所有模板文件加载到这个QWeb实例中。
一个QWeb2.Engine()
实例也充当着“模板命名空间”的角色。
QWeb2.Engine.QWeb2.Engine.render(template[, context])
将之前加载的模板渲染成一个字符串,并且在渲染过程中(如果提供的话)使用context
来查找模板中访问到的变量(例如要显示的字符串)。
参数
template (String()) – 要渲染的模板的名称
context(Object()) – 用于模板的基本命名空间 渲染
返回 String字符串
该引擎还公开了一个在某些情况下可能有用的其他方法(例如,在Odoo Web中,如果需要一个带有独立模板命名空间的环境,看板视图会获取它们自己的QWeb2.Engine()
实例,以避免与更通用的“模块”模板发生冲突):
QWeb2.Engine.QWeb2.Engine.add_template(templates)
在QWeb实例中加载一个模板文件(即一组模板)。这些模板可以以如下形式指定:
XML 字符串
QWeb 将尝试将其解析为 XML 文档,然后加载 它。
一个 URL
QWeb 将尝试下载 URL 内容,然后加载 生成的 XML 字符串。
一个XML文档或DocumentNode节点
QWeb将遍历文档的第一层级(即提供的根节点的子节点),并加载任何命名模板或模板覆盖。
一个QWeb2.Engine()还公开了多个属性,用于自定义其行为:
QWeb2.Engine.QWeb2.Engine.prefix
在解析过程中用于识别指令的前缀。类型为字符串,默认值为 .t
。
QWeb2.Engine.QWeb2.Engine.debug
布尔标志,用于将引擎置于“调试模式”。通常情况下,QWeb会在模板执行期间截获所有引发的错误。而在调试模式下,它不会拦截任何异常,而是让它们直接通过。
QWeb2.Engine.QWeb2.Engine.jQuery
在模板继承处理过程中使用的jQuery实例,默认为 window.jQuery
。
QWeb2.Engine.QWeb2.Engine.preprocess_node
一个函数。如果存在,则会在将每个DOM节点编译为模板代码之前调用此函数。在Odoo Web中,该函数用于自动翻译模板中的文本内容和某些属性。默认值为 Function
或 null
。
它在这一点上与Genshi相似,尽管它并不使用(也不支持)XML命名空间。
虽然它还使用了其他一些模板引擎,要么是因为历史原因,要么是因为它们对于特定用例来说仍然是更好的选择。Odoo 9.0版本仍然依赖于Jinja和Mako这两个模板引擎。