使用 twisted.web.template 进行 HTML 模板化¶
Python 模板化的快速入门¶
HTML 模板化是将模板文档(描述样式和结构,但本身不包含任何内容)转换为包含应用程序中对象信息的 HTML 输出的过程。Python 中有很多用于执行此操作的库:举几个例子,Jinja2 和 Django 模板。您可以在 Twisted Web 应用程序中轻松使用这些库中的任何一个,方法是将它们作为 WSGI 应用程序 运行,或者通过调用您首选的模板系统 API 来生成其输出作为字符串,然后将这些字符串写入 Request.write
。
在我们开始解释如何使用它之前,我想强调的是,如果您更喜欢其他方法来生成 HTML,则不需要使用 Twisted 的模板系统。如果您觉得它适合您的个人风格或应用程序,可以使用它,但您可以随意使用其他东西。Twisted 包含模板用于自身使用,因为 twisted.web
服务器需要在各种地方生成 HTML,我们不想为此添加另一个大型依赖项。Twisted 与其他系统完全兼容,因此这与我们使用自己的系统无关。
twisted.web.template - 为什么以及如何使用它¶
Twisted 包含一个模板系统,twisted.web.template
。这对于想要为 Web 界面生成一些基本 HTML 的 Twisted 应用程序来说非常方便,无需额外的依赖项。
twisted.web.template
还包括对 Deferred
的支持,因此您可以根据应用程序返回的 Deferred
的结果,逐步渲染页面的输出。此功能在模板库中相当独特。
在 twisted.web.template
中,模板是 XHTML 文件,其中还包含一个特殊命名空间,用于指示文档的动态部分。例如
<html xmlns:t="http://twistedmatrix.com/ns/twisted.web.template/0.1">
<body>
<div t:render="header" />
<div id="content">
<p>Content goes here.</p>
</div>
<div t:render="footer" />
</body>
</html>
模板的基本单元是 twisted.web.template.Element
。Element 被赋予加载上述示例中一小段标记的方法,并且知道如何将标记中的 render
属性与使用 twisted.web.template.renderer()
公开的 Python 方法相关联。
from twisted.python.filepath import FilePath
from twisted.web.template import Element, XMLFile, renderer
class ExampleElement(Element):
loader = XMLFile(FilePath("template-1.xml"))
@renderer
def header(self, request, tag):
return tag("Header.")
@renderer
def footer(self, request, tag):
return tag("Footer.")
为了将两者结合起来,我们必须渲染元素。对于这个简单的示例,我们可以使用 flattenString
API,它将单个模板对象(例如 Element
)转换为一个 Deferred
,该 Deferred
使用单个字符串(渲染过程的 HTML 输出)触发。
from element_1 import ExampleElement
from twisted.web.template import flattenString
def renderDone(output):
print(output)
flattenString(None, ExampleElement()).addCallback(renderDone)
这个简短的程序有点作弊;我们知道模板中没有 Deferred
需要 reactor 最终触发;因此,我们可以简单地添加一个回调,它输出结果。此外,没有一个 renderer
函数需要 request
对象,因此在这里传递 None
是可以接受的。(这里的“request”对象仅用于将有关渲染过程的信息传递给每个渲染器,因此您可以始终使用对您的应用程序有意义的任何对象。但是,请注意,来自库代码的渲染器可能需要一个 IRequest
。)
如果您自己运行它,您会看到它会生成以下输出
<html>
<body>
<div>Header.</div>
<div id="content">
<p>Content goes here.</p>
</div>
<div>Footer.</div>
</body>
</html>
渲染器方法的第三个参数是 Tag
对象,它表示模板中具有 t:render
属性的 XML 元素。调用 Tag
会在 DOM 中的元素中添加子元素,这些子元素可以是字符串、更多 Tag
或其他可渲染对象,例如 Element
。例如,要使标题和页脚变为粗体
from twisted.python.filepath import FilePath
from twisted.web.template import Element, XMLFile, renderer, tags
class ExampleElement(Element):
loader = XMLFile(FilePath("template-1.xml"))
@renderer
def header(self, request, tag):
return tag(tags.b("Header."))
@renderer
def footer(self, request, tag):
return tag(tags.b("Footer."))
以类似于第一个示例的方式渲染它将生成
<html>
<body>
<div><b>Header.</b></div>
<div id="content">
<p>Content goes here.</p>
</div>
<div><b>Footer.</b></div>
</body>
</html>
除了添加子元素之外,还可以使用调用语法在标签上设置属性。例如,要更改 div
上的 id
并添加子元素
from twisted.python.filepath import FilePath
from twisted.web.template import Element, XMLFile, renderer, tags
class ExampleElement(Element):
loader = XMLFile(FilePath("template-1.xml"))
@renderer
def header(self, request, tag):
return tag(tags.p("Header."), id="header")
@renderer
def footer(self, request, tag):
return tag(tags.p("Footer."), id="footer")
这将生成以下页面
<html>
<body>
<div id="header"><p>Header.</p></div>
<div id="content">
<p>Content goes here.</p>
</div>
<div id="footer"><p>Footer.</p></div>
</body>
</html>
调用标签会修改它,并返回标签本身,因此如果您有多个子元素或属性要添加到标签中,您可以将它传递下去并多次调用它。 twisted.web.template
还公开了一些方便的对象,用于在 tags
对象中从渲染器方法内部构建更复杂的标记结构。在上面的示例中,我们只使用了 tags.p
和 tags.b
,但应该有一个 tags.x
用于每个有效的 HTML 标签 x 。可能有一些遗漏,但如果您发现一个,请随时提交错误。
模板属性¶
t:attr
标签允许您在封闭元素上设置 HTML 属性(例如 href
在 <a href="...
中)。
插槽¶
t:slot
标签允许您指定“插槽”,您可以方便地使用来自 Python 程序的多个数据片段填充这些插槽。
以下示例演示了 t:attr
和 t:slot
的实际应用。这里我们有一个布局,它在您新潮的 Twisted 驱动的社交网络网站上显示一个人的个人资料。我们使用 t:attr
标签将个人资料图片上的“src”属性插入,其中 src 属性的实际值由 t:slot
标签在 t:attr
标签内部指定。困惑吗?当您看到代码时,它应该更有意义
<div xmlns:t="http://twistedmatrix.com/ns/twisted.web.template/0.1"
t:render="person_profile"
class="profile">
<img><t:attr name="src"><t:slot name="profile_image_url" /></t:attr></img>
<p><t:slot name="person_name" /></p>
</div>
from twisted.python.filepath import FilePath
from twisted.web.template import Element, XMLFile, renderer
class ExampleElement(Element):
loader = XMLFile(FilePath("slots-attributes-1.xml"))
@renderer
def person_profile(self, request, tag):
# Note how convenient it is to pass these attributes in!
tag.fillSlots(
person_name="Luke", profile_image_url="http://example.com/user.png"
)
return tag
<div class="profile">
<img src="http://example.com/user.png" />
<p>Luke</p>
</div>
迭代¶
通常,您将有一系列内容,并希望渲染它们中的每一个,为每一个内容重复模板的一部分。这可以通过在渲染器中克隆 tag
来完成
<ul xmlns:t="http://twistedmatrix.com/ns/twisted.web.template/0.1">
<li t:render="widgets"><t:slot name="widgetName"/></li>
</ul>
from twisted.python.filepath import FilePath
from twisted.web.template import Element, XMLFile, flattenString, renderer
class WidgetsElement(Element):
loader = XMLFile(FilePath("iteration-1.xml"))
widgetData = ["gadget", "contraption", "gizmo", "doohickey"]
@renderer
def widgets(self, request, tag):
for widget in self.widgetData:
yield tag.clone().fillSlots(widgetName=widget)
def printResult(result):
print(result)
flattenString(None, WidgetsElement()).addCallback(printResult)
<ul>
<li>gadget</li><li>contraption</li><li>gizmo</li><li>doohickey</li>
</ul>
这个渲染器之所以有效,是因为渲染器可以返回任何可以渲染的东西,而不仅仅是 tag
。在本例中,我们定义了一个生成器,它返回一个可迭代的东西。我们也可以返回一个 list
。任何可迭代的东西都将被 twisted.web.template
渲染,它会渲染其中的每个项目。在本例中,每个项目都是渲染器接收的标签的副本,每个副本都填充了小部件的名称。
子视图¶
另一个常见的模式是将页面一小部分的渲染逻辑委托给一个单独的 Element
。例如,上面迭代示例中的小部件可能更复杂。您可以定义一个 Element
子类,它可以渲染单个小部件。然后,容器上的渲染器方法可以生成此新 Element
子类的实例。
<ul xmlns:t="http://twistedmatrix.com/ns/twisted.web.template/0.1">
<li t:render="widgets"><span t:render="name" /></li>
</ul>
from twisted.python.filepath import FilePath
from twisted.web.template import Element, TagLoader, XMLFile, flattenString, renderer
class WidgetsElement(Element):
loader = XMLFile(FilePath("subviews-1.xml"))
widgetData = ["gadget", "contraption", "gizmo", "doohickey"]
@renderer
def widgets(self, request, tag):
for widget in self.widgetData:
yield WidgetElement(TagLoader(tag), widget)
class WidgetElement(Element):
def __init__(self, loader, name):
Element.__init__(self, loader)
self._name = name
@renderer
def name(self, request, tag):
return tag(self._name)
def printResult(result):
print(result)
flattenString(None, WidgetsElement()).addCallback(printResult)
<ul>
<li><span>gadget</span></li><li><span>contraption</span></li><li><span>gizmo</span></li><li><span>doohickey</span></li>
</ul>
TagLoader
允许与小部件相关的整体模板的一部分被重新用于 WidgetElement
,它本质上是一个普通的 Element
子类,与 WidgetsElement
并没有太大区别。请注意,此模板中 span
标签上的 name 渲染器是从 WidgetElement
满足的,而不是 WidgetsElement
。
透明¶
请注意渲染器、插槽和属性如何要求您在某个外部 HTML 元素上指定渲染器。如果您不想被迫在 DOM 中添加元素只是为了将一些内容放入其中,该怎么办?也许它会弄乱您的布局,并且您无法在 IE 中使用那个额外的 div
标签来使其工作?也许您需要 t:transparent
,它允许您在没有任何周围的“容器”标签的情况下放置一些内容。例如
<div xmlns:t="http://twistedmatrix.com/ns/twisted.web.template/0.1">
<!-- layout decision - these things need to be *siblings* -->
<t:transparent t:render="renderer1" />
<t:transparent t:render="renderer2" />
</div>
from twisted.python.filepath import FilePath
from twisted.web.template import Element, XMLFile, renderer
class ExampleElement(Element):
loader = XMLFile(FilePath("transparent-1.xml"))
@renderer
def renderer1(self, request, tag):
return tag("hello")
@renderer
def renderer2(self, request, tag):
return tag("world")
<div>
<!-- layout decision - these things need to be *siblings* -->
hello
world
</div>
引用¶
twisted.web.template
将引用放置到 DOM 中的任何字符串。这提供了针对 XSS 攻击 的保护,除了通常使将任意字符串放到网页上变得容易之外,无需担心它们可能包含的内容。这可以通过使用我们之前示例中相同模板的元素轻松演示。这是一个元素,它在 HTML 中返回一些“特殊”字符(‘<’,‘>’ 和 ‘”’,在属性值中是特殊的)
from twisted.python.filepath import FilePath
from twisted.web.template import Element, XMLFile, renderer
class ExampleElement(Element):
loader = XMLFile(FilePath("template-1.xml"))
@renderer
def header(self, request, tag):
return tag("<<<Header>>>!")
@renderer
def footer(self, request, tag):
return tag('>>>"Footer!"<<<', id='<"fun">')
请注意,它们都在输出中被安全地引用,并且将在 Web 浏览器中按您从 Python 方法返回的方式显示。
<html>
<body>
<div><<<Header>>>!</div>
<div id="content">
<p>Content goes here.</p>
</div>
<div id="<"fun">">>>>"Footer!"<<<</div>
</body>
</html>
Deferreds¶
最后,一个简单的 Deferred 支持演示,这是 twisted.web.template
的独特功能。简而言之,任何渲染器都可以返回一个 Deferred,该 Deferred 会使用一些模板内容而不是模板内容本身进行触发。如上所示,flattenString
将返回一个 Deferred,该 Deferred 会使用字符串的完整内容进行触发。但是,如果内容很多,您可能不想在开始将部分内容发送到您的 HTTP 客户端之前等待:在这种情况下,您可以使用 flatten
。很难在基于浏览器的应用程序中直接演示这一点;除非您在触发 Deferred 之前插入很长的延迟,否则它看起来就像您的浏览器立即显示了所有内容。这是一个示例,它只打印一些 HTML 模板,并在某些事件发生的地方插入标记
import sys
from twisted.internet.defer import Deferred
from twisted.web.template import Element, XMLString, flatten, renderer
sample = XMLString(
"""
<div xmlns:t="http://twistedmatrix.com/ns/twisted.web.template/0.1">
Before waiting ...
<span t:render="wait"></span>
... after waiting.
</div>
"""
)
class WaitForIt(Element):
def __init__(self):
Element.__init__(self, loader=sample)
self.deferred = Deferred()
@renderer
def wait(self, request, tag):
return self.deferred.addCallback(lambda aValue: tag("A value: " + repr(aValue)))
def done(ignore):
print("[[[Deferred fired.]]]")
print("[[[Rendering the template.]]]")
it = WaitForIt()
flatten(None, it, sys.stdout.write).addCallback(done)
print("[[[In progress... now firing the Deferred.]]]")
it.deferred.callback("<value>")
print("[[[All done.]]]")
如果您运行此示例,您应该获得以下输出
[[[Rendering the template.]]]
<div>
Before waiting ...
[[[In progress... now firing the Deferred.]]]
<span>A value: '<value>'</span>
... after waiting.
</div>[[[Deferred fired.]]]
[[[All done.]]]
这表明部分输出(直到“[[[In progress...
”)在渲染时立即写出。但是,一旦它遇到 Deferred,WaitForIt
的渲染需要暂停,直到在该 Deferred 上调用 .callback(...)
。您可以看到,在指示 Deferred 正在触发的消息完成之前,不会产生任何进一步的输出。通过返回 Deferred 并使用 flatten
,您可以避免缓冲大量数据。
关于格式和 DOCTYPE 的简短说明¶
twisted.web.template
的目标是同时发出有效的 HTML 或 XHTML 。但是,为了获得您想要的最大程度的标准兼容输出格式,您必须知道您想要哪一个,并采取一些简单的步骤来正确发出它。如果您完全忽略本节,许多浏览器可能会与大多数输出一起工作,但 HTML 规范建议您指定适当的 DOCTYPE 。
由于模板中的 DOCTYPE
声明将描述模板本身,而不是其输出,因此它不会包含在您的输出中。如果您希望使用 DOCTYPE 对模板输出进行注释,则必须将其带外写入浏览器。一种方法是在您准备开始发出响应时简单地执行 request.write('<!DOCTYPE html>\n')
。XML DOCTYPE
声明也是如此。
twisted.web.template
将删除用于声明 http://twistedmatrix.com/ns/twisted.web.template/0.1
命名空间的 xmlns
属性,但它不会修改其他命名空间声明属性。因此,如果您希望以 HTML 格式序列化,则不应使用其他命名空间;如果您希望序列化为 XML,请随时插入任何合适的命名空间声明,它们将出现在您的输出中。
注意
这种宽松的方法在许多情况下是正确的。但是,在某些情况下,尤其是 <script> 和 <style> 标签,引用规则在 HTML 和 XML 之间以及 HTML 中不同浏览器解析器之间存在很大差异。如果您想在脚本或样式表中生成动态内容,最好的选择是外部加载资源,这样您就不必担心引用规则。第二好的选择是严格配置您的内容类型和 DOCTYPE 声明以用于 XML,其引用规则很简单,并且与 twisted.web.template
采用的方法兼容。并且,请记住:无论您如何放置它,放置在 <script> 或 <style> 标签中的任何用户输入都是潜在的安全问题。
一点历史¶
使用过 Divmod Nevow 的人可能会注意到一些相似之处。 twisted.web.template
实际上源自 Nevow 的最新版本,但只包含 Nevow 渲染管道中的最新组件,并且没有 Nevow 随着时间的推移而积累的任何遗留兼容层。这应该使使用 twisted.web.template
对于许多长期使用 Twisted 的用户来说是一种类似的体验,这些用户以前使用过 Nevow 的 Twisted 友好模板,但对于新用户来说更简单。