阅读(4390) (0)

Tornado 模板和用户界面

2022-03-07 14:47:24 更新

Tornado 包含一种简单、快速且灵活的模板语言。本节介绍该语言以及国际化等相关问题。

Tornado 也可以与任何其他 Python 模板语言一起使用,尽管没有将这些系统集成到 RequestHandler.render. 只需将模板呈现为字符串并将其传递给RequestHandler.write

配置模板

默认情况下,Tornado 在与引用它们的 ​.py ​文件相同的目录中查找模板文件。 要将您的模板文件放在不同的目录中,请使用 ​template_path​ 应用程序设置(或者如果您对不同的处理程序有不同的模板路径,则覆盖 ​RequestHandler.get_template_path​)。

要从非文件系统位置加载模板,子类 ​tornado.template.BaseLoader​传递一个实例作为​template_loader​应用程序设置。

编译后的模板默认缓存; 要关闭此缓存并重新加载模板以对底层文件的更改始终可见,请使用应用程序设置​compiled_template_cache=False​ 或​debug=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 %}​。 表达式被 ​{{​ 和 ​}}​ 包围,例如 ​{{ item[0] }}​。

控制语句大多数精确地映射到 Python 语句。 它支持 ​if​、​for​、​while ​和 ​try​,它们都以 ​{% end %}​ 结束。 它还支持使用 ​extends ​和 ​block ​语句的模板继承,在 tornado.template 的文档中有详细描述。

表达式可以是任何 Python 表达式,包括函数调用。 模板代码在包含以下对象和函数的命名空间中执行。 (请注意,此列表适用于使用 RequestHandler.render 和 render_string 呈现的模板。如果您直接在 RequestHandler 之外使用 tornado.template 模块,则其中许多条目不存在)。

当你构建一个真正的应用程序时,你会想要使用 Tornado 模板的所有特性,尤其是模板继承。阅读本节中有关这些功能的所有信息tornado.template (一些功能,包括​UIModules​在 tornado.web模块中实现)

在底层,Tornado 模板被直接翻译成 Python。您包含在模板中的表达式被逐字复制到代表您的模板的 Python 函数中。它不会试图阻止模板语言中的任何内容;它明确创建它是为了提供其他更严格的模板系统所阻止的灵活性。因此,如果您在模板表达式中编写随机内容,则在执行模板时会出现随机 Python 错误。

默认情况下,使用 tornado.escape.xhtml_escape 函数对所有模板输出进行转义。 可以通过将 ​autoescape=None​ 传递给 Application 或 tornado.template.Loader 构造函数来全局更改此行为,对于具有 ​{% autoescape None %}​ 指令的模板文件,或者通过替换 ​{{ ... }}​ 来更改单个表达式 使用 ​{% raw ...%}​。 此外,在这里的每一个地方,都可以使用替代转义函数的名称来代替 ​None​。

请注意,虽然 Tornado 的自动转义有助于避免 XSS 漏洞,但并非在所有情况下都足够。 出现在某些位置的表达式,例如 JavaScript 或 CSS,可能需要额外的转义。 此外,必须注意始终在可能包含不受信任的内容的 HTML 属性中使用双引号和 xhtml_escape,或者必须为属性使用单独的转义函数(参见例如此博客文章)。

国际化

当前用户的语言环境(无论他们是否登录)始终在请求处理程序中作为​self.locale​和在模板中作为​locale​可用。 语言环境的名称(例如 ​en_US​)以 ​locale.name​的形式提供,您可以使用 Locale.translate 方法翻译字符串。 模板还具有可用于字符串翻译的全局函数调用 ​_()​。 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_secure_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_translations 或 tornado.locale.load_gettext_translations

您可以使用 tornado.locale.get_supported_locales() 获取应用程序中支持的语言环境列表。 根据支持的语言环境,用户的语言环境被选择为最接近的匹配。 例如,如果用户的语言环境是 ​es_GT​,并且支持 ​es​ 语言环境,那么对于该请求,​self.locale​ 将是 ​es​。 如果找不到匹配项,我们将使用 ​en_US​。

用户界面模块

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)

使用应用程序中的 ​ui_modules​ 设置告诉 Tornado 使用 ​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_css​、​embedded_javascript​、​javascript_files​ 或 ​css_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 ... %}​ 指令不同,模板模块具有与其包含模板不同的命名空间——它们只能看到全局模板命名空间和它们自己的关键字参数。