第二章:表單和模板

2018-02-24 15:49 更新

在第一章中,我們學(xué)習(xí)了使用Tornado創(chuàng)建一個(gè)Web應(yīng)用的基礎(chǔ)知識(shí)。包括處理函數(shù)、HTTP方法以及Tornado框架的總體結(jié)構(gòu)。在這章中,我們將學(xué)習(xí)一些你在創(chuàng)建Web應(yīng)用時(shí)經(jīng)常會(huì)用到的更強(qiáng)大的功能。

和大多數(shù)Web框架一樣,Tornado的一個(gè)重要目標(biāo)就是幫助你更快地編寫(xiě)程序,盡可能整潔地復(fù)用更多的代碼。盡管Tornado足夠靈活,可以使用幾乎所有Python支持的模板語(yǔ)言,Tornado自身也提供了一個(gè)輕量級(jí)、快速并且靈活的模板語(yǔ)言在tornado.template模塊中。

2.1 簡(jiǎn)單示例:Poem Maker Pro

讓我們以一個(gè)叫作Poem Maker Pro的簡(jiǎn)單例子開(kāi)始。Poem Maker Pro這個(gè)Web應(yīng)用有一個(gè)讓用戶填寫(xiě)的HTML表單,然后處理表單的結(jié)果。代碼清單2-1是它的Python代碼。

代碼清單2-1 簡(jiǎn)單表單和模板: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()

除了poemmaker.py,你還需要將代碼清單2-2和代碼清單2-3中的兩個(gè)文件加入到templates子文件夾中。

代碼清單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>

在命令行執(zhí)行下述命令:

$ python poemmaker.py --port=8000

現(xiàn)在,在瀏覽器中打開(kāi)http://localhost:8000。當(dāng)瀏覽器請(qǐng)求根目錄(/)時(shí),Tornado程序?qū)秩緄ndex.html,展示如圖2-1所示的簡(jiǎn)單HTML表單。

圖2-1

圖2-1 Poem Maker Pro:輸入表單

這個(gè)表單包括多個(gè)文本域(命名為noun1、noun2等),其中的內(nèi)容將在用戶點(diǎn)擊"Submit"按鈕時(shí)以POST請(qǐng)求的方式送到/poem?,F(xiàn)在往里面填寫(xiě)東西然后點(diǎn)擊提交吧。

為了響應(yīng)這個(gè)POST請(qǐng)求,Tornado應(yīng)用跳轉(zhuǎn)到poem.html,插入你在表單中填寫(xiě)的值。結(jié)果是Robert Frost的詩(shī)《The Road Not Taken》的輕微修改版本。圖2-2展示了這個(gè)結(jié)果。

圖2-2

圖2-2 Poem Maker Pro:輸出

2.1.1 渲染模板

從結(jié)構(gòu)上講,poemmaker.py和第一章中的例子很相似。我們定義了幾個(gè)RequestHandler子類并把它們傳給tornado.web.Application對(duì)象。那么有什么不一樣的地方呢?首先,我們向Application對(duì)象的init方法傳遞了一個(gè)template_path參數(shù)。

template_path=os.path.join(os.path.dirname(__file__), "templates")

template_path參數(shù)告訴Tornado在哪里尋找模板文件。我們將在本章和第三章中講解其確切性質(zhì)和語(yǔ)法,而它的基本要點(diǎn)是:模板是一個(gè)允許你嵌入Python代碼片段的HTML文件。上面的代碼告訴Python在你Tornado應(yīng)用文件同目錄下的templates文件夾中尋找模板文件。

一旦我們告訴Tornado在哪里找到模板,我們可以使用RequestHandler類的render方法來(lái)告訴Tornado讀入模板文件,插入其中的模版代碼,并返回結(jié)果給瀏覽器。比如,在IndexHandler中,我們發(fā)現(xiàn)了下面的語(yǔ)句:

self.render('index.html')

這段代碼告訴Tornado在templates文件夾下找到一個(gè)名為index.html的文件,讀取其中的內(nèi)容,并且發(fā)送給瀏覽器。

2.1.2 填充

實(shí)際上index.html完全不能稱之為"模板",它所包含的完全是已編寫(xiě)好的HTML標(biāo)記。這可以是模板的一個(gè)不錯(cuò)的使用方式,但在更通常的情況下我們希望HTML輸出可以結(jié)合我們的程序傳入給模板的值。模板poem.html使用PoemPageHandler渲染,是這種方式的一個(gè)很好的例子。讓我們看看它是如何工作的吧。

在poem.html中,你可以看到模板中有一些被雙大括號(hào)({{和}})括起來(lái)的字符串,就像這樣:

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

在雙大括號(hào)中的單詞是占位符,當(dāng)我們渲染模板時(shí)希望以實(shí)際值代替。我們可以使用向render函數(shù)中傳遞關(guān)鍵字參數(shù)的方法指定什么值將被填充到HTML文件中的對(duì)應(yīng)位置,其中關(guān)鍵字對(duì)應(yīng)模板文件中占位符的名字。下面是在PoemPageHandler中相應(yīng)的代碼部分:

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的值,依此類推。假設(shè)用戶在表單中按順序鍵入了pineapples、grandfather clock、irradiated和supernovae,那么結(jié)果HTML將會(huì)如下所示:

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

2.2 模板語(yǔ)法

既然我們已經(jīng)看到了一個(gè)模板在實(shí)際應(yīng)用中的簡(jiǎn)單例子,那么讓我們深入地了解它們是如何工作的吧。Tornado模板是被Python表達(dá)式和控制語(yǔ)句標(biāo)記的簡(jiǎn)單文本文件。Tornado的語(yǔ)法非常簡(jiǎn)單直接。熟悉Django、Liquid或其他相似框架的用戶會(huì)發(fā)現(xiàn)它們非常相似,很容易學(xué)會(huì)。

在2.1節(jié)中,我們展示了如何在一個(gè)Web應(yīng)用中使用render方法傳送HTML給瀏覽器。你可以在Tornado應(yīng)用之外使用Python解釋器導(dǎo)入模板模塊嘗試模板系統(tǒng),此時(shí)結(jié)果會(huì)被直接輸出出來(lái)。

>>> 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.2.1 填充表達(dá)式

在代碼清單2-1中,我們演示了填充Python變量的值到模板的雙大括號(hào)中的使用。實(shí)際上,你可以將任何Python表達(dá)式放在雙大括號(hào)中。Tornado將插入一個(gè)包含任何表達(dá)式計(jì)算結(jié)果值的字符串到輸出中。下面是幾個(gè)可能的例子:

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

2.2.2 控制流語(yǔ)句

你同樣可以在Tornado模板中使用Python條件和循環(huán)語(yǔ)句??刂普Z(yǔ)句以{%和%}包圍,并以類似下面的形式被使用:

{% if page is None %}

{% if len(entries) == 3 %}

控制語(yǔ)句的大部分就像對(duì)應(yīng)的Python語(yǔ)句一樣工作,支持if、for、while和try。在這些情況下,語(yǔ)句塊以{%開(kāi)始,并以%}結(jié)束。

所以這個(gè)模板:

<html>
    <head>
        <title>{{ title }}</title>
    </head>
    <body>
        <h1>{{ header }}</h1>
        <ul>
            {% for book in books %}
                <li>{{ book }}</li>
            {% end %}
        </ul>
    </body>
</html>

當(dāng)被下面這個(gè)處理函數(shù)調(diào)用時(shí):

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

將會(huì)渲染得到下面的輸出:

<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模板系統(tǒng),Tornado模板語(yǔ)言的一個(gè)最好的東西是在if和for語(yǔ)句塊中可以使用的表達(dá)式?jīng)]有限制。因此,你可以在你的模板中執(zhí)行所有的Python代碼。

同樣,你也可以在你的控制語(yǔ)句塊中間使用{% set foo = 'bar' %}來(lái)設(shè)置變量。你還有很多可以在控制語(yǔ)句塊中做的事情,但是在大多數(shù)情況下,你最好使用UI模塊來(lái)做更復(fù)雜的劃分。我們稍后會(huì)更詳細(xì)的看到這一點(diǎn)。

2.2.3 在模板中使用函數(shù)

Tornado在所有模板中默認(rèn)提供了一些便利的函數(shù)。它們包括:

escape(s)

替換字符串s中的&、為他們對(duì)應(yīng)的HTML字符。

url_escape(s)

使用urllib.quote_plus替換字符串s中的字符為URL編碼形式。

json_encode(val)

將val編碼成JSON格式。(在系統(tǒng)底層,這是一個(gè)對(duì)json庫(kù)的dumps函數(shù)的調(diào)用。查閱相關(guān)的文檔以獲得更多關(guān)于該函數(shù)接收和返回參數(shù)的信息。)

squeeze(s)

過(guò)濾字符串s,把連續(xù)的多個(gè)空白字符替換成一個(gè)空格。

在Tornado 1.x中,模版不是被自動(dòng)轉(zhuǎn)義的。在Tornado 2.0中,模板被默認(rèn)為自動(dòng)轉(zhuǎn)義(并且可以在Application構(gòu)造函數(shù)中使用autoscaping=None關(guān)閉)。在不同版本的遷移時(shí)要注意向后兼容。

在模板中使用一個(gè)你自己編寫(xiě)的函數(shù)也是很簡(jiǎn)單的:只需要將函數(shù)名作為模板的參數(shù)傳遞即可,就像其他變量一樣。

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

2.3 復(fù)雜示例:The Alpha Munger

在代碼清單2-4中,我們把在這一章中談?wù)撨^(guò)的所有東西都放了進(jìn)來(lái)。這個(gè)應(yīng)用被稱為T(mén)he Alpha Munger。用戶輸入兩個(gè)文本:一個(gè)"源"文本和一個(gè)"替代"文本。應(yīng)用會(huì)返回替代文本的一個(gè)副本,并將其中每個(gè)單詞替換成源文本中首字母相同的某個(gè)單詞。圖2-3展示了要填的表單,圖2-4展示了結(jié)果文本。

這個(gè)應(yīng)用包括四個(gè)文件:main.py(Tornado程序)、style.css(CSS樣式表文件)、index.html和munged.html(Tornado模板)。讓我們看看代碼吧:

代碼清單2-4 復(fù)雜表單和模板: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()

圖2-3

圖2-3 Alpha Munger:輸入表單

圖2-4

圖2-4 Alpha Munger:輸出

記住Application構(gòu)造函數(shù)中的static_path參數(shù)。我們將在下面進(jìn)行詳細(xì)的介紹,但是現(xiàn)在你所需要知道的就是static_path參數(shù)指定了你應(yīng)用程序放置靜態(tài)資源(如圖像、CSS文件、JavaScript文件等)的目錄。另外,你還需要在templates文件夾下添加index.html和munged.html這兩個(gè)文件。

代碼清單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中的內(nèi)容寫(xiě)到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; }

2.3.1 它如何工作

這個(gè)Tornado應(yīng)用定義了兩個(gè)請(qǐng)求處理類:IndexHandler和MungedPageHandler。IndexHandler類簡(jiǎn)單地渲染了index.html中的模板,其中包括一個(gè)允許用戶POST一個(gè)源文本(在source域中)和一個(gè)替換文本(在change域中)到/poem的表單。

MungedPageHandler類用于處理到/poem的POST請(qǐng)求。當(dāng)一個(gè)請(qǐng)求到達(dá)時(shí),它對(duì)傳入的數(shù)據(jù)進(jìn)行一些基本的處理,然后為瀏覽器渲染模板。map_by_first_letter方法將傳入的文本(從source域)分割成單詞,然后創(chuàng)建一個(gè)字典,其中每個(gè)字母表中的字母對(duì)應(yīng)文本中所有以其開(kāi)頭的單詞(我們將其放入一個(gè)叫作source_map的變量)。再把這個(gè)字典和用戶在替代文本(表單的change域)中指定的內(nèi)容一起傳給模板文件munged.html。此外,我們還將Python標(biāo)準(zhǔn)庫(kù)的random.choice函數(shù)傳入模板,這個(gè)函數(shù)以一個(gè)列表作為輸入,返回列表中的任一元素。

在munged.html中,我們迭代替代文本中的每行,再迭代每行中的每個(gè)單詞。如果當(dāng)前單詞的第一個(gè)字母是source_map字典的一個(gè)鍵,我們使用random.choice函數(shù)從字典的值中隨機(jī)選擇一個(gè)單詞并展示它。如果字典的鍵中沒(méi)有這個(gè)字母,我們展示源文本中的原始單詞。每個(gè)單詞包括一個(gè)span標(biāo)簽,其中的class屬性指定這個(gè)單詞是替換后的(class="replaced")還是原始的(class="unchanged")。(我們還將原始單詞放到了span標(biāo)簽的title屬性中,以便于用戶在鼠標(biāo)經(jīng)過(guò)單詞時(shí)可以查看是什么單詞被替代了。你可以在圖2-5中看到這個(gè)動(dòng)作。)

圖2-5

圖2-5 含有被替換單詞提示的Alpha Munger

在這個(gè)例子中,你可能注意到了debug=True的使用。它調(diào)用了一個(gè)便利的測(cè)試模式:tornado.autoreload模塊,此時(shí),一旦主要的Python文件被修改,Tornado將會(huì)嘗試重啟服務(wù)器,并且在模板改變時(shí)會(huì)進(jìn)行刷新。對(duì)于快速改變和實(shí)時(shí)更新這非常棒,但不要再生產(chǎn)上使用它,因?yàn)樗鼘⒎乐筎ornado緩存模板!

2.3.2 提供靜態(tài)文件

當(dāng)編寫(xiě)Web應(yīng)用時(shí),你總希望提供像樣式表、JavaScript文件和圖像這樣不需要為每個(gè)文件編寫(xiě)?yīng)毩⑻幚砗瘮?shù)的"靜態(tài)內(nèi)容"。Tornado提供了幾個(gè)有用的捷徑來(lái)使其變得容易。

2.3.2.1 設(shè)置靜態(tài)路徑

你可以通過(guò)向Application類的構(gòu)造函數(shù)傳遞一個(gè)名為static_path的參數(shù)來(lái)告訴Tornado從文件系統(tǒng)的一個(gè)特定位置提供靜態(tài)文件。Alpha Munger中的相關(guān)代碼片段如下:

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
)

在這里,我們?cè)O(shè)置了一個(gè)當(dāng)前應(yīng)用目錄下名為static的子目錄作為static_path的參數(shù)。現(xiàn)在應(yīng)用將以讀取static目錄下的filename.ext來(lái)響應(yīng)諸如/static/filename.ext的請(qǐng)求,并在響應(yīng)的主體中返回。

2.3.2.2 使用static_url生成靜態(tài)URL

Tornado模板模塊提供了一個(gè)叫作static_url的函數(shù)來(lái)生成static目錄下文件的URL。讓我們來(lái)看看在index.html中static_url的調(diào)用的示例代碼:

<link rel="stylesheet" href="{{ static_url("style.css") }}">

這個(gè)對(duì)static_url的調(diào)用生成了URL的值,并渲染輸出類似下面的代碼:

<link rel="stylesheet" href="/static/style.css?v=ab12">

那么為什么使用static_url而不是在你的模板中硬編碼呢?有如下幾個(gè)原因。其一,static_url函數(shù)創(chuàng)建了一個(gè)基于文件內(nèi)容的hash值,并將其添加到URL末尾(查詢字符串的參數(shù)v)。這個(gè)hash值確保瀏覽器總是加載一個(gè)文件的最新版而不是之前的緩存版本。無(wú)論是在你應(yīng)用的開(kāi)發(fā)階段,還是在部署到生產(chǎn)環(huán)境使用時(shí),都非常有用,因?yàn)槟愕挠脩舨槐卦贋榱丝吹侥愕撵o態(tài)內(nèi)容而清除瀏覽器緩存了。

另一個(gè)好處是你可以改變你應(yīng)用URL的結(jié)構(gòu),而不需要改變模板中的代碼。例如,你可以配置Tornado響應(yīng)來(lái)自像路徑/s/filename.ext的請(qǐng)求時(shí)提供靜態(tài)內(nèi)容,而不是默認(rèn)的/static路徑。如果你使用static_url而不是硬編碼的話,你的代碼不需要改變。比如說(shuō),你想把靜態(tài)資源從我們剛才使用的/static目錄移到新的/s目錄。你可以簡(jiǎn)單地改變靜態(tài)路徑由static變?yōu)閟,然后每個(gè)使用static_url包裹的引用都會(huì)被自動(dòng)更新。如果你在每個(gè)引用靜態(tài)資源的文件中硬編碼靜態(tài)路徑部分,你將不得不手動(dòng)修改每個(gè)模板。

2.3.3 模板的下一步

到目前為止,你已經(jīng)能夠處理Tornado模板系統(tǒng)的簡(jiǎn)單功能了。對(duì)于像Alpha Munger這樣簡(jiǎn)單的Web應(yīng)用而言,基礎(chǔ)的功能對(duì)你而言足夠用了。但是我們?cè)谀0宀糠值膶W(xué)習(xí)并沒(méi)有結(jié)束。Tornado在塊和模塊的形式上仍然有一些技巧,這兩個(gè)功能使得編寫(xiě)和維護(hù)復(fù)雜的Web應(yīng)用更加簡(jiǎn)單。我們將在第三章中看到這些功能。

以上內(nèi)容是否對(duì)您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)