1.3 表单和模板操作

在Tornado框架中可以灵活地使用表单和模板技术,通过使用这些技术可以实现动态Web功能。

1.3.1 一个基本的注册表单

在下面的实例文件001.py中,首先实现一个让用户填写注册信息的HTML表单,然后显示表单处理结果。

源码路径:daima\1\1-3\001.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=8001, help="运行在指定端口", 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()

为了突出Web程序界面的美观性,接下来我们将使用模板技术。框架Tornado自身提供了一个轻量级的模板模块tornado.template,用于快速并且灵活地实现模板功能。我们将模板文件保存在“templates”文件夹中,其中文件index.html作为注册表单。具体实现代码如下。

源码路径:daima\1\1-3\templates\index.html

<!DOCTYPE html>
<html>
     <head><title>会员登录</title></head>
     <body>
          <h1>输入注册信息.</h1>
          <form method="post" action="/poem">
          <p>用户名<br><input type="text" name="noun1"></p>
          <p>密码<br><input type="text" name="noun2"></p>
          <p>确认密码<br><input type="text" name="verb"></p>
          <p>性别<br><input type="text" name="noun3"></p>
          <input type="submit">
          </form>
     </body>
</html>

模板文件poem.html用于显示注册结果信息。具体实现代码如下。

源码路径:daima\1\1-3\templates\poem.html

<!DOCTYPE html>
<html>
     <head><title>注册结果</title></head>
     <body>
          <h1>下面是你的注册信息</h1>
          <p>用户名:{{roads}}<br>密码:{{wood}}<br>确认密码:{{made}}<br>性别:
             {{difference}}.</p>
     </body>
</html>

开始调试本实例。首先运行前面的Python文件001.py,然后在浏览器中输入http://localhost:8001/,接下来会显示注册表单,这是由模板文件index.html实现的。执行效果如图1-10所示。在表单中输入注册信息,并单击“提交查询内容”按钮后显示注册结果,这是由模板文件poem.html实现的。执行效果如图1-11所示。

图1-10 注册表单

图1-11 注册结果

在上面的实例文件001.py中,定义了RequestHandler子类,并把它们传给tornado.web.Application对象。通过如下代码向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中通过如下代码告诉Tornado在文件夹“templates”下找到一个名为index.html的文件,读取其中的内容,并发送给浏览器。

self.render('index.html')

1.3.2 在模板中使用函数

在框架Tornado中,为模板功能提供了如下内置函数。

•escape(s):替换字符串s中的&、<、>为它们对应的HTML字符。

•url_escape(s):使用urllib.quote_plus替换字符串s中的字符为URL编码形式。

•json_encode(val):将val编码成JSON格式。(在系统底层,这是一个对JSON库的dumps函数的调用。)

•squeeze(s):过滤字符串s,把连续的多个空白字符替换成一个空格。

在模板中可以使用一个自己编写的函数,这时只需要将函数名作为模板的参数传递即可,就像使用其他变量一样。例如:

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

再看下面的演示实例。首先在模板文件界面中提示用户输入两个文本:“源”文本和“替代”文本。单击“提交”按钮后会返回替代文本的一个副本,并将其中每个单词替换成源文本中首字母相同的某个单词。本实例包括4个文件:002.py(Tornado程序)、style.css(CSS文件)、index.html和munged.html(Tornado模板)。

(1)在Python文件002.py中定义了两个请求处理类——IndexHandler和MungedPageHandler。具体实现代码如下。

源码路径:daima\1\1-3\moban2\002.py

from tornado.options import define, options
define("port", default=8001, help="运行给定的端口", 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()

•类IndexHandler——简单渲染了index.html中的模板,其中包括一个允许用户发送一个源文本(在source域中)和一个替换文本(在change域中)到/poem的表单。

•类MungedPageHandler——用于处理到/poem的POST请求。当一个请求到达时,它对传入的数据进行一些基本的处理,然后为浏览器渲染模板。map_by_first_letter方法将传入的文本(从source域)分割成单词,然后创建一个字典,其中每个字母表中的字母对应文本中所有以指定字母开头的单词(我们将其放入一个叫作source_map的变量中)。再把这个字典和用户在替代文本(表单的change域)中指定的内容一起传给模板文件munged.html。此外,我们还将Python标准库的random.choice函数传入模板,这个函数以一个列表作为输入,返回列表中的任一元素。

•参数static_path——指定了应用程序放置静态资源的目录,如图像、CSS文件、JavaScript文件等。

•在编写Web应用程序时,总希望提供像样式表、JavaScript文件和图像这样不需要为每个文件编写独立处理程序的“静态内容”。例如,可以通过向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的子目录作为static_path的参数。现在应用将以读取static目录下的filename.ext来响应诸如/static/filename.ext的请求,并在响应的主体中返回。

(2)模板文件index.html用于显示主表单界面。具体实现代码如下。

源码路径:daima\1\1-3\moban2\templates\index.html

<!DOCTYPE html>
<html>
     <head>
          <link rel="stylesheet" href="{{ static_url("style.css") }}">
          <title>操作</title>
     </head>
     <body>
          <h1>替换操作</h1>
          <p>在下面输入两个文本,替换文本将用源文本中同一字母开头的单词替换单词。</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>

在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()创建了一个基于文件内容的散列值,并将其添加到URL末尾(查询字符串的参数v)。这个散列值确保浏览器总是加载一个文件的最新版而不是之前的缓存版本。无论是在应用的开发阶段,还是在部署到生产环境使用时,这个方式都非常有用,因为用户不必再为了看到静态内容而清除浏览器缓存了。

•可以改变应用URL的结构,而不需要改变模板中的代码。例如,可以配置Tornado响应来自路径/s/filename.ext的请求时提供静态内容,而不是默认的/static路径。如果使用static_url而不是硬编码,代码不需要改变。比如,要把静态资源从我们刚才使用的/static目录移到新的/s目录,可以简单地改变静态路径static为s,然后每个使用static_url包裹的引用都会自动更新。如果你在每个引用静态资源的文件中硬编码静态路径部分,将不得不手动修改每个模板。

(3)模板文件munged.html用于显示替换结果。具体实现代码如下。

源码路径:daima\1\1-3\moban2\templates\munged.html

<!DOCTYPE html>
<html>
     <head>
          <link rel="stylesheet" href="{{ static_url("style.css") }}">
          <title>结果</title>
     </head>
     <body>
          <h1>现在的文本是</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>

在上述代码中迭代替代文本中的每行,再迭代每行中的每个单词。如果当前单词的第一个字母是source_map字典的一个键,则使用random.choice函数从字典的值中随机选择一个单词并展示它。如果字典的键中没有这个字母,则展示源文本中的原始单词。每个单词包括一个span标签,其中的class属性指定这个单词是替换后的(class="replaced")还是原始的(class="unchanged")。(我们还将原始单词放到了span标签的title属性中,以便于用户在鼠标指针经过单词时可以查看哪些单词被替换。下面看执行效果。假设在表单中分别输入“from tornado.template import Template”和“print Template("my name is {{d('mortimer')}}").generate(d=disemvowel)”,如图1-12所示。

图1-12 输入表单

单击“提交查询内容”按钮后执行替换操作,并显示替换后的结果,如图1-13所示。

图1-13 替换后的结果