模板和UI

.. testsetup::

import tornado.web

Tornado 包含一个简单,快速并灵活的模板语言. 本节介绍了语言以及相 关的问题,比如国际化.

Tornado 也可以使用其他的Python模板语言, 虽然没有准备把这些系统整合 到 .RequestHandler.render 里面. 而是简单的将模板转成字符串并传递 给 .RequestHandler.write

配置模板

  1. 默认情况下, Tornado会在和当前 ``.py`` 文件相同的目录查找关联的模板
  2. 文件. 如果想把你的模板文件放在不同的目录中, 可以使用
  3. ``template_path`` `Application setting
  4. <.Application.settings>` (或复写 `.RequestHandler.get_template_path`
  5. 如果你不同的处理函数有不同的模板路径).
  6. 为了从非文件系统位置加载模板, 实例化子类 `tornado.template.BaseLoader`
  7. 并为其在应用设置(application setting)中配置
  8. ``template_loader`` .
  9. 默认情况下编译出来的模板会被缓存; 为了关掉这个缓存也为了使(对目标的)
  10. 修改在重新加载后总是可见, 使用应用设置(application settings)中的
  11. ``compiled_template_cache=False`` ``debug=True``.
  12. 模板语法
  13. ~~~~~~~~~~~~~~~
  14. 一个Tornado模板仅仅是用一些标记把Python控制序列和表达式嵌入
  15. HTML(或者任意其他文本格式)的文件中::
  16. <html>
  17. <head>
  18. <title>{{ title }}</title>
  19. </head>
  20. <body>
  21. <ul>
  22. {% for item in items %}
  23. <li>{{ escape(item) }}</li>
  24. {% end %}
  25. </ul>
  26. </body>
  27. </html>
  28. 如果你把这个目标保存为"template.html"并且把它放在你Python文件的
  29. 相同目录下, 你可以使用下面的代码渲染它:
  30. .. testcode::
  31. class MainHandler(tornado.web.RequestHandler):
  32. def get(self):
  33. items = ["Item 1", "Item 2", "Item 3"]
  34. self.render("template.html", title="My title", items=items)
  35. .. testoutput::
  36. :hide:
  37. Tornado模板支持 *控制语句(control statements)* *表达式(expressions)*.
  38. 控制语句被包在 ``{%`` ``%}`` 中间, e.g.,
  39. ``{% if len(items) > 2 %}``. 表达式被包在 ``{{``
  40. ``}}`` 之间, e.g., ``{{ items[0] }}``.
  41. 控制语句或多或少都和Python语句类似. 我们支持 ``if``, ``for``,
  42. ``while``, ``try``, 这些都必须使用 ``{% end %}`` 来标识结束. 我们也
  43. 支持 *模板继承(template inheritance)* 使用 ``extends`` ``block``
  44. 标签声明, 这些内容的详细信息都可以在 `tornado.template` 中看到.
  45. 表达式可以是任意的Python表达式, 包括函数调用. 模板代码会在包含以下对象
  46. 和函数的命名空间中执行 (注意这个列表适用于使用 `.RequestHandler.render`
  47. `~.RequestHandler.render_string` 渲染模板的情况. 如果你直接在
  48. `.RequestHandler` 之外使用 `tornado.template` 模块, 下面这些很多都不存
  49. 在).
  50. - ``escape``: `tornado.escape.xhtml_escape` 的别名
  51. - ``xhtml_escape``: `tornado.escape.xhtml_escape` 的别名
  52. - ``url_escape``: `tornado.escape.url_escape` 的别名
  53. - ``json_encode``: `tornado.escape.json_encode` 的别名
  54. - ``squeeze``: `tornado.escape.squeeze` 的别名
  55. - ``linkify``: `tornado.escape.linkify` 的别名
  56. - ``datetime``: Python `datetime` 模块
  57. - ``handler``: 当前的 `.RequestHandler` 对象
  58. - ``request``: `handler.request <.HTTPServerRequest>` 的别名
  59. - ``current_user``: `handler.current_user
  60. <.RequestHandler.current_user>` 的别名
  61. - ``locale``: `handler.locale <.Locale>` 的别名
  62. - ``_``: `handler.locale.translate <.Locale.translate>` 的别名
  63. - ``static_url``: `handler.static_url <.RequestHandler.static_url>` 的别名
  64. - ``xsrf_form_html``: `handler.xsrf_form_html
  65. <.RequestHandler.xsrf_form_html>` 的别名
  66. - ``reverse_url``: `.Application.reverse_url` 的别名
  67. - 所有从 ``ui_methods`` ``ui_modules``
  68. ``Application`` 设置的条目
  69. - 任何传递给 `~.RequestHandler.render`
  70. `~.RequestHandler.render_string` 的关键字参数
  71. 当你正在构建一个真正的应用, 你可能想要使用Tornado模板的所有特性,
  72. 尤其是目标继承. 阅读所有关于这些特性的介绍在 `tornado.template`
  73. 部分 (一些特性, 包括 ``UIModules`` 是在 `tornado.web` 模块中实现的)
  74. 在引擎下, Tornado模板被直接转换为Python. 包含在你模板中的表达式会
  75. 逐字的复制到一个代表你模板的Python函数中. 我们不会试图阻止模板语言
  76. 中的任何东西; 我们明确的创造一个高度灵活的模板系统, 而不是有严格限制
  77. 的模板系统. 因此, 如果你在模板表达式中随意填充(代码), 当你执行它的时
  78. 候你也会得到各种随机错误.
  79. 所有模板输出默认都会使用 `tornado.escape.xhtml_escape` 函数转义.
  80. 这个行为可以通过传递 ``autoescape=None`` `.Application` 或者
  81. `.tornado.template.Loader` 构造器来全局改变, 对于一个模板文件可以使
  82. ``{% autoescape None %}`` 指令, 对于一个单一表达式可以使用
  83. ``{% raw ...%}`` 来代替 ``{{ ... }}``. 此外, 在每个地方一个可选的
  84. 转义函数名可以被用来代替 ``None``.
  85. 注意, 虽然Tornado的自动转义在预防XSS漏洞上是有帮助的, 但是它并不能
  86. 胜任所有的情况. 在某一位置出现的表达式, 例如Javascript CSS, 可能需
  87. 要另外的转义. 此外, 要么是必须注意总是在可能包含不可信内容的HTML
  88. 使用双引号和 `.xhtml_escape` , 要么必须在属性中使用单独的转义函数
  89. (参见 e.g. http://wonko.com/post/html-escaping)
  90. 国际化

当前用户的区域设置(无论他们是否登录)总是可以通过在请求处理程序中 使用 self.locale 或者在模板中使用 locale 进行访问. 区域的名字 (e.g., en_US) 可以通过 locale.name 获得, 你可以翻译字符串 通过 .Locale.translate 方法. 模板也有一个叫做 _() 全局函数 用来进行字符串翻译. 翻译函数有两种形式::

  1. _("Translate this string")

是直接根据当前的区域设置进行翻译, 还有::

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

是可以根据第三个参数的值来翻译字符串单复数的. 在上面的例子中, 如果 len(people)1, 那么第一句翻译将被返回, 其他情况 第二句的翻译将会返回.

翻译最通用的模式四使用Python命名占位符变量(上面例子中的 %(num)d ) 因为占位符可以在翻译时变化.

这是一个正确的国际化模板::

  1. <html>
  2. <head>
  3. <title>FriendFeed - {{ _("Sign in") }}</title>
  4. </head>
  5. <body>
  6. <form action="{{ request.path }}" method="post">
  7. <div>{{ _("Username") }} <input type="text" name="username"/></div>
  8. <div>{{ _("Password") }} <input type="password" name="password"/></div>
  9. <div><input type="submit" value="{{ _("Sign in") }}"/></div>
  10. {% module xsrf_form_html() %}
  11. </form>
  12. </body>
  13. </html>

默认情况下, 我们通过用户的浏览器发送的 Accept-Language 头来发现 用户的区域设置. 如果我们没有发现恰当的 Accept-Language 值, 我们 会使用 en_US . 如果你让用户进行自己偏爱的区域设置, 你可以通过复 写 .RequestHandler.get_user_locale 来覆盖默认选择的区域:

.. testcode::

  1. class BaseHandler(tornado.web.RequestHandler):
  2. def get_current_user(self):
  3. user_id = self.get_secure_cookie("user")
  4. if not user_id: return None
  5. return self.backend.get_user_by_id(user_id)
  6. def get_user_locale(self):
  7. if "locale" not in self.current_user.prefs:
  8. # Use the Accept-Language header
  9. return None
  10. return self.current_user.prefs["locale"]

.. testoutput:: :hide:

如果 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 .

.. _ui-modules:

UI 模块 ~~

Tornado支持 UI modules 使它易于支持标准, 在你的应用程序中复用 UI组件. UI模块像是特殊的函数调用来渲染你的页面上的组件并且它们可 以包装自己的CSS和JavaScript.

例如, 如果你实现一个博客, 并且你想要有博客入口出现在首页和每篇博 客页, 你可以实现一个 Entry 模块来在这些页面上渲染它们. 首先, 为你的UI模块新建一个Python模块, e.g., uimodules.py::

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

在你的应用设置中, 使用 ui_modules 配置, 告诉Tornado使用 uimodules.py ::

  1. from . import uimodules
  2. class HomeHandler(tornado.web.RequestHandler):
  3. def get(self):
  4. entries = self.db.query("SELECT * FROM entries ORDER BY date DESC")
  5. self.render("home.html", entries=entries)
  6. class EntryHandler(tornado.web.RequestHandler):
  7. def get(self, entry_id):
  8. entry = self.db.get("SELECT * FROM entries WHERE id = %s", entry_id)
  9. if not entry: raise tornado.web.HTTPError(404)
  10. self.render("entry.html", entry=entry)
  11. settings = {
  12. "ui_modules": uimodules,
  13. }
  14. application = tornado.web.Application([
  15. (r"/", HomeHandler),
  16. (r"/entry/([0-9]+)", EntryHandler),
  17. ], **settings)

在一个模板中, 你可以使用 {% module %} 语法调用一个模块. 例如, 你可以调用 Entry 模块从 home.html::

  1. {% for entry in entries %}
  2. {% module Entry(entry) %}
  3. {% end %}

entry.html::

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

模块可以包含自定义的CSS和JavaScript函数, 通过复写 embedded_css, embedded_javascript, javascript_files, 或 css_files 方法::

  1. class Entry(tornado.web.UIModule):
  2. def embedded_css(self):
  3. return ".entry { margin-bottom: 1em; }"
  4. def render(self, entry, show_comments=False):
  5. return self.render_string(
  6. "module-entry.html", show_comments=show_comments)

模块CSS和JavaScript将被加载(或包含)一次, 无论模块在一个页面上被使 用多少次. CSS总是包含在页面的 <head> 标签中, JavaScript 总是被 包含在页面最底部的 </body> 标签之前.

当不需要额外的Python代码时, 一个模板文件本身可以作为一个模块. 例如, 先前的例子可以重写到下面的 module-entry.html::

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

这个被修改过的模块模块可以被引用::

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

set_resources 函数只能在模板中通过 {% module Template(...) %} 才可用. 不像 {% include ... %} 指令, 模板模块有一个明确的命名空间 它们的包含模板-它们只能看到全局模板命名空间和它们自己的关键字参数.