在第二章中,我們看到了Tornado模板系統(tǒng)如何簡單地傳遞信息給網(wǎng)頁,使你在插入動態(tài)數(shù)據(jù)時保持網(wǎng)頁標記的整潔。然而,大多數(shù)站點希望復(fù)用像header、footer和布局網(wǎng)格這樣的內(nèi)容。在這一章中,我們將看到如何使用擴展Tornado模板或UI模塊完成這一工作。
當你花時間為你的Web應(yīng)用建立和制定模板時,希望像你的后端Python代碼一樣重用你的前端代碼似乎只是合邏輯的,不是嗎?幸運的是,Tornado可以讓你做到這一點。Tornado通過extends和block語句支持模板繼承,這就讓你擁有了編寫能夠在合適的地方復(fù)用的流體模板的控制權(quán)和靈活性。
為了擴展一個已經(jīng)存在的模板,你只需要在新的模板文件的頂部放上一句{% extends "filename.html" %}。
比如,為了在新模板中擴展一個父模板(在這里假設(shè)為main.html),你可以這樣使用:
{% extends "main.html" %}
這就使得新文件繼承main.html的所有標簽,并且覆寫為期望的內(nèi)容。
擴展一個模板使你復(fù)用之前寫過的代碼更加簡單,但是這并不會為你提供所有的東西,除非你可以適應(yīng)并改變那些之前的模板。所以,block語句出現(xiàn)了。
一個塊語句壓縮了一些當你擴展時可能想要改變的模板元素。比如,為了使用一個能夠根據(jù)不同頁覆寫的動態(tài)header塊,你可以在父模板main.html中添加如下代碼:
<header>
{% block header %}{% end %}
</header>
然后,為了在子模板index.html中覆寫{% block header %}{% end %}
部分,你可以使用塊的名字引用,并把任何你想要的內(nèi)容放到其中。
{% block header %}{% end %}
{% block header %}
<h1>Hello world!</h1>
{% end %}
任何繼承這個模板的文件都可以包含它自己的{% block header %}
和{% end %}
,然后把一些不同的東西加進去。
為了在Web應(yīng)用中調(diào)用這個子模板,你可以在你的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!
我們已經(jīng)可以看到這種方法在處理整體頁面結(jié)構(gòu)和節(jié)約多頁面網(wǎng)站的開發(fā)時間上多么有用。更好的是,你可以為每個頁面使用多個塊,此時像header和footer這樣的動態(tài)元素將會被包含在同一個流程中。
下面是一個在父模板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 塊基礎(chǔ)
你也可以保留父模板塊語句中的默認文本和標記,就像擴展模板沒有指定它自己的塊版本一樣被渲染。這種情況下,你可以根據(jù)某頁的情況只替換必須的東西,這在包含或替換腳本、CSS文件和標記塊時非常有用。
正如模板文檔所記錄的,"錯誤報告目前...呃...是非常有意思的"。一個語法錯誤或者沒有閉合的{% block %}語句可以使得瀏覽器直接顯示500: Internal Server Error(如果你運行在debug模式下會引發(fā)完整的Python堆棧跟蹤)。如圖3-3所示。
總之,為了你自己好的話,你需要使自己的模板盡可能的魯棒,并且在模板被渲染之前發(fā)現(xiàn)錯誤。
圖3-3 塊錯誤
所以,你會認為這聽起來很有趣,但卻不能描繪出在一個標準的Web應(yīng)用中如何使用?那么讓我們在這里看一個例子,我們的朋友Burt希望運行一個名叫Burt's Books的書店。
Burt通過他的書店賣很多書,他的網(wǎng)站會展示很多不同的內(nèi)容,比如新品推薦、商店信息等等。Burt希望有一個固定的外觀和感覺的網(wǎng)站,同時也能更簡單的更新頁面和段落。
為了做到這些,Burt's Book使用了以Tornado為基礎(chǔ)的網(wǎng)站,其中包括一個擁有樣式、布局和header/footer細節(jié)的主模版,以及一個處理頁面的輕量級的子模板。在這個系統(tǒng)中,Burt可以把最新發(fā)布、員工推薦、即將發(fā)行等不同頁面編寫在一起,共同使用通用的基礎(chǔ)屬性。
Burt's Book的網(wǎng)站使用一個叫作main.html的主要基礎(chǔ)模板,用來包含網(wǎng)站的通用架構(gòu),如下面的代碼所示:
<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="https://atts.w3cschool.cn/attachments/image/cimg/script.js") }}"></script>
</body>
</html>
這個頁面定義了結(jié)構(gòu),應(yīng)用了一個CSS樣式表,并加載了主要的JavaScript文件。其他模板可以擴展它,在必要時替換header、body和footer塊。
這個網(wǎng)站的index頁(index.html)歡迎友好的網(wǎng)站訪問者并提供一些商店的信息。通過擴展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模板的默認行為,繼承了來自父模板的聯(lián)系信息。
為了運作網(wǎng)站,傳遞信息給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()
這個例子的結(jié)構(gòu)和我們之前見到的不太一樣,但你一點都不需要害怕。我們不再像之前那樣通過使用一個處理類列表和一些其他關(guān)鍵字參數(shù)調(diào)用tornado.web.Application的構(gòu)造函數(shù)來創(chuàng)建實例,而是定義了我們自己的Application子類,在這里我們簡單地稱之為Application。在我們定義的init方法中,我們創(chuàng)建了處理類列表以及一個設(shè)置的字典,然后在初始化子類的調(diào)用中傳遞這些值,就像下面的代碼一樣:
tornado.web.Application.__init__(self, handlers, **settings)
所以在這個系統(tǒng)中,Burt's Book可以很容易地改變index頁面并保持基礎(chǔ)模板在其他頁面被使用時完好。此外,他們可以充分利用Tornado的真實能量,由Python腳本和/或數(shù)據(jù)庫提供動態(tài)內(nèi)容。我們將在之后看到更多相關(guān)的內(nèi)容。
Tornado默認會自動轉(zhuǎn)義模板中的內(nèi)容,把標簽轉(zhuǎn)換為相應(yīng)的HTML實體。這樣可以防止后端為數(shù)據(jù)庫的網(wǎng)站被惡意腳本攻擊。比如,你的網(wǎng)站中有一個評論部分,用戶可以在這里添加任何他們想說的文字進行討論。雖然一些HTML標簽在標記和樣式?jīng)_突時不構(gòu)成重大威脅(如評論中沒有閉標簽),但標簽會允許攻擊者加載其他的JavaScript文件,打開通向跨站腳本攻擊、XSS或漏洞之門。
讓我們考慮Burt's Book網(wǎng)站上的一個用戶反饋頁面。Melvin,今天感覺特別邪惡,在評論里提交了下面的文字:
Totally hacked your site lulz <script>alert('RUNNING EVIL H4CKS AND SPL01TS NOW...')</script>
當我們在沒有轉(zhuǎn)義用戶內(nèi)容的情況下給一個不知情的用戶構(gòu)建頁面時,腳本標簽被作為一個HTML元素解釋,并被瀏覽器執(zhí)行,所以Alice看到了如圖3-4所示的提示窗口。幸虧Tornado會自動轉(zhuǎn)義在雙大括號間被渲染的表達式。更早地轉(zhuǎn)義Melvin輸入的文本不會激活HTML標簽,并且會渲染為下面的字符串:
Totally hacked your site lulz <script>alert('RUNNING EVIL H4CKS AND SPL01TS NOW...')</script>
圖3-4 網(wǎng)站漏洞問題
現(xiàn)在當Alice訪問網(wǎng)站時,沒有惡意腳本被執(zhí)行,所以她看到的頁面如圖3-5所示。
圖3-5 網(wǎng)站漏洞問題--解決
在Tornado1.x版本中,模板沒有被自動轉(zhuǎn)義,所以我們之前談?wù)摰姆雷o措施需要顯式地在未過濾的用戶輸入上調(diào)用escape()函數(shù)。
所以在這里,我們可以看到自動轉(zhuǎn)義是如何防止你的訪客進行惡意攻擊的。然而,當通過模板和模塊提供HTML動態(tài)內(nèi)容時它仍會讓你措手不及。
舉個例子,如果Burt想在footer中使用模板變量設(shè)置email聯(lián)系鏈接,他將不會得到期望的HTML鏈接??紤]下面的模板片段:
{% set mailLink = "<a href="mailto:contact@burtsbooks.com">Contact Us</a>" %}
{{ mailLink }}'
它會在頁面源代碼中渲染成如下代碼:
<a href="mailto:contact@burtsbooks.com">Contact Us</a>
此時自動轉(zhuǎn)義被運行了,很明顯,這無法讓人們聯(lián)系上Burt。
為了處理這種情況,你可以禁用自動轉(zhuǎn)義,一種方法是在Application構(gòu)造函數(shù)中傳遞autoescape=None,另一種方法是在每頁的基礎(chǔ)上修改自動轉(zhuǎn)義行為,如下所示:
{% autoescape None %}
{{ mailLink }}
這些autoescape塊不需要結(jié)束標簽,并且可以設(shè)置xhtml_escape來開啟自動轉(zhuǎn)義(默認行為),或None來關(guān)閉。
然而,在理想的情況下,你希望保持自動轉(zhuǎn)義開啟以便繼續(xù)防護你的網(wǎng)站。因此,你可以使用{% raw %}指令來輸出不轉(zhuǎn)義的內(nèi)容。
{% raw mailLink %}
需要特別注意的是,當你使用諸如Tornado的linkify()和xsrf_form_html()函數(shù)時,自動轉(zhuǎn)義的設(shè)置被改變了。所以如果你希望在前面代碼的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()簡記的好處,又可以保持在其他地方自動轉(zhuǎn)義的好處。
正如前面我們所看到的,模板系統(tǒng)既輕量級又強大。在實踐中,我們希望遵循軟件工程的諺語,Don't Repeat Yourself。為了消除冗余的代碼,我們可以使模板部分模塊化。比如,展示物品列表的頁面可以定位一個單獨的模板用來渲染每個物品的標記。另外,一組共用通用導(dǎo)航結(jié)構(gòu)的頁面可以從一個共享的模塊渲染內(nèi)容。Tornado的UI模塊在這種情況下特別有用
UI模塊是封裝模板中包含的標記、樣式以及行為的可復(fù)用組件。它所定義的元素通常用于多個模板交叉復(fù)用或在同一個模板中重復(fù)使用。模塊本身是一個繼承自Tornado的UIModule類的簡單Python類,并定義了一個render方法。當一個模板使用{% module Foo(...) %}標簽引用一個模塊時,Tornado的模板引擎調(diào)用模塊的render方法,然后返回一個字符串來替換模板中的模塊標簽。UI模塊也可以在渲染后的頁面中嵌入自己的JavaScript和CSS文件,或指定額外包含的JavaScript或CSS文件。你可以定義可選的embedded_javascript、embedded_css、javascript_files和css_files方法來實現(xiàn)這一方法。
為了在你的模板中引用模塊,你必須在應(yīng)用的設(shè)置中聲明它。ui_moudles參數(shù)期望一個模塊名為鍵、類為值的字典輸入來渲染它們??紤]代碼清單3-1。
代碼清單3-1 模塊基礎(chǔ):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類結(jié)合了起來。
現(xiàn)在,當調(diào)用HelloHandler并渲染hello.html時,我們可以使用{% module Hello() %}模板標簽來包含HelloModule類中render方法返回的字符串。
<html>
<head><title>UI Module Example</title></head>
<body>
{% module Hello() %}
</body>
</html>
這個hello.html模板通過在模塊標簽自身的位置調(diào)用HelloModule返回的字符串進行填充。下一節(jié)的例子將會展示如何擴展UI模塊來渲染它們自己的模板并包含腳本和樣式表。
很多時候,一個非常有用的做法是讓模塊指向一個模板文件而不是在模塊類中直接渲染字符串。這些模板的標記看起來就像我們已經(jīng)看到過的作為整體的模板。
UI模塊的一個常見應(yīng)用是迭代數(shù)據(jù)庫或API查詢中獲得的結(jié)果,為每個獨立項目的數(shù)據(jù)渲染相同的標記。比如,Burt想在Burt's Book里創(chuàng)建一個推薦閱讀部分,他已經(jīng)創(chuàng)建了一個名為recommended.html的模板,其代碼如下所示。就像前面看到的那樣,我們將使用{% module Book(book) %}標簽調(diào)用模塊。
{% extends "main.html" %}
{% block body %}
<h2>Recommended Reading</h2>
{% for book in books %}
{% module Book(book) %}
{% end %}
{% end %}
Burt還創(chuàng)建了一個叫作book.html的圖書模塊的模板,并把它放到了templates/modules目錄下。一個簡單的圖書模板看起來像下面這樣:
<div class="book">
<h3 class="book_title">{{ book["title"] }}</h3>
<img src="{{ book["image"] }}" class="book_image"/>
</div>
現(xiàn)在,當我們定義BookModule類的時候,我們將調(diào)用繼承自UIModule的render_string方法。這個方法顯式地渲染模板文件,當我們返回給調(diào)用者時將其關(guān)鍵字參數(shù)作為一個字符串。
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參數(shù)的每項都將會調(diào)用這個模塊。每次使用一個新的book參數(shù)調(diào)用Book模塊時,模塊(以及book.html模板)可以引用book參數(shù)的字典中的項,并以適合的方式格式化數(shù)據(jù)(如圖3-6)。
圖3-6 包含樣式數(shù)據(jù)的圖書模塊
現(xiàn)在,我們可以定義一個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參數(shù)中添加映射值。因為模板可以指向任何定義在ui_modules字典中的模塊,所以在自己的模塊中指定功能非常容易。
在這個例子中,你可能已經(jīng)注意到了locale.format_date()的使用。它調(diào)用了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使用時可以隱藏時間,只顯示月、日和年。
這個模塊在你處理時間和日期時非常有用,并且還提供了處理本地化字符串的支持。
為了給這些模塊提供更高的靈活性,Tornado允許你使用embedded_css和embedded_javascript方法嵌入其他的CSS和JavaScript文件。舉個例子,如果你想在調(diào)用模塊時給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!\")"
當調(diào)用模塊時,document.write(\"hi!\")將被包圍,并被插入到的閉標簽中:
<script type="text/javascript">
//<![CDATA[
document.write("hi!")
//]]>
</script>
顯然,只是在文檔主體中寫這些內(nèi)容并不是世界上最有用的事情,而我們還有另一個給予你極大靈活性的選項,當創(chuàng)建這些模塊時,可以在每個模塊中包含JavaScript文件。
類似的,你也可以把只在這些模塊被調(diào)用時加載的額外的CSS規(guī)則放進來:
def embedded_css(self):
return ".book {background-color:#F5F5F5}"
在這種情況下,.book {background-color:#555}
這條CSS規(guī)則被包裹在中,并被直接添加到的閉標簽之前。
<style type="text/css">
.book {background-color:#F5F5F5}
</style>
更加靈活的是,你甚至可以簡單地使用html_body()來在閉合的標簽前添加完整的HTML標記:
def html_body(self):
return "<script>document.write("Hello!")</script>"
顯然,雖然直接內(nèi)嵌添加腳本和樣式表很有用,但是為了更嚴謹?shù)陌ㄒ约案麧嵉拇a?。?,添加樣式表和腳本文件會顯得更好。他們的工作方式基本相同,所以你可以使用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"
當一個模塊需要額外的庫而應(yīng)用的其他地方不是必需的時候,這種方式非常有用。比如,你有一個使用JQuery UI庫的模塊(而在應(yīng)用的其他地方都不會被使用),你可以只在這個樣本模塊中加載jquery-ui.min.js文件,減少那些不需要它的頁面的加載時間。
因為模塊的內(nèi)嵌JavaScript和內(nèi)嵌HTML函數(shù)的目標都是緊鄰標簽,html_body()、javascript_files()和embedded_javascript()都會將內(nèi)容渲染后插到頁面底部,那么它們出現(xiàn)的順序正好是你指定它們的順序的倒序。
如果你有一個模塊如下面的代碼所示:
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()最先被編寫,它緊挨著出現(xiàn)在標簽的上面。embedded_javascript()接著被渲染,最后是javascript_files()。你可以在圖3-7中看到它是如何工作的。
需要小心的是,你不能包括一個需要其他地方東西的方法(比如依賴其他文件的JavaScript函數(shù)),因為此時他們可能會按照和你期望不同的順序進行渲染。
總之,模塊允許你在模板中渲染格式化數(shù)據(jù)時非常靈活,同時也讓你能夠只在調(diào)用模塊時包含指定的一些額外的樣式和函數(shù)規(guī)則。
正如我們之前看到的,Tornado使擴展模板更容易,以便你的網(wǎng)站代碼可以在整個應(yīng)用中輕松復(fù)用。而使用模塊后,你可以在什么文件、樣式和腳本動作需要被包括進來這個問題上擁有更細粒度的決策。然而,我們的例子依賴于使用Python原生數(shù)據(jù)結(jié)構(gòu)時是否簡單,在你的實際應(yīng)用中硬編碼大數(shù)據(jù)結(jié)構(gòu)的感覺可不好。下一步,我們將看到如何配合持久化存儲來處理存儲、提供和編輯動態(tài)內(nèi)容。
更多建議: