範本與 UI

Tornado 包含一個簡單、快速且靈活的範本語言。本節將描述該語言以及相關問題,例如國際化。

Tornado 也可以與任何其他 Python 範本語言一起使用,儘管沒有提供將這些系統整合到 RequestHandler.render 的機制。只需將範本渲染成字串並將其傳遞給 RequestHandler.write 即可。

設定範本

預設情況下,Tornado 會在與引用它們的 .py 檔案相同的目錄中尋找範本檔案。若要將範本檔案放在不同的目錄中,請使用 template_path Application 設定 (或覆寫 RequestHandler.get_template_path,如果您對不同的處理器有不同的範本路徑)。

若要從非檔案系統位置載入範本,請繼承 tornado.template.BaseLoader 並將一個實例作為 template_loader 應用程式設定傳遞。

預設情況下,已編譯的範本會被快取;若要關閉此快取並重新載入範本,以便始終可見基礎檔案的變更,請使用應用程式設定 compiled_template_cache=Falsedebug=True

範本語法

Tornado 範本只是 HTML (或任何其他基於文字的格式),其中嵌入了 Python 控制序列和表達式。

<html>
   <head>
      <title>{{ title }}</title>
   </head>
   <body>
     <ul>
       {% for item in items %}
         <li>{{ escape(item) }}</li>
       {% end %}
     </ul>
   </body>
 </html>

如果您將此範本另存為 “template.html” 並將其放在與您的 Python 檔案相同的目錄中,您可以使用以下程式碼呈現此範本

class MainHandler(tornado.web.RequestHandler):
    def get(self):
        items = ["Item 1", "Item 2", "Item 3"]
        self.render("template.html", title="My title", items=items)

Tornado 範本支援控制語句表達式。控制語句以 {%%} 包圍,例如 {% if len(items) > 2 %}。表達式以 {{}} 包圍,例如 {{ items[0] }}

控制語句或多或少與 Python 語句完全對應。我們支援 ifforwhiletry,所有這些都以 {% end %} 終止。我們也支援使用 extendsblock 語句進行範本繼承,這些語句在 tornado.template 的文件中詳細說明。

表達式可以是任何 Python 表達式,包括函式呼叫。範本程式碼會在包含以下物件和函式的命名空間中執行。(請注意,此列表適用於使用 RequestHandler.renderrender_string 呈現的範本。如果您在 RequestHandler 之外直接使用 tornado.template 模組,則其中許多條目不存在)。

當您建構實際應用程式時,您會想要使用 Tornado 樣板的所有功能,尤其是樣板繼承。請在 tornado.template 章節中詳閱這些功能(某些功能,包括 UIModules,實作於 tornado.web 模組中)

在底層,Tornado 樣板會直接轉換為 Python。您在樣板中包含的表達式會逐字複製到代表您樣板的 Python 函式中。我們不會嘗試阻止樣板語言中的任何東西;我們明確建立它是為了提供其他更嚴格的樣板系統所無法提供的彈性。因此,如果您在樣板表達式中寫入隨機的東西,當您執行樣板時,會得到隨機的 Python 錯誤。

預設情況下,所有樣板輸出都會使用 tornado.escape.xhtml_escape 函式進行跳脫處理。此行為可以透過傳遞 autoescape=NoneApplicationtornado.template.Loader 建構函式,對於具有 {% autoescape None %} 指令的樣板檔案,或是透過以 {% raw ...%} 取代 {{ ... }} 來變更單一表達式。此外,在這些地方的每一個地方,都可以使用替代的跳脫函式的名稱來取代 None

請注意,雖然 Tornado 的自動跳脫處理有助於避免 XSS 漏洞,但在所有情況下都不足夠。出現在特定位置(例如在 JavaScript 或 CSS 中)的表達式可能需要額外的跳脫處理。此外,必須小心始終在可能包含不受信任內容的 HTML 屬性中使用雙引號和 xhtml_escape,否則必須為屬性使用單獨的跳脫函式(例如,請參閱 這篇部落格文章)。

國際化

目前使用者(無論他們是否已登入)的地區設定始終可在請求處理程式中以 self.locale 取得,在樣板中則以 locale 取得。地區設定的名稱(例如 en_US)可作為 locale.name 取得,您可以使用 Locale.translate 方法翻譯字串。樣板也有可用的全域函式呼叫 _() 來進行字串翻譯。翻譯函式有兩種形式

_("Translate this string")

這會根據目前的地區設定直接翻譯字串,以及

_("A person liked this", "%(num)d people liked this",
  len(people)) % {"num": len(people)}

這會根據第三個引數的值翻譯可以是單數或複數的字串。在上面的範例中,如果 len(people)1,則會傳回第一個字串的翻譯,否則會傳回第二個字串的翻譯。

翻譯最常見的模式是使用 Python 具名佔位符來表示變數(上述範例中的 %(num)d),因為佔位符可以在翻譯時移動。

這是一個正確國際化的樣板

<html>
   <head>
      <title>FriendFeed - {{ _("Sign in") }}</title>
   </head>
   <body>
     <form action="{{ request.path }}" method="post">
       <div>{{ _("Username") }} <input type="text" name="username"/></div>
       <div>{{ _("Password") }} <input type="password" name="password"/></div>
       <div><input type="submit" value="{{ _("Sign in") }}"/></div>
       {% module xsrf_form_html() %}
     </form>
   </body>
 </html>

預設情況下,我們會使用使用者瀏覽器傳送的 Accept-Language 標頭來偵測使用者的地區設定。如果找不到適當的 Accept-Language 值,我們會選擇 en_US。如果您讓使用者將他們的地區設定設定為偏好設定,您可以透過覆寫 RequestHandler.get_user_locale 來覆寫此預設地區設定選擇。

class BaseHandler(tornado.web.RequestHandler):
    def get_current_user(self):
        user_id = self.get_signed_cookie("user")
        if not user_id: return None
        return self.backend.get_user_by_id(user_id)

    def get_user_locale(self):
        if "locale" not in self.current_user.prefs:
            # Use the Accept-Language header
            return None
        return self.current_user.prefs["locale"]

如果 get_user_locale 傳回 None,我們會退回到 Accept-Language 標頭。

tornado.locale 模組支援以兩種格式載入翻譯:gettext 和相關工具使用的 .mo 格式,以及簡單的 .csv 格式。應用程式通常會在啟動時呼叫 tornado.locale.load_translationstornado.locale.load_gettext_translations 一次;請參閱這些方法以取得有關支援格式的更多詳細資訊。

您可以使用 tornado.locale.get_supported_locales() 取得應用程式中支援的地區設定清單。使用者地區設定的選擇是基於支援的地區設定中最近似的符合項。例如,如果使用者的地區設定是 es_GT,且支援 es 地區設定,則該請求的 self.locale 將為 es。如果找不到最近似的符合項,我們會退回到 en_US

UI 模組

Tornado 支援 UI 模組,以便輕鬆支援應用程式中標準、可重複使用的 UI 小工具。UI 模組就像是特殊函式呼叫,用於呈現頁面的元件,而且它們可以附帶自己的 CSS 和 JavaScript。

例如,如果您正在實作部落格,而且您希望部落格文章同時出現在部落格首頁和每篇部落格文章頁面上,您可以製作一個 Entry 模組,以便在兩個頁面上呈現它們。首先,為您的 UI 模組建立一個 Python 模組,例如 uimodules.py

class Entry(tornado.web.UIModule):
    def render(self, entry, show_comments=False):
        return self.render_string(
            "module-entry.html", entry=entry, show_comments=show_comments)

告知 Tornado 使用應用程式中的 ui_modules 設定來使用 uimodules.py

from . import uimodules

class HomeHandler(tornado.web.RequestHandler):
    def get(self):
        entries = self.db.query("SELECT * FROM entries ORDER BY date DESC")
        self.render("home.html", entries=entries)

class EntryHandler(tornado.web.RequestHandler):
    def get(self, entry_id):
        entry = self.db.get("SELECT * FROM entries WHERE id = %s", entry_id)
        if not entry: raise tornado.web.HTTPError(404)
        self.render("entry.html", entry=entry)

settings = {
    "ui_modules": uimodules,
}
application = tornado.web.Application([
    (r"/", HomeHandler),
    (r"/entry/([0-9]+)", EntryHandler),
], **settings)

在樣板中,您可以使用 {% module %} 陳述式呼叫模組。例如,您可以從 home.html 呼叫 Entry 模組

{% for entry in entries %}
  {% module Entry(entry) %}
{% end %}

entry.html

{% module Entry(entry, show_comments=True) %}

模組可以透過覆寫 embedded_cssembedded_javascriptjavascript_filescss_files 方法來包含自訂 CSS 和 JavaScript 函式

class Entry(tornado.web.UIModule):
    def embedded_css(self):
        return ".entry { margin-bottom: 1em; }"

    def render(self, entry, show_comments=False):
        return self.render_string(
            "module-entry.html", show_comments=show_comments)

無論模組在頁面上使用多少次,模組 CSS 和 JavaScript 都會包含一次。CSS 始終包含在頁面的 <head> 中,而 JavaScript 始終包含在頁面結尾的 </body> 標記之前。

當不需要額外的 Python 程式碼時,樣板檔案本身可以用作模組。例如,可以重寫前面的範例,將下列程式碼放入 module-entry.html

{{ set_resources(embedded_css=".entry { margin-bottom: 1em; }") }}
<!-- more template html... -->

這個修改後的樣板模組會以

{% module Template("module-entry.html", show_comments=True) %}

呼叫。set_resources 函式僅在透過 {% module Template(...) %} 叫用的樣板中可用。與 {% include ... %} 指令不同,樣板模組具有與其包含樣板不同的命名空間 - 它們只能看到全域樣板命名空間和它們自己的關鍵字引數。