3.1 块和替换
当你花时间为你的 Web 应用建立和制定模板时,希望像你的后端 Python 代码一样重用你的前端代码似乎只是合逻辑的,不是吗?幸运的是,Tornado 可以让你做到这一点。Tornado 通过 extends 和 block 语句支持模板继承,这就让你拥有了编写能够在合适的地方复用的流体模板的控制权和灵活性。
为了扩展一个已经存在的模板,你只需要在新的模板文件的顶部放上一句 {% extends "filename.html" %}。
比如,为了在新模板中扩展一个父模板(在这里假设为 main.html),你可以这样使用:
{% extends "main.html" %}
这就使得新文件继承 main.html 的所有标签,并且覆写为期望的内容。
3.1.1 块基础
扩展一个模板使你复用之前写过的代码更加简单,但是这并不会为你提供所有的东西,除非你可以适应并改变那些之前的模板。所以,block 语句出现了。
一个块语句压缩了一些当你扩展时可能想要改变的模板元素。比如,为了使用一个能够根据不同页覆写的动态 header 块,你可以在父模板 main.html 中添加如下代码:
<header>
{% block header %}{% end %}
</header>
然后,为了在子模板 index.html 中覆写 {% block header %}{% end %}
部分,你可以使用块的名字引用,并把任何你想要的内容放到其中。
{% block header %}{% end %}
{% block header %}
<h1>Hello world!</h1>
{% end %}
任何继承这个模板的文件都可以包含它自己的 {% block header %}
和 {% end %}
,然后把一些不同的东西加进去。
为了在 Web 应用中调用这个子模板,你可以在你的 Python 脚本中很轻松地渲染它,就像之前你渲染其他模板那样:
class MainHandler(tornado.web.RequestHandler):
def get(self):
self.render("index.html")
所以此时,main.html 中的 body 块在加载时会被以 index.html 中的信息"Hello world!"填充(参见图 3-1)。
图 3-1 Hello world!
我们已经可以看到这种方法在处理整体页面结构和节约多页面网站的开发时间上多么有用。更好的是,你可以为每个页面使用多个块,此时像 header 和 footer 这样的动态元素将会被包含在同一个流程中。
下面是一个在父模板 main.html 中使用多个块的例子:
<html>
<body>
<header>
{% block header %}{% end %}
</header>
<content>
{% block body %}{% end %}
</content>
<footer>
{% block footer %}{% end %}
</footer>
</body>
</html>
当我们扩展父模板 main.html 时,可以在子模板 index.html 中引用这些块。
{% extends "main.html" %}
{% block header %}
<h1>{{ header_text }}</h1>
{% end %}
{% block body %}
<p>Hello from the child template!</p>
{% end %}
{% block footer %}
<p>{{ footer_text }}</p>
{% end %}
用来加载模板的 Python 脚本和上一个例子差不多,不过在这里我们传递了几个字符串变量给模板使用(如图 3-2):
class MainHandler(tornado.web.RequestHandler):
def get(self):
self.render(
"index.html",
header_text = "Header goes here",
footer_text = "Footer goes here"
)
图 3-2 块基础
你也可以保留父模板块语句中的默认文本和标记,就像扩展模板没有指定它自己的块版本一样被渲染。这种情况下,你可以根据某页的情况只替换必须的东西,这在包含或替换脚本、CSS 文件和标记块时非常有用。
正如模板文档所记录的,"错误报告目前...呃...是非常有意思的"。一个语法错误或者没有闭合的{% block %}语句可以使得浏览器直接显示 500: Internal Server Error(如果你运行在 debug 模式下会引发完整的 Python 堆栈跟踪)。如图 3-3 所示。
总之,为了你自己好的话,你需要使自己的模板尽可能的鲁棒,并且在模板被渲染之前发现错误。
图 3-3 块错误
3.1.2 模板练习:Burt's Book
所以,你会认为这听起来很有趣,但却不能描绘出在一个标准的 Web 应用中如何使用?那么让我们在这里看一个例子,我们的朋友 Burt 希望运行一个名叫 Burt's Books 的书店。
Burt 通过他的书店卖很多书,他的网站会展示很多不同的内容,比如新品推荐、商店信息等等。Burt 希望有一个固定的外观和感觉的网站,同时也能更简单的更新页面和段落。
为了做到这些,Burt's Book 使用了以 Tornado 为基础的网站,其中包括一个拥有样式、布局和 header/footer 细节的主模版,以及一个处理页面的轻量级的子模板。在这个系统中,Burt 可以把最新发布、员工推荐、即将发行等不同页面编写在一起,共同使用通用的基础属性。
Burt's Book 的网站使用一个叫作 main.html 的主要基础模板,用来包含网站的通用架构,如下面的代码所示:
<html>
<head>
<title>{{ page_title }}</title>
<link rel="stylesheet" href="{{ static_url("css/style.css") }}" />
</head>
<body>
<div id="container">
<header>
{% block header %}<h1>Burt's Books</h1>{% end %}
</header>
<div id="main">
<div id="content">
{% block body %}{% end %}
</div>
</div>
<footer>
{% block footer %}
<p>
For more information about our selection, hours or events, please email us at
<a href="mailto:contact@burtsbooks.com">contact@burtsbooks.com</a>.
</p>
{% end %}
</footer>
</div>
<script src="{{ static_url("js/script.js") }}"></script>
</body>
</html>
这个页面定义了结构,应用了一个 CSS 样式表,并加载了主要的 JavaScript 文件。其他模板可以扩展它,在必要时替换 header、body 和 footer 块。
这个网站的 index 页(index.html)欢迎友好的网站访问者并提供一些商店的信息。通过扩展 main.html,这个文件只需要包括用于替换默认文本的 header 和 body 块的信息。
{% extends "main.html" %}
{% block header %}
<h1>{{ header_text }}</h1>
{% end %}
{% block body %}
<div id="hello">
<p>Welcome to Burt's Books!</p>
<p>...</p>
</div>
{% end %}
在 footer 块中,这个文件使用了 Tornado 模板的默认行为,继承了来自父模板的联系信息。
为了运作网站,传递信息给 index 模板,下面给出 Burt's Book 的 Python 脚本(main.py):
import tornado.web
import tornado.httpserver
import tornado.ioloop
import tornado.options
import os.path
from tornado.options import define, options
define("port", default=8000, help="run on the given port", type=int)
class Application(tornado.web.Application):
def __init__(self):
handlers = [
(r"/", MainHandler),
]
settings = dict(
template_path=os.path.join(os.path.dirname(__file__), "templates"),
static_path=os.path.join(os.path.dirname(__file__), "static"),
debug=True,
)
tornado.web.Application.__init__(self, handlers, **settings)
class MainHandler(tornado.web.RequestHandler):
def get(self):
self.render(
"index.html",
page_title = "Burt's Books | Home",
header_text = "Welcome to Burt's Books!",
)
if __name__ == "__main__":
tornado.options.parse_command_line()
http_server = tornado.httpserver.HTTPServer(Application())
http_server.listen(options.port)
tornado.ioloop.IOLoop.instance().start()
这个例子的结构和我们之前见到的不太一样,但你一点都不需要害怕。我们不再像之前那样通过使用一个处理类列表和一些其他关键字参数调用 tornado.web.Application 的构造函数来创建实例,而是定义了我们自己的 Application 子类,在这里我们简单地称之为 Application。在我们定义的 init 方法中,我们创建了处理类列表以及一个设置的字典,然后在初始化子类的调用中传递这些值,就像下面的代码一样:
tornado.web.Application.__init__(self, handlers, **settings)
所以在这个系统中,Burt's Book 可以很容易地改变 index 页面并保持基础模板在其他页面被使用时完好。此外,他们可以充分利用 Tornado 的真实能量,由 Python 脚本和/或数据库提供动态内容。我们将在之后看到更多相关的内容。
3.1.3 自动转义
Tornado 默认会自动转义模板中的内容,把标签转换为相应的 HTML 实体。这样可以防止后端为数据库的网站被恶意脚本攻击。比如,你的网站中有一个评论部分,用户可以在这里添加任何他们想说的文字进行讨论。虽然一些 HTML 标签在标记和样式冲突时不构成重大威胁(如评论中没有闭标签),但标签会允许攻击者加载其他的 JavaScript 文件,打开通向跨站脚本攻击、XSS 或漏洞之门。
让我们考虑 Burt's Book 网站上的一个用户反馈页面。Melvin,今天感觉特别邪恶,在评论里提交了下面的文字:
Totally hacked your site lulz <script>alert('RUNNING EVIL H4CKS AND SPL01TS NOW...')</script>
当我们在没有转义用户内容的情况下给一个不知情的用户构建页面时,脚本标签被作为一个 HTML 元素解释,并被浏览器执行,所以 Alice 看到了如图 3-4 所示的提示窗口。幸亏 Tornado 会自动转义在双大括号间被渲染的表达式。更早地转义 Melvin 输入的文本不会激活 HTML 标签,并且会渲染为下面的字符串:
Totally hacked your site lulz <script>alert('RUNNING EVIL H4CKS AND SPL01TS NOW...')</script>
图 3-4 网站漏洞问题
现在当 Alice 访问网站时,没有恶意脚本被执行,所以她看到的页面如图 3-5 所示。
图 3-5 网站漏洞问题--解决
在 Tornado1.x 版本中,模板没有被自动转义,所以我们之前谈论的防护措施需要显式地在未过滤的用户输入上调用 escape() 函数。
所以在这里,我们可以看到自动转义是如何防止你的访客进行恶意攻击的。然而,当通过模板和模块提供 HTML 动态内容时它仍会让你措手不及。
举个例子,如果 Burt 想在 footer 中使用模板变量设置 email 联系链接,他将不会得到期望的 HTML 链接。考虑下面的模板片段:
{% set mailLink = "<a href="mailto:contact@burtsbooks.com">Contact Us</a>" %}
{{ mailLink }}'
它会在页面源代码中渲染成如下代码:
<a href="mailto:contact@burtsbooks.com">Contact Us</a>
此时自动转义被运行了,很明显,这无法让人们联系上 Burt。
为了处理这种情况,你可以禁用自动转义,一种方法是在 Application 构造函数中传递 autoescape=None,另一种方法是在每页的基础上修改自动转义行为,如下所示:
{% autoescape None %}
{{ mailLink }}
这些 autoescape 块不需要结束标签,并且可以设置 xhtml_escape 来开启自动转义(默认行为),或 None 来关闭。
然而,在理想的情况下,你希望保持自动转义开启以便继续防护你的网站。因此,你可以使用{% raw %}指令来输出不转义的内容。
{% raw mailLink %}
需要特别注意的是,当你使用诸如 Tornado 的 linkify() 和 xsrf_form_html() 函数时,自动转义的设置被改变了。所以如果你希望在前面代码的 footer 中使用 linkify() 来包含链接,你可以使用一个{% raw %}块:
{% block footer %}
<p>
For more information about our selection, hours or events, please email us at
<a href="mailto:contact@burtsbooks.com">contact@burtsbooks.com</a>.
</p>
<p class="small">
Follow us on Facebook at
{% raw linkify("https://fb.me/burtsbooks", extra_params='ref=website') %}.
</p>
{% end %}
这样,你可以既利用 linkify() 简记的好处,又可以保持在其他地方自动转义的好处。
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论