1. 程式人生 > >第三章:模板擴展

第三章:模板擴展

gif 但是 pro 第三章 沖突 totally self 訪問網站 tel

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

3.1 塊和替換

當你花時間為你的Web應用建立和制定模板時,希望像你的後端Python代碼一樣重用你的前端代碼似乎只是合邏輯的,不是嗎?幸運的是,Tornado可以讓你做到這一點。Tornado通過extendsblock語句支持模板繼承,這就讓你擁有了編寫能夠在合適的地方復用的流體模板的控制權和靈活性。

為了擴展一個已經存在的模板,你只需要在新的模板文件的頂部放上一句{% 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:[email protected]">[email protected]</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 網站漏洞問題

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

技術分享圖片

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

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

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

舉個例子,如果Burt想在footer中使用模板變量設置email聯系鏈接,他將不會得到期望的HTML鏈接。考慮下面的模板片段:

{% set mailLink = "<a href="mailto:[email protected]">Contact Us</a>" %}
{{ mailLink }}‘

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

&lt;a href=&quot;mailto:[email protected]&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:[email protected]">[email protected]</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 包含樣式數據的圖書模塊

現在,我們可以定義一個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原生數據結構時是否簡單,在你的實際應用中硬編碼大數據結構的感覺可不好。下一步,我們將看到如何配合持久化存儲來處理存儲、提供和編輯動態內容。

第三章:模板擴展