第三章:模板擴展

第二章中,咱們看到了Tornado模板系統如何簡單地傳遞信息給網頁,使你在插入動態數據時保持網頁標記的整潔。然而,大多數站點但願複用像header、footer和佈局網格這樣的內容。在這一章中,咱們將看到如何使用擴展Tornado模板或UI模塊完成這一工做。javascript

3.1 塊和替換

當你花時間爲你的Web應用創建和制定模板時,但願像你的後端Python代碼同樣重用你的前端代碼彷佛只是合邏輯的,不是嗎?幸運的是,Tornado可讓你作到這一點。Tornado經過extendsblock語句支持模板繼承,這就讓你擁有了編寫可以在合適的地方複用的流體模板的控制權和靈活性。css

爲了擴展一個已經存在的模板,你只須要在新的模板文件的頂部放上一句{% extends "filename.html" %}。好比,爲了在新模板中擴展一個父模板(在這裏假設爲main.html),你能夠這樣使用:html

{% extends "main.html" %}

這就使得新文件繼承main.html的全部標籤,而且覆寫爲指望的內容。前端

3.1.1 塊基礎

擴展一個模板使你複用以前寫過的代碼更加簡單,可是這並不會爲你提供全部的東西,除非你能夠適應並改變那些以前的模板。因此,block語句出現了。java

一個塊語句壓縮了一些當你擴展時可能想要改變的模板元素。好比,爲了使用一個可以根據不一樣頁覆寫的動態header塊,你能夠在父模板main.html中添加以下代碼:python

<header>
    {% block header %}{% end %}
</header>

而後,爲了在子模板index.html中覆寫{% block header %}{% end %}部分,你可使用塊的名字引用,並把任何你想要的內容放到其中。jquery

{% block header %}{% end %}

{% block header %}
    <h1>Hello world!</h1>
{% end %}

任何繼承這個模板的文件均可以包含它本身的{% block header %}{% end %},而後把一些不一樣的東西加進去。web

爲了在Web應用中調用這個子模板,你能夠在你的Python腳本中很輕鬆地渲染它,就像以前你渲染其餘模板那樣:ajax

class MainHandler(tornado.web.RequestHandler):
    def get(self):
        self.render("index.html")

因此此時,main.html中的body塊在加載時會被以index.html中的信息"Hello world!"填充(參見圖3-1)。數據庫

圖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

圖3-2 塊基礎

你也能夠保留父模板塊語句中的默認文本和標記,就像擴展模板沒有指定它本身的塊版本同樣被渲染。這種狀況下,你能夠根據某頁的狀況只替換必須的東西,這在包含或替換腳本、CSS文件和標記塊時很是有用。

正如模板文檔所記錄的,"錯誤報告目前...呃...是很是有意思的"。一個語法錯誤或者沒有閉合的{% block %}語句可使得瀏覽器直接顯示500: Internal Server Error(若是你運行在debug模式下會引起完整的Python堆棧跟蹤)。如圖3-3所示。

總之,爲了你本身好的話,你須要使本身的模板儘量的魯棒,而且在模板被渲染以前發現錯誤。

圖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 &lt;script&gt;alert('RUNNING EVIL H4CKS AND SPL01TS NOW...')&lt;/script&gt;
圖3-4

圖3-4 網站漏洞問題

如今當Alice訪問網站時,沒有惡意腳本被執行,因此她看到的頁面如圖3-5所示。

圖3-5

圖3-5 網站漏洞問題--解決

在Tornado1.x版本中,模板沒有被自動轉義,因此咱們以前談論的防禦措施須要顯式地在未過濾的用戶輸入上調用escape()函數。

因此在這裏,咱們能夠看到自動轉義是如何防止你的訪客進行惡意攻擊的。然而,當經過模板和模塊提供HTML動態內容時它仍會讓你措手不及。

舉個例子,若是Burt想在footer中使用模板變量設置email聯繫連接,他將不會獲得指望的HTML連接。考慮下面的模板片斷:

{% set mailLink = "<a href="mailto:contact@burtsbooks.com">Contact Us</a>" %}
{{ mailLink }}'

它會在頁面源代碼中渲染成以下代碼:

&lt;a href=&quot;mailto:contact@burtsbooks.com&quot;&gt;Contact Us&lt;/a&gt;

此時自動轉義被運行了,很明顯,這沒法讓人們聯繫上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_javascriptembedded_cssjavascript_filescss_files方法來實現這一方法。

3.2.1 基礎模塊使用

爲了在你的模板中引用模塊,你必須在應用的設置中聲明它。ui_modules參數指望一個模塊名爲鍵、類爲值的字典輸入來渲染它們。考慮代碼清單3-1。

代碼清單3-1 模塊基礎:hello_module.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 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類的時候,咱們將調用繼承自UIModulerender_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

圖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_cssembedded_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 {Liberation Mono", Courier, monospace; margin: 0px 2px; font-weight: bold; font-style: normal;"><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原生數據結構時是否簡單,在你的實際應用中硬編碼大數據結構的感受可很差。下一步,咱們將看到如何配合持久化存儲來處理存儲、提供和編輯動態內容。

相關文章
相關標籤/搜索