tornado学习二:表单和模板
Tornado自身也提供了一个轻量级、快速并且灵活的模板语言,在tornado.template模块中。
一、示例
示例:Poem Maker Pro这个Web应用有一个让用户填写的HTML表单,然后处理表单的结果。
# 代码清单2-1 简单表单和模板:poemmaker.py import os.path import tornado.httpserver import tornado.ioloop import tornado.options import tornado.web from tornado.options import define, options define("port", default=8000, help="run on the given port", type=int) class IndexHandler(tornado.web.RequestHandler): def get(self): self.render('index.html') class PoemPageHandler(tornado.web.RequestHandler): def post(self): noun1 = self.get_argument('noun1') noun2 = self.get_argument('noun2') verb = self.get_argument('verb') noun3 = self.get_argument('noun3') self.render('poem.html', roads=noun1, wood=noun2, made=verb, difference=noun3) if __name__ == '__main__': tornado.options.parse_command_line() app = tornado.web.Application( handlers=[(r'/', IndexHandler), (r'/poem', PoemPageHandler)], template_path=os.path.join(os.path.dirname(__file__), "templates") ) http_server = tornado.httpserver.HTTPServer(app) http_server.listen(options.port) tornado.ioloop.IOLoop.instance().start()
在以上代码中,Application传入参数template_path,并在响应时使用到了index.html和poem.html模板。因此,还需要定义这两个模板
代码清单2-2 Poem Maker表单:index.html <!DOCTYPE html> <html> <head><title>Poem Maker Pro</title></head> <body> <h1>Enter terms below.</h1> <form method="post" action="/poem"> <p>Plural noun<br><input type="text" name="noun1"></p> <p>Singular noun<br><input type="text" name="noun2"></p> <p>Verb (past tense)<br><input type="text" name="verb"></p> <p>Noun<br><input type="text" name="noun3"></p> <input type="submit"> </form> </body> </html> 代码清单2-3 Poem Maker模板:poem.html <!DOCTYPE html> <html> <head><title>Poem Maker Pro</title></head> <body> <h1>Your poem</h1> <p>Two {{roads}} diverged in a {{wood}}, and I—<br> I took the one less travelled by,<br> And that has {{made}} all the {{difference}}.</p> </body> </html>
在命令行执行下述命令:
$ python poemmaker.py --port=8000
现在,在浏览器中打开http://localhost:8000。当浏览器请求根目录(/)时,Tornado程序将渲染index.html,展示如图2-1所示的简单HTML表单。
图2-1 Poem Maker Pro:输入表单
这个表单包括多个文本域(命名为noun1、noun2等),其中的内容将在用户点击"Submit"按钮时以POST请求的方式送到/poem
。
为了响应这个POST请求,Tornado应用跳转到poem.html,插入你在表单中填写的值。结果是Robert Frost的诗《The Road Not Taken》的轻微修改版本。图2-2展示了这个结果。
图2-2 Poem Maker Pro:输出
二、 渲染模板
在以上示例中,我们向Application对象的__init__方法传递了一个template_path参数。
template_path=os.path.join(os.path.dirname(__file__), "templates")
template_path参数告诉Tornado在哪里寻找模板文件。
模板是一个允许你嵌入Python代码片段的HTML文件。上面的代码告诉Python在你Tornado应用文件同目录下的templates文件夹中寻找模板文件。
一旦我们告诉Tornado在哪里找到模板,我们可以使用RequestHandler类的render方法来告诉Tornado读入模板文件,插入其中的模版代码,并返回结果给浏览器。比如,在IndexHandler中,我们发现了下面的语句:
self.render('index.html')
这段代码告诉Tornado在templates文件夹下找到一个名为index.html的文件,读取其中的内容,并且发送给浏览器。
三、填充
实际上index.html完全不能称之为"模板",它所包含的完全是已编写好的HTML标记。
这可以是模板的一个不错的使用方式,但在更通常的情况下我们希望HTML输出可以结合我们的程序传入给模板的值。
模板poem.html使用PoemPageHandler渲染,是这种方式的一个很好的例子。让我们看看它是如何工作的吧。
在poem.html中,你可以看到模板中有一些被双大括号({{和}})括起来的字符串,就像这样:
<p>Two {{roads}} diverged in a {{wood}}, and I—<br/> I took the one less travelled by,<br> And that has {{made}} all the {{difference}}.</p>
在双大括号中的单词是占位符,当我们渲染模板时希望以实际值代替。
我们可以使用向render函数中传递关键字参数的方法指定什么值将被填充到HTML文件中的对应位置,其中关键字对应模板文件中占位符的名字。
下面是在PoemPageHandler中相应的代码部分:
noun1 = self.get_argument('noun1') noun2 = self.get_argument('noun2') verb = self.get_argument('verb') noun3 = self.get_argument('noun3') self.render('poem.html', roads=noun1, wood=noun2, made=verb, difference=noun3)
在这里,我们告诉模板使用变量noun1(该变量是从get_argument方法取得的)作为模板中roads的值,noun2作为模板中wood的值,依此类推。
假设用户在表单中按顺序键入了pineapples、grandfather clock、irradiated和supernovae,那么结果HTML将会如下所示:
<p>Two pineapples diverged in a grandfather clock, and I—<br> I took the one less travelled by,<br> And that has irradiated all the supernovae.</p>
四、模板语法
既然我们已经看到了一个模板在实际应用中的简单例子,那么让我们深入地了解它们是如何工作的吧。
Tornado模板是被Python表达式和控制语句标记的简单文本文件。
Tornado的语法非常简单直接。熟悉Django、Liquid或其他相似框架的用户会发现它们非常相似,很容易学会。
在2.1节中,我们展示了如何在一个Web应用中使用render方法传送HTML给浏览器。
你可以在Tornado应用之外使用Python解释器导入模板模块尝试模板系统,此时结果会被直接输出出来。
>>> from tornado.template import Template >>> content = Template("<html><body><h1>{{ header }}</h1></body></html>") >>> print content.generate(header="Welcome!") <html><body><h1>Welcome!</h1></body></html>
五、填充表达式
在代码清单2-1中,我们演示了填充Python变量的值到模板的双大括号中的使用。
实际上,你可以将任何Python表达式放在双大括号中。
tornado将插入一个包含任何表达式计算结果值的字符串到输出中。下面是几个可能的例子:
>>> from tornado.template import Template >>> print Template("{{ 1+1 }}").generate() 2 >>> print Template("{{ 'scrambled eggs'[-4:] }}").generate() eggs >>> print Template("{{ ', '.join([str(x*x) for x in range(10)]) }}").generate() 0, 1, 4, 9, 16, 25, 36, 49, 64, 81
六、控制流语句
你同样可以在Tornado模板中使用Python条件和循环语句。控制语句以{%和%}包围,并以类似下面的形式被使用:
{% if page is None %} 或 {% if len(entries) == 3 %}
控制语句的大部分就像对应的Python语句一样工作,支持if、for、while和try。在这些情况下,语句块以{%开始,并以%}结束。
所以这个模板:
<html> <head> <title>{{ title }}</title> </head> <body> <h1>{{ header }}</h1> <ul> {% for book in books %} <li>{{ book }}</li> {% end %} </ul> </body> </html>
当被下面这个处理函数调用时:
class BookHandler(tornado.web.RequestHandler): def get(self): self.render( "book.html", title="Home Page", header="Books that are great", books=[ "Learning Python", "Programming Collective Intelligence", "Restful Web Services" ] )
将会渲染得到下面的输出:
<html> <head> <title>Home Page</title> </head> <body> <h1>Books that are great</h1> <ul> <li>Learning Python</li> <li>Programming Collective Intelligence</li> <li>Restful Web Services</li> </ul> </body> </html>
不像许多其他的Python模板系统,Tornado模板语言的一个最好的东西是在if和for语句块中可以使用的表达式没有限制。因此,你可以在你的模板中执行所有的Python代码。
同样,你也可以在你的控制语句块中间使用{% set foo = 'bar' %}
来设置变量。你还有很多可以在控制语句块中做的事情,但是在大多数情况下,你最好使用UI模块来做更复杂的划分。我们稍后会更详细的看到这一点。
七、在模板中使用函数
Tornado在所有模板中默认提供了一些便利的函数。它们包括:
escape(s)
替换字符串s中的&、<、>为他们对应的HTML字符。
url_escape(s)
使用urllib.quote_plus替换字符串s中的字符为URL编码形式。
json_encode(val)
将val编码成JSON格式。(在系统底层,这是一个对json库的dumps函数的调用。查阅相关的文档以获得更多关于该函数接收和返回参数的信息。)
squeeze(s)
过滤字符串s,把连续的多个空白字符替换成一个空格。
在Tornado 1.x中,模版不是被自动转义的。在Tornado 2.0中,模板被默认为自动转义(并且可以在Application构造函数中使用autoscaping=None关闭)。在不同版本的迁移时要注意向后兼容。
在模板中使用一个你自己编写的函数也是很简单的:只需要将函数名作为模板的参数传递即可,就像其他变量一样。
>>> from tornado.template import Template >>> def disemvowel(s): ... return ''.join([x for x in s if x not in 'aeiou']) ... >>> disemvowel("george") 'grg' >>> print Template("my name is {{d('mortimer')}}").generate(d=disemvowel) my name is mrtmr
八、复杂示例:static_path,template_path,debug
The Alpha Munger
在代码清单2-4中,我们把在这一章中谈论过的所有东西都放了进来。这个应用被称为The Alpha Munger。用户输入两个文本:一个"源"文本和一个"替代"文本。应用会返回替代文本的一个副本,并将其中每个单词替换成源文本中首字母相同的某个单词。图2-3展示了要填的表单,图2-4展示了结果文本。
这个应用包括四个文件:main.py(Tornado程序)、style.css(CSS样式表文件)、index.html和munged.html(Tornado模板)。让我们看看代码吧:
代码清单2-4 复杂表单和模板:main.py import os.path import random import tornado.httpserver import tornado.ioloop import tornado.options import tornado.web from tornado.options import define, options define("port", default=8000, help="run on the given port", type=int) class IndexHandler(tornado.web.RequestHandler): def get(self): self.render('index.html') class MungedPageHandler(tornado.web.RequestHandler): def map_by_first_letter(self, text): mapped = dict() for line in text.split('\r\n'): for word in [x for x in line.split(' ') if len(x) > 0]: if word[0] not in mapped: mapped[word[0]] = [] mapped[word[0]].append(word) return mapped def post(self): source_text = self.get_argument('source') text_to_change = self.get_argument('change') source_map = self.map_by_first_letter(source_text) change_lines = text_to_change.split('\r\n') self.render('munged.html', source_map=source_map, change_lines=change_lines, choice=random.choice) if __name__ == '__main__': tornado.options.parse_command_line() app = tornado.web.Application( handlers=[(r'/', IndexHandler), (r'/poem', MungedPageHandler)], template_path=os.path.join(os.path.dirname(__file__), "templates"), static_path=os.path.join(os.path.dirname(__file__), "static"), debug=True ) http_server = tornado.httpserver.HTTPServer(app) http_server.listen(options.port) tornado.ioloop.IOLoop.instance().start()
记住Application构造函数中的static_path参数。
我们将在下面进行详细的介绍,但是现在你所需要知道的就是static_path参数指定了你应用程序放置静态资源(如图像、CSS文件、JavaScript文件等)的目录。
另外,你还需要在templates文件夹下添加index.html和munged.html这两个文件。
代码清单2-5 Alpha Munger表单:index.html <!DOCTYPE html> <html> <head> <link rel="stylesheet" href="{{ static_url("style.css") }}"> <title>The Alpha Munger</title> </head> <body> <h1>The Alpha Munger</h1> <p>Enter two texts below. The replacement text will have its words replaced by words beginning with the same letter in the source text.</p> <form method="post" action="/poem"> <p>Source text<br> <textarea rows=4 cols=55 name="source"></textarea></p> <p>Text for replacement<br> <textarea rows=4 cols=55 name="change"></textarea></p> <input type="submit"> </form> </body> </html> 代码清单2-6 Alpha Munger模板:munged.html <!DOCTYPE html> <html> <head> <link rel="stylesheet" href="{{ static_url("style.css") }}"> <title>The Alpha Munger</title> </head> <body> <h1>Your text</h1> <p> {% for line in change_lines %} {% for word in line.split(' ') %} {% if len(word) > 0 and word[0] in source_map %} <span class="replaced" title="{{word}}">{{ choice(source_map[word[0]]) }}</span> {% else %} <span class="unchanged" title="unchanged">{{word}}</span> {% end %} {% end %} <br> {% end %} </p> </body> </html> 最后,将代码清单2-7中的内容写到static子目录下的style.css文件中。 代码清单2-7 Alpha Munger样式表:style.css body { font-family: Helvetica,Arial,sans-serif; width: 600px; margin: 0 auto; } .replaced:hover { color: #00f; }
它如何工作
这个Tornado应用定义了两个请求处理类:IndexHandler和MungedPageHandler。
IndexHandler类简单地渲染了index.html中的模板,其中包括一个允许用户POST一个源文本(在source域中)和一个替换文本(在change域中)到/poem
的表单。
MungedPageHandler类用于处理到/poem
的POST请求。
当一个请求到达时,它对传入的数据进行一些基本的处理,然后为浏览器渲染模板。
map_by_first_letter方法将传入的文本(从source域)分割成单词,然后创建一个字典,其中每个字母表中的字母对应文本中所有以其开头的单词(我们将其放入一个叫作source_map的变量)。
再把这个字典和用户在替代文本(表单的change域)中指定的内容一起传给模板文件munged.html。
此外,我们还将Python标准库的random.choice函数传入模板,这个函数以一个列表作为输入,返回列表中的任一元素。
在munged.html中,我们迭代替代文本中的每行,再迭代每行中的每个单词。
如果当前单词的第一个字母是source_map字典的一个键,我们使用random.choice函数从字典的值中随机选择一个单词并展示它。
如果字典的键中没有这个字母,我们展示源文本中的原始单词。
每个单词包括一个span标签,其中的class属性指定这个单词是替换后的(class="replaced")还是原始的(class="unchanged")。
(我们还将原始单词放到了span标签的title属性中,以便于用户在鼠标经过单词时可以查看是什么单词被替代了。你可以在图2-5中看到这个动作。)
在这个例子中,你可能注意到了debug=True的使用。
它调用了一个便利的测试模式:tornado.autoreload模块,此时,一旦主要的Python文件被修改,Tornado将会尝试重启服务器,并且在模板改变时会进行刷新。
对于快速改变和实时更新这非常棒,但不要再生产上使用它,因为它将防止Tornado缓存模板!
提供静态文件
当编写Web应用时,你总希望提供像样式表、JavaScript文件和图像这样不需要为每个文件编写独立处理函数的"静态内容"。
Tornado提供了几个有用的捷径来使其变得容易。
- 设置静态路径
- 使用static_url生成静态URL
设置静态路径
你可以通过向Application类的构造函数传递一个名为static_path的参数来告诉Tornado从文件系统的一个特定位置提供静态文件。
Alpha Munger中的相关代码片段如下:
app = tornado.web.Application( handlers=[(r'/', IndexHandler), (r'/poem', MungedPageHandler)], template_path=os.path.join(os.path.dirname(__file__), "templates"), static_path=os.path.join(os.path.dirname(__file__), "static"), debug=True )
使用static_url生成静态URL
Tornado模板模块提供了一个叫作static_url的函数来生成static目录下文件的URL。让我们来看看在index.html中static_url的调用的示例代码:
<link rel="stylesheet" href="{{ static_url("style.css") }}">
这个对static_url的调用生成了URL的值,并渲染输出类似下面的代码:
<link rel="stylesheet" href="/static/style.css?v=ab12">
那么为什么使用static_url而不是在你的模板中硬编码呢?
有如下几个原因。其一,static_url函数创建了一个基于文件内容的hash值,并将其添加到URL末尾(查询字符串的参数v)。
这个hash值确保浏览器总是加载一个文件的最新版而不是之前的缓存版本。
无论是在你应用的开发阶段,还是在部署到生产环境使用时,都非常有用,因为你的用户不必再为了看到你的静态内容而清除浏览器缓存了。
另一个好处是你可以改变你应用URL的结构,而不需要改变模板中的代码。
例如,你可以配置Tornado响应来自像路径/s/filename.ext的请求时提供静态内容,而不是默认的/static路径。
如果你使用static_url而不是硬编码的话,你的代码不需要改变。
比如说,你想把静态资源从我们刚才使用的/static目录移到新的/s目录。你可以简单地改变静态路径由static变为s,然后每个使用static_url包裹的引用都会被自动更新。
如果你在每个引用静态资源的文件中硬编码静态路径部分,你将不得不手动修改每个模板。
在第二章中,我们看到了Tornado模板系统如何简单地传递信息给网页,使你在插入动态数据时保持网页标记的整洁。然而,大多数站点希望复用像header、footer和布局网格这样的内容。在这一章中,我们将看到如何使用扩展Tornado模板或UI模块完成这一工作。
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标签在标记和样式冲突时不构成重大威胁(如评论中没有闭<h1>标签),但<script>标签会允许攻击者加载其他的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()简记的好处,又可以保持在其他地方自动转义的好处。
3.2 UI模块¶
正如前面我们所看到的,模板系统既轻量级又强大。在实践中,我们希望遵循软件工程的谚语,Don't Repeat Yourself。为了消除冗余的代码,我们可以使模板部分模块化。比如,展示物品列表的页面可以定位一个单独的模板用来渲染每个物品的标记。另外,一组共用通用导航结构的页面可以从一个共享的模块渲染内容。Tornado的UI模块在这种情况下特别有用
UI模块是封装模板中包含的标记、样式以及行为的可复用组件。它所定义的元素通常用于多个模板交叉复用或在同一个模板中重复使用。模块本身是一个继承自Tornado的UIModule类的简单Python类,并定义了一个render方法。当一个模板使用{% module Foo(...) %}标签引用一个模块时,Tornado的模板引擎调用模块的render方法,然后返回一个字符串来替换模板中的模块标签。UI模块也可以在渲染后的页面中嵌入自己的JavaScript和CSS文件,或指定额外包含的JavaScript或CSS文件。你可以定义可选的embedded_javascript、embedded_css、javascript_files和css_files方法来实现这一方法。
3.2.1 基础模块使用¶
为了在你的模板中引用模块,你必须在应用的设置中声明它。ui_modules参数期望一个模块名为键、类为值的字典输入来渲染它们。考虑代码清单3-1。
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 HelloHandler(tornado.web.RequestHandler): def get(self): self.render('hello.html') class HelloModule(tornado.web.UIModule): def render(self): return '<h1>Hello, world!</h1>' if __name__ == '__main__': tornado.options.parse_command_line() app = tornado.web.Application( handlers=[(r'/', HelloHandler)], template_path=os.path.join(os.path.dirname(__file__), 'templates'), ui_modules={'Hello': HelloModule} ) server = tornado.httpserver.HTTPServer(app) server.listen(options.port) tornado.ioloop.IOLoop.instance().start()
这个例子中ui_module字典里只有一项,它把到名为Hello的模块的引用和我们定义的HelloModule类结合了起来。
现在,当调用HelloHandler并渲染hello.html时,我们可以使用{% module Hello() %}模板标签来包含HelloModule类中render方法返回的字符串。
<html> <head><title>UI Module Example</title></head> <body> {% module Hello() %} </body> </html>
这个hello.html模板通过在模块标签自身的位置调用HelloModule返回的字符串进行填充。下一节的例子将会展示如何扩展UI模块来渲染它们自己的模板并包含脚本和样式表。
3.2.2 模块深入¶
很多时候,一个非常有用的做法是让模块指向一个模板文件而不是在模块类中直接渲染字符串。这些模板的标记看起来就像我们已经看到过的作为整体的模板。
UI模块的一个常见应用是迭代数据库或API查询中获得的结果,为每个独立项目的数据渲染相同的标记。比如,Burt想在Burt's Book里创建一个推荐阅读部分,他已经创建了一个名为recommended.html的模板,其代码如下所示。就像前面看到的那样,我们将使用{% module Book(book) %}标签调用模块。
{% extends "main.html" %} {% block body %} <h2>Recommended Reading</h2> {% for book in books %} {% module Book(book) %} {% end %} {% end %}
Burt还创建了一个叫作book.html的图书模块的模板,并把它放到了templates/modules目录下。一个简单的图书模板看起来像下面这样:
<div class="book"> <h3 class="book_title">{{ book["title"] }}</h3> <img src="{{ book["image"] }}" class="book_image"/> </div>
现在,当我们定义BookModule类的时候,我们将调用继承自UIModule的render_string方法。这个方法显式地渲染模板文件,当我们返回给调用者时将其关键字参数作为一个字符串。
class BookModule(tornado.web.UIModule): def render(self, book): return self.render_string('modules/book.html', book=book)
在完整的例子中,我们将使用下面的模板来格式化每个推荐书籍的所有属性,代替先前的book.html
<div class="book"> <h3 class="book_title">{{ book["title"] }}</h3> {% if book["subtitle"] != "" %} <h4 class="book_subtitle">{{ book["subtitle"] }}</h4> {% end %} <img src="{{ book["image"] }}" class="book_image"/> <div class="book_details"> <div class="book_date_released">Released: {{ book["date_released"]}}</div> <div class="book_date_added"> Added: {{ locale.format_date(book["date_added"], relative=False) }} </div> <h5>Description:</h5> <div class="book_body">{% raw book["description"] %}</div> </div> </div>
使用这个布局,传递给recommended.html模板的books参数的每项都将会调用这个模块。每次使用一个新的book参数调用Book模块时,模块(以及book.html模板)可以引用book参数的字典中的项,并以适合的方式格式化数据(如图3-6)。

图3-6 包含样式数据的图书模块
现在,我们可以定义一个RecommendedHandler类来渲染模板,就像你通常的操作那样。这个模板可以在渲染推荐书籍列表时引用Book模块。
class RecommendedHandler(tornado.web.RequestHandler): def get(self): self.render( "recommended.html", page_title="Burt's Books | Recommended Reading", header_text="Recommended Reading", books=[ { "title":"Programming Collective Intelligence", "subtitle": "Building Smart Web 2.0 Applications", "image":"/static/images/collective_intelligence.gif", "author": "Toby Segaran", "date_added":1310248056, "date_released": "August 2007", "isbn":"978-0-596-52932-1", "description":"<p>This fascinating book demonstrates how you " "can build web applications to mine the enormous amount of data created by people " "on the Internet. With the sophisticated algorithms in this book, you can write " "smart programs to access interesting datasets from other web sites, collect data " "from users of your own applications, and analyze and understand the data once " "you've found it.</p>" }, ... ] )
如果要用更多的模块,只需要简单地在ui_modules参数中添加映射值。因为模板可以指向任何定义在ui_modules字典中的模块,所以在自己的模块中指定功能非常容易。
在这个例子中,你可能已经注意到了locale.format_date()的使用。它调用了tornado.locale模块提供的日期处理方法,这个模块本身是一组i18n方法的集合。format_date()选项默认格式化GMT Unix时间戳为XX time ago
,并且可以向下面这样使用:
{{ locale.format_date(book["date"]) }}
relative=False将使其返回一个绝对时间(包含小时和分钟),而full_format=True选项将会展示一个包含月、日、年和时间的完整日期(比如,July 9, 2011 at 9:47 pm
),当搭配shorter=True使用时可以隐藏时间,只显示月、日和年。
这个模块在你处理时间和日期时非常有用,并且还提供了处理本地化字符串的支持。
3.2.3 嵌入JavaScript和CSS¶
为了给这些模块提供更高的灵活性,Tornado允许你使用embedded_css和embedded_javascript方法嵌入其他的CSS和JavaScript文件。举个例子,如果你想在调用模块时给DOM添加一行文字,你可以通过从模块中嵌入JavaScript来做到:
class BookModule(tornado.web.UIModule): def render(self, book): return self.render_string( "modules/book.html", book=book, ) def embedded_javascript(self): return "document.write(\"hi!\")"
当调用模块时,document.write(\"hi!\")将被<script>包围,并被插入到<body>的闭标签中:
<script type="text/javascript"> //<![CDATA[ document.write("hi!") //]]> </script>
显然,只是在文档主体中写这些内容并不是世界上最有用的事情,而我们还有另一个给予你极大灵活性的选项,当创建这些模块时,可以在每个模块中包含JavaScript文件。
类似的,你也可以把只在这些模块被调用时加载的额外的CSS规则放进来:
def embedded_css(self): return ".book {background-color:#F5F5F5}"
在这种情况下,.book {background-color:#555}
这条CSS规则被包裹在<style>中,并被直接添加到<head>的闭标签之前。
<style type="text/css"> .book {background-color:#F5F5F5} </style>
更加灵活的是,你甚至可以简单地使用html_body()来在闭合的</body>标签前添加完整的HTML标记:
def html_body(self): return "<script>document.write(\"Hello!\")</script>"
显然,虽然直接内嵌添加脚本和样式表很有用,但是为了更严谨的包含(以及更整洁的代码!),添加样式表和脚本文件会显得更好。他们的工作方式基本相同,所以你可以使用javascript_files()和css_files()来包含完整的文件,不论是本地的还是外部的。
比如,你可以添加一个额外的本地CSS文件如下:
def css_files(self): return "/static/css/newreleases.css"
或者你可以取得一个外部的JavaScript文件:
def javascript_files(self): return "https://ajax.googleapis.com/ajax/libs/jqueryui/1.8.14/jquery-ui.min.js"
当一个模块需要额外的库而应用的其他地方不是必需的时候,这种方式非常有用。比如,你有一个使用JQuery UI库的模块(而在应用的其他地方都不会被使用),你可以只在这个样本模块中加载jquery-ui.min.js文件,减少那些不需要它的页面的加载时间。
因为模块的内嵌JavaScript和内嵌HTML函数的目标都是紧邻</body>标签,html_body()、javascript_files()和embedded_javascript()都会将内容渲染后插到页面底部,那么它们出现的顺序正好是你指定它们的顺序的倒序。
如果你有一个模块如下面的代码所示:
class SampleModule(tornado.web.UIModule): def render(self, sample): return self.render_string( "modules/sample.html", sample=sample ) def html_body(self): return "<div class=\"addition\"><p>html_body()</p></div>" def embedded_javascript(self): return "document.write(\"<p>embedded_javascript()</p>\")" def embedded_css(self): return ".addition {color: #A1CAF1}" def css_files(self): return "/static/css/sample.css" def javascript_files(self): return "/static/js/sample.js"
html_body()最先被编写,它紧挨着出现在</body>标签的上面。embedded_javascript()接着被渲染,最后是javascript_files()。你可以在图3-7中看到它是如何工作的。
需要小心的是,你不能包括一个需要其他地方东西的方法(比如依赖其他文件的JavaScript函数),因为此时他们可能会按照和你期望不同的顺序进行渲染。
总之,模块允许你在模板中渲染格式化数据时非常灵活,同时也让你能够只在调用模块时包含指定的一些额外的样式和函数规则。
3.3 总结¶
正如我们之前看到的,Tornado使扩展模板更容易,以便你的网站代码可以在整个应用中轻松复用。而使用模块后,你可以在什么文件、样式和脚本动作需要被包括进来这个问题上拥有更细粒度的决策。然而,我们的例子依赖于使用Python原生数据结构时是否简单,在你的实际应用中硬编码大数据结构的感觉可不好。下一步,我们将看到如何配合持久化存储来处理存储、提供和编辑动态内容。
posted on 2018-11-02 16:13 myworldworld 阅读(700) 评论(0) 收藏 举报