使用 twisted.web.template 进行 HTML 模板化

Python 模板化的快速入门

HTML 模板化是将模板文档(描述样式和结构,但本身不包含任何内容)转换为包含应用程序中对象信息的 HTML 输出的过程。Python 中有很多用于执行此操作的库:举几个例子,Jinja2Django 模板。您可以在 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 文件,其中还包含一个特殊命名空间,用于指示文档的动态部分。例如

template-1.xml

<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 方法相关联。

element_1.py

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 输出)触发。

render_1.py

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 。)

如果您自己运行它,您会看到它会生成以下输出

output-1.html

<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 。例如,要使标题和页脚变为粗体

element_2.py

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."))

以类似于第一个示例的方式渲染它将生成

output-2.html

<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 并添加子元素

element_3.py

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")

这将生成以下页面

output-3.html

<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.ptags.b ,但应该有一个 tags.x 用于每个有效的 HTML 标签 x 。可能有一些遗漏,但如果您发现一个,请随时提交错误。

模板属性

t:attr 标签允许您在封闭元素上设置 HTML 属性(例如 href<a href="... 中)。

插槽

t:slot 标签允许您指定“插槽”,您可以方便地使用来自 Python 程序的多个数据片段填充这些插槽。

以下示例演示了 t:attrt:slot 的实际应用。这里我们有一个布局,它在您新潮的 Twisted 驱动的社交网络网站上显示一个人的个人资料。我们使用 t:attr 标签将个人资料图片上的“src”属性插入,其中 src 属性的实际值由 t:slot 标签在 t:attr 标签内部指定。困惑吗?当您看到代码时,它应该更有意义

slots-attributes-1.xml

<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>

slots_attributes_1.py

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

slots-attributes-output.html

<div class="profile">
<img src="http://example.com/user.png" /> 
<p>Luke</p>
</div>

迭代

通常,您将有一系列内容,并希望渲染它们中的每一个,为每一个内容重复模板的一部分。这可以通过在渲染器中克隆 tag 来完成

iteration-1.xml

<ul xmlns:t="http://twistedmatrix.com/ns/twisted.web.template/0.1">
    <li t:render="widgets"><t:slot name="widgetName"/></li>
</ul>

iteration-1.py

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)

iteration-output-1.xml

<ul>
    <li>gadget</li><li>contraption</li><li>gizmo</li><li>doohickey</li>
</ul>

这个渲染器之所以有效,是因为渲染器可以返回任何可以渲染的东西,而不仅仅是 tag 。在本例中,我们定义了一个生成器,它返回一个可迭代的东西。我们也可以返回一个 list 。任何可迭代的东西都将被 twisted.web.template 渲染,它会渲染其中的每个项目。在本例中,每个项目都是渲染器接收的标签的副本,每个副本都填充了小部件的名称。

子视图

另一个常见的模式是将页面一小部分的渲染逻辑委托给一个单独的 Element 。例如,上面迭代示例中的小部件可能更复杂。您可以定义一个 Element 子类,它可以渲染单个小部件。然后,容器上的渲染器方法可以生成此新 Element 子类的实例。

subviews-1.xml

<ul xmlns:t="http://twistedmatrix.com/ns/twisted.web.template/0.1">
    <li t:render="widgets"><span t:render="name" /></li>
</ul>

subviews-1.py

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)

subviews-output-1.xml

<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 ,它允许您在没有任何周围的“容器”标签的情况下放置一些内容。例如

transparent-1.xml

<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>

transparent_element.py

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")

transparent-output.html

<div>
<!-- layout decision - these things need to be *siblings* -->
hello
world
</div>

引用

twisted.web.template 将引用放置到 DOM 中的任何字符串。这提供了针对 XSS 攻击 的保护,除了通常使将任意字符串放到网页上变得容易之外,无需担心它们可能包含的内容。这可以通过使用我们之前示例中相同模板的元素轻松演示。这是一个元素,它在 HTML 中返回一些“特殊”字符(‘<’,‘>’ 和 ‘”’,在属性值中是特殊的)

quoting_element.py

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 方法返回的方式显示。

quoting-output.html

<html>
<body>
  <div>&lt;&lt;&lt;Header&gt;&gt;&gt;!</div>
  <div id="content">
    <p>Content goes here.</p>
  </div>
  <div id="&lt;&quot;fun&quot;&gt;">&gt;&gt;&gt;"Footer!"&lt;&lt;&lt;</div>
</body>
</html>

Deferreds

最后,一个简单的 Deferred 支持演示,这是 twisted.web.template 的独特功能。简而言之,任何渲染器都可以返回一个 Deferred,该 Deferred 会使用一些模板内容而不是模板内容本身进行触发。如上所示,flattenString 将返回一个 Deferred,该 Deferred 会使用字符串的完整内容进行触发。但是,如果内容很多,您可能不想在开始将部分内容发送到您的 HTTP 客户端之前等待:在这种情况下,您可以使用 flatten 。很难在基于浏览器的应用程序中直接演示这一点;除非您在触发 Deferred 之前插入很长的延迟,否则它看起来就像您的浏览器立即显示了所有内容。这是一个示例,它只打印一些 HTML 模板,并在某些事件发生的地方插入标记

wait_for_it.py

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.]]]")

如果您运行此示例,您应该获得以下输出

waited-for-it.html

[[[Rendering the template.]]]
<div>
    Before waiting ...
    [[[In progress... now firing the Deferred.]]]
<span>A value: '&lt;value&gt;'</span>
    ... after waiting.
    </div>[[[Deferred fired.]]]
[[[All done.]]]

这表明部分输出(直到“[[[In progress... ”)在渲染时立即写出。但是,一旦它遇到 Deferred,WaitForIt 的渲染需要暂停,直到在该 Deferred 上调用 .callback(...) 。您可以看到,在指示 Deferred 正在触发的消息完成之前,不会产生任何进一步的输出。通过返回 Deferred 并使用 flatten ,您可以避免缓冲大量数据。

关于格式和 DOCTYPE 的简短说明

twisted.web.template 的目标是同时发出有效的 HTMLXHTML 。但是,为了获得您想要的最大程度的标准兼容输出格式,您必须知道您想要哪一个,并采取一些简单的步骤来正确发出它。如果您完全忽略本节,许多浏览器可能会与大多数输出一起工作,但 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 友好模板,但对于新用户来说更简单。