翻译:introduce to tornado - Extending Templates

原创
2013/04/19 01:16
阅读数 2.2K

在第二章中,我们看到了如何使用tornado的template轻松地将数据从handler传送给web页面。让我们在保持简洁的web tag结构的同时,轻松地向web页面插入动态数据,然而大部分网站都希望使用一个高可用的响应模型,将内容按照页眉、页脚和布局框架的形式进行管理。在这一章我们将让你了解到如何通过tornado的template或UI模块完成这种扩展。

块和替换

当你花费了大量的时间为你的web应用创建和制作模板时,你有没有发现,它们似乎只是按照逻辑的形式进行布局,你希望你的前端代码和后端代码一样能够尽可能多的重用对吗?tornado提供了丰富的模板集成和扩展的块语句帮助你完成这一点,tornado可以按照你想要的方式灵活的控制和改变你现有的模板,提高它们的重用性。如果想要扩展现有的模板,你只需要将{% extends ”filename.html“%}放到你的模板中。例如使用你的父模板(main.html)去扩展一个新的模板,你只需要这么做:

  1. {% extends ”main.html” %}  

它将会在新的web页面中继承并使用main.html,然后将main.html的内容插入到你希望显示的地方。有了这个系统,你可以创建一个主模板,嵌入到其他有特殊需求的子模版中,在子模块中你可以使用动态内容或效果快速地扩展你的应用。

基本的块

除非你可以快速的使用和改变tornado中的模板,否则不建议你去改动扩展模板,你可以使用块语句去完成你的改动。

一个块语句可以将一些元素封装到模板中,假如你想要改变它。例如为了实现一个动态的标题,你希望这个效果能覆盖每一个页面,那么你可以把这个效果写到你的父模板main.html中。

Code    View Copy Print
  1. <header>  
  2. {% block header %}{% end %}   
  3. </header>  

然后,去重写子模块中的{% block header%}{% end %}切分的内容。你可以参考下面的方式使用任何内容填充:

Code    View Copy Print
  1. {% extends main.html %}   
  2.   
  3. {% block header %}   
  4.     <h1>Hello world!</h1>  
  5. {% end %}  

所有继承的模板都可以将{% block header %} 和 {% end %}的标签插入到任何地方。你可以通过一个简单的python脚本在web应用中调用一个已命名的子模板,就像这样:

Code    View Copy Print
  1. class MainHandler(tornado.web.RequestHandler):   
  2.     def get(self):   
  3.         self.render(“index.html”)  

例如在这里,index.html会在页面显示一个main.html的内容:“hello world”,你可以在图片3-1中查看到效果。

现在我们可以看到,这是一个非常有用的功能,让我们可以更快地去管理多个页面的整体页面结构,你还可以使用不同的block在同一个页面中,动态的元素如页眉、页脚都可以放在同一个页面中。

下面是一个例子,我们添加了多个blocks到我们父模板中:

Code    View Copy Print
  1. <html>  
  2.     <body>  
  3.     <header>  
  4.         {% block header %}{% end %}   
  5.     </header>  
  6.         <content>  
  7.             {% block body %}{% end %}   
  8.         </content>  
  9.     <footer>  
  10.         {% block footer %}{% end %}   
  11.     </footer>  
  12.     </body>  
  13. </html>  

图3-1 hello world

 

我们可以参考这个子模板index.html的形式去扩展我们的父模板main.html。

Code    View Copy Print
  1. {% extends ”main.html” %}   
  2. {% block header %}   
  3.     <h1>{{ header_text }}</h1>  
  4. {% end %}   
  5. {% block body %}   
  6.     <p>Hello from the child template!</p>  
  7. {% end %}   
  8. {% block footer %}   
  9.     <p>{{ footer_text }}</p>  
  10. {% end %}  

在python脚本和之前看上去的一样,只不过我们现在通过添加一些变量将数据插入到template中,请查看效果图3-2:

Code    View Copy Print
  1. class MainHandler(tornado.web.RequestHandler):   
  2.     def get(self):   
  3.         self.render(   
  4.             ”index.html”,   
  5.             header_text = ”Header goes here”,   
  6.             footer_text = ”Footer goes here”   
  7.         )  

 

图3-2

 

你也可以在父模板中放入一些默认的文本到块语句标识符内。假如扩展的模板没有指定自己的替换块,将会显示默认的文本,这种方式让你可以根据需要灵活地修改其中一些页面的块语句,特别适合导入或替换:JavaScript、CSS文件和标识的块。

图3-3

一个模板的文档标识应该能够帮助我们显示一些错误的语法或异常的关闭:“错误报告位于……”。有一些{% block %}声明的异常关闭或语法错误将会导致返回一个500:内部服务器错误的提示(如果你是在debug模式下运行,会显示一个完整的python堆栈跟踪表)到浏览器上。(请查看图3-3的内容)

总而言之,你自己需要保证模板的健壮性,并尽可能地在错误出现前找到它。

一个模板的练习:Burt’s Books

你是否认为这些听起来非常有趣,但是又想象不出来如何在一个web应用中使用这些特性呢?让我们来看一个例子吧:我们的好朋友Burt经营了一家名为Burt’s Book的书店

Burt通过商店购买了很多书,他现在需要一个网站为来访者展示不同的书籍介绍及更多的东西,Burt希望有一个页面布局上保持一致,但是又可以非常方便地更新页面和内容的网站。

为此,Burt‘s Book使用了Tornado来搭建这个网站,使用一个主模板来定义所有的样式、布局、标题、页眉、页脚等细节,然后用一个非常轻量的子模板处理页面信息,有了这样一个应用系统,Burt就可以在一个页面完成发布图书信息、员工建议、事件安排等更多共享信息的操作。Burt’s Book的网站基于一个主模板main.html来完成网站的整体架构,它看起来是这样的:

Code    View Copy Print
  1. <html>  
  2.     <head>  
  3.         <title>{{ page_title }}</title>  
  4.         <link rel=“stylesheet” href=”{{ static_url(“css/style.css”) }}” />  
  5.     </head>  
  6.     <body>  
  7.         <div id=“container”>  
  8.             <header>  
  9.                 {% block header %}<h1>Burt’s Books</h1>{% end %}   
  10.             </header>  
  11.             <div id=“main”>  
  12.                 <div id=“content”>  
  13.                     {% block body %}{% end %}   
  14.                 </div>  
  15.             </div>  
  16.             <footer>  
  17.                 {% block footer %}   
  18.                     <p>  
  19.         For more information about our selection, hours or events, please email us at   
  20.         <a href=“mailto:contact@burtsbooks.com”>contact@burtsbooks.com</a>.   
  21.                     </p>  
  22.                 {% end %}   
  23.             </footer>  
  24.         </div>  
  25.         <script src=”{{ static_url(“js/script.js”) }}”></script>  
  26. <    /body>  
  27. </html>  

这个页面定义了整个结构,应用了一个CSS样式表,并且加载了主要的JavaScript文件。其它模板可以对这个主模板进行扩展,替换掉其中的页面、页脚、内容。

经过扩展main.html后,我们只需要替换掉页眉、和内容的默认文本就可以实现一个index.html页面,这个网站的首页index.html向web访问的人提供了一些关于商店的信息。

Code    View Copy Print
  1. {% extends ”main.html” %}   
  2. {% block header %}   
  3.     <h1>{{ header_text }}</h1>  
  4. {% end %}   
  5. {% block body %}   
  6.     <div id=“hello”>  
  7.         <p>Welcome to Burt’s Books!<p>  
  8.         <p></p>  
  9.     </div>  
  10. {% end %}  

在这里我们所有的子模板使用tornado模板继承main.html默认的页脚,将所有的信息传送给我们的index.html模板之后,这个Burt’s Book 站点的python脚本 main.py就可以运行了。

Code    View Copy Print
  1. import tornado.web   
  2. import tornado.httpserver   
  3. import tornado.ioloop   
  4. import tornado.options   
  5. import os.path   
  6. from tornado.options import define, options   
  7. define(“port”, default=8000, help=”run on the given port”, type=int)   
  8. class Application(tornado.web.Application):   
  9.     def __init__(self):   
  10.         handlers = [   
  11.             (r"/", MainHandler),   
  12.         ]   
  13.         settings = dict(   
  14.             template_path=os.path.join(os.path.dirname(__file__), ”templates”),   
  15.             static_path=os.path.join(os.path.dirname(__file__), ”static”),   
  16.             debug=True,   
  17.         )   
  18.         tornado.web.Application.__init__(self, handlers, **settings)   
  19.   
  20. class MainHandler(tornado.web.RequestHandler):   
  21.     def get(self):   
  22.         self.render(   
  23.             ”index.html”,   
  24.             page_title = ”Burt’s Books | Home”,   
  25.             header_text = ”Welcome to Burt’s Books!”,   
  26.         )   
  27.   
  28. if __name__ == ”__main__“:   
  29.     tornado.options.parse_command_line()   
  30.     http_server = tornado.httpserver.HTTPServer(Application())   
  31.     http_server.listen(options.port)   
  32.     tornado.ioloop.IOLoop.instance().start()  

这个例子的结构和我们之前见到的似乎有些不同,但是这没什么好怕的,我们不是通过调用tornado.web.application构造函数列表的实例的形式来实现,而是通过和其它参数来定义我们自己的应用类,我们通过一个很简单的方式去初始化和调用我们自己实现的方法,我们创建了一个handlers的列表和一个字典去传递对应的数值,并且使用这些值去调用并初始化我们的父类。像这样

tornado.web.Application.__init__(self, handlers, **settings)

当这个应用系统完成之后,Burt’s Book就可以很轻松地改变索引页面,并且保证main.html模板可以正常地被其它子页面调用。此外他们还可以发挥tornado框架的优势,通过python脚本让网站使用动态的内容或者数据库,我们将在后续的部分看到更多细节的实现。

转义

在默认情况下,tornado将会对HTML模板开启自动转义,将其转换为关联的HTML实体,这有助于防止恶意脚本对网站数据库的攻击,假如你的网站有一个讨论的功能,用户可以添加任何他们喜欢的文章并对此进行讨论。虽然大部分的HTML tag并不能够给网站带来危险,但是一些未转义的<script>标记可以让攻击者加载外部的JavaScript文件,开启一些后门、跨站脚本、XSS漏洞等等。

让我们来思考一下这个例子,Burt’s Book有一个用户反馈的页面, Melvin今天在评论表单中提交了一个恶意攻击的文本:

  1. Totally hacked your site lulz »   
  2. &lt;script&gt;alert(‘RUNNING EVIL H4CKS AND SPL01TS NOW…’)&lt;/script&gt;  

现在当Alice登录这个网站的时候,他的网页将会显示图3-4的内容。

图3-4

 

在tornado1.x中,template没有提供自动转移的功能,所以我们需要讨论一下,如何通过调用escape()方法清除用户的危险输入。

在这里我们可以看到怎么通过转义去保护你的用户不被恶意代码攻击。但是它也同样会拦截你的一些动态的HTML模板和模块。

例如,Burt想要通过模板的变量添加一个邮箱的连接信息在页脚,Burt添加的连接将会被拦截。让我们看看Burt的代码:

Code    View Copy Print
  1. {% set mailLink = ”<a href=\”mailto:contact@burtsbooks.com\”>Contact Us</a>“ %}   
  2. {{ mailLink }}  

这段代码在页面中将会被调整成下面的内容:

Code    View Copy Print
  1. &lt;a href=&quot;mailto:contact@burtsbooks.com&quot;&gt;Contact Us&lt;/a&gt;  

图3-5

这就是转义autoescaping()导致的,很明显,用户将没办法联系到Burt。

为了处理这样的情况,你可以通过设置autoescape = None禁用autoescaping功能,或者像下面这样在每一页修改autoescape的功能。

  1. {% autoescape None %}   
  2. {{ mailLink }}  

这些autoescape 不需要使用{end} tag标记结束,当然我们也可以通过设置xhtml_escape去启用autoescaping(这是默认开启的),或者将其关闭。

实际上,你无论如何都要一直启用autoescaping对网站进行保护,当然也可以在标签中使用{%raw %}去禁用自动转义对模板的渲染。

这里有一个特别重要的事情,当你使用tornado的linkify()和xsrf_form_html()功能时,会影响autoescaping的设置。例如:你想要使用linkify()在页脚中插入一个链接(autoescaping已经启用了),你可以使用{%raw%}来实现关闭autoescaping功能:

Code    View Copy Print
  1. {% block footer %}   
  2.     <p>  
  3.         For more information about our selection, hours or events, please email us at   
  4.         <a href=“mailto:contact@burtsbooks.com”>contact@burtsbooks.com</a>.   
  5.     </p>  
  6.     <p class=“small”>  
  7.         Follow us on Facebook at   
  8.         {% raw linkify(“https://fb.me/burtsbooks”, extra_params=‘ref=website’) %}.   
  9.     </p>  
  10. {% end %}  

这样你就可以快速的在这里使用linkify()功能,但是在其他地方autoescaping仍然起作用

 

UI模块

正如我们看到的,template系统非常轻量级但功能又非常强大,在实际使用中,我们还需要遵从一些软件工程的原则:DRY原则(Don’t Repeat Yourself)。不要在项目中出现重复的代码,我们可以通过template 模块尽可能地去掉冗余代码。例如,我们可以为显示物品清单定义一个列表模板,在每一个需要使用的地方通过render去调用它,此外还可以定义一个导航模板放到共享模板中。tornado的UI模块可以有效的解决这些问题。

UI模块将很多可重用的组件:设计风格、样式、特效放到了一个模板中。这些页面元素通常都被多个模板重用或在一个模板中反复使用。module模块本身就是一个简单的python类,它继承了tornado的UImodule类中定义的render方法。当一个模模板通过{%module Foo(…)%} tag 去引用另一个模板时 ,tornado的template 引擎将会去调用module类中的render方法进行渲染,然后返回一个替换模板的字符串给template。UI模块可以嵌入自己的JavaScript和CSS到渲染的页面中,当然你也可以定义一些可选的embedded_javascript , embedded_css,javascript_file,css_file等文件到页面中。

使用基本模块

想要在你的模板中引入一个模块,你必须要在应用程序中声明它。这个UI 模块将会把maps模块作为参数导入到模板中,请查看例子3-1:

Code    View Copy Print
  1. import tornado.web   
  2. import tornado.httpserver   
  3. import tornado.ioloop   
  4. import tornado.options   
  5. import os.path   
  6. from tornado.options import define, options   
  7. define(“port”, default=8000, help=”run on the given port”, type=int)   
  8.   
  9. class HelloHandler(tornado.web.RequestHandler):   
  10.     def get(self):   
  11.         self.render(‘hello.html’)   
  12.   
  13. class HelloModule(tornado.web.UIModule):   
  14.     def render(self):   
  15.         return ’<h1>Hello, world!</h1>’   
  16.   
  17. if __name__ == ’__main__‘:   
  18.     tornado.options.parse_command_line()   
  19.     app = tornado.web.Application(   
  20.         handlers=[(r'/', HelloHandler)],   
  21.         template_path=os.path.join(os.path.dirname(__file__), ’templates’),   
  22.         ui_modules={‘Hello’, HelloModule}   
  23.     )   
  24.     server = tornado.httpserver.HTTPServer(app)   
  25.     server.listen(options.port)   
  26.     tornado.ioloop.IOLoop.instance().start()  

在这个例子中只有一个项用到了UI_module字典。同时我们在HelloModule类中已经定义了一个名字为Hello的模块,现在当我们调用HelloHandler 时将会显示hello.html,我们可以通过{%module Hello()%} 这个模板标签导入HelloModule类渲染后生成的字符串:

Code    View Copy Print
  1. <html>  
  2.     <head><title>UI Module Example</title></head>  
  3.     <body>  
  4.         {% module Hello() %}   
  5.     </body>  
  6. </html>  

这个hello.html模板将会用调用HelloModule类返回的字符串,去替换掉module标签。这个例子在接下来的部分将会展示如何扩展UI模块,导入JavaScript脚本和样式表来渲染我们的模板。

深入模块

通常,我们会将模板中的module 标签用module类中渲染的字符串替换掉。这些模板将会被我们当成一个整体。

在一个应用程序中,UI模块通常用来对数据库查询或者API查询的结果进行迭代,在一个独立的项目中展现带有相同表示的数据。例如Burt希望能够在网站中创建一个推荐阅读的模块,在下面的代码中我们可以看到,他将会创建一个recommended.html模板,并通过{%module Book(book)%} tag来插入数据:

Code    View Copy Print
  1. {% module Book(book) %} tag.   
  2.     {% extends ”main.html” %}   
  3.   
  4.     {% block body %}   
  5.     <h2>Recommended Reading</h2>   
  6.         {% for book in books %}   
  7.             {% module Book(book) %}   
  8.         {% end %}       
  9.     {% end %}  

Burt将会在book.html模板中创建一个Book模块,它被存放在templates/modules目录中。一个简单的book模板看起来是这样的:

Code    View Copy Print
  1. <div class=“book”>  
  2.     <h3 class=“book_title”>{{ book["title"] }}</h3>  
  3.     <img src=”{{ book["image"] }}” class=“book_image”/>  
  4. </div>  

现在我们定义一个BookModule类,它将会继承UIModule中的render_string方法,这个方法会将模板和它的关键字提取出来渲染然后作为字符串返回给调用者。

Code    View Copy Print
  1. class BookModule(tornado.web.UIModule):   
  2.     def render(self, book):   
  3.         return self.render_string(‘modules/book.html’, book=book)  

在整个完整的示例中,我们将会使用以下模板去格式化有推荐书籍的属性,并替换掉book.html模板中的标签。

Code    View Copy Print
  1. <div class=“book”>  
  2.      <h3 class=“book_title”>{{ book["title"] }}</h3>  
  3.      {% if book["subtitle"] != ”" %}   
  4.           <h4 class=“book_subtitle”>{{ book["subtitle"] }}</h4>  
  5.      {% end %}   
  6.      <img src=”{{ book["image"] }}” class=“book_image”/>  
  7.      <div class=“book_details”>  
  8.           <div class=“book_date_released”>Released: {{ book["date_released"]}}</div>  
  9.           <div class=“book_date_added”>Added: {{ >>  
  10. locale.format_date(book["date_added"], relative=False) }}</div>  
  11.           <h5>Description:</h5>  
  12.           <div class=“book_body”>{% raw book["description"] %}</div>  
  13.      </div>  
  14. </div>  

根据这样的排列,这个模块将会调用每一本书籍的参数并传递给recommended.html模板,每一次调用都会生成一个新的书籍参数,这个模块(还有book.html模块)可以按照恰当的格式引用每一本书籍的参数(请查看效果图3-6)

现在我们可以定义一个RecommendedHandler,它将会按照Book 模块返回的推荐书籍列表来渲染一个模板。

Code    View Copy Print
  1. class RecommendedHandler(tornado.web.RequestHandler):   
  2.     def get(self):   
  3.         self.render(   
  4.             ”recommended.html”,   
  5.             page_title=”Burt’s Books | Recommended Reading”,   
  6.             header_text=”Recommended Reading”,   
  7.             books=[   
  8.                 {   
  9.                     "title":"Programming Collective Intelligence",   
  10.                     "subtitle": "Building Smart Web 2.0 Applications",   
  11.                     "image":"/static/images/collective_intelligence.gif",   
  12.                     "author": "Toby Segaran",   
  13.                     "date_added":1310248056,   
  14.                     "date_released": "August 2007",   
  15.                     "isbn":"978-0-596-52932-1",   
  16.                     "description":"<p>This fascinating book demonstrates how you »   
  17. can build web applications to mine the enormous amount of data created by people »   
  18. on the Internet. With the sophisticated algorithms in this book, you can write »   
  19. smart programs to access interesting datasets from other web sites, collect data »   
  20. from users of your own applications, and analyze and understand the data once »   
  21. you've found it.</p>"   
  22.                 },   
  23.                 ...   
  24.             ]   
  25.         )  

通过添加ui_modules 参数的映射就可以使用附加的模块,因为templates可以调用任何模块中定义的ui_modules映射,轻松地将特殊功能插入到我们的模板中。

图片3-6

在这个例子中,你可能已经注意到了我们使用了locale.format_date()。它调用tornado.locale.module中的datahandling方法。这是一个拥有许多国际化选项的方法format_data(),在默认情况下,它使用GMT的Unix时间戳来显示时间,当然我们也可以通过这样的方式{{locale.format_date(book["data"])}} relative = False的方式获得一个绝对时间(小时和分钟),或者通过full_format=True的方式让它显示一个完整的时间(例如 July 9, 2011 at 9:47pm),还可以通过shorter = True的方式让它只显示月日年。

这个模块可以有效地帮助我们处理时间和日期的格式

嵌入 JavaScript 和CSS

为了在模块中引入更多特性,tornado允许你在模块中嵌入单独的CSS和JavaScript到embedded_css()和embedded_javascript()方法中,例如假如你想要在调用模块时添加一行文字到DOM中,你可以通过嵌入JavaScript到模块中实现这个需求。

Code    View Copy Print
  1. class BookModule(tornado.web.UIModule):   
  2.     def render(self, book):   
  3.         return self.render_string(   
  4.             ”modules/book.html”,   
  5.             book=book,   
  6.         )   
  7.     def embedded_javascript(self):   
  8.         return ”document.write(\”hi!\”)”  

当模块被调用的时候,在靠近<body>标签的地方,将会添加一个document.write(\”hi!\”)到<script> 标签中。

很明显,仅仅添加内容到文档中不是最有效率事情,我们可以在调用的时候根据不同的模块灵活地定制导入不同的JavaScript和CSS:

Code    View Copy Print
  1. def embedded_css(self):   
  2.     return ”.book {background-color:#F5F5F5}”  

在这个例子中,则将会在<head>标签的附件插入<style>标签引入.book{background-color:#555} CSS 规则:

Code    View Copy Print
  1. <style type=“text/css”>  
  2. .book {background-color:#F5F5F5}   
  3. </style>  

如果希望得到更多的特性,你可以使用html_body()在靠近</body>的地方插入更多html标签

Code    View Copy Print
  1. def html_body(self):   
  2.     return ”<script>document.write(\”Hello!\”)</script>”  

很明显,它能够帮助我们在简洁的代码风格下有效地管理复杂的关联文件(样式表、脚本文件),你还可以通过使用javascript_file()和css_files()去导入一些本地或外部的支持文件。例如你可以像这样导入一个独立的CSS文件:

Code    View Copy Print
  1. def css_files(self):   
  2.     return ”/static/css/newreleases.css”  

或者去获取一个外部的JavaScript文件:

Code    View Copy Print
  1. def javascript_files(self):   
  2.     return ”https://ajax.googleapis.com/ajax/libs/jqueryui/1.8.14/jquery-ui.min.js”  

这样可以高效的管理模块额外导入的库。假如你有一个模块需要使用jQuery UI 库(其它模块不需要使用),你可以只在这个模块加载jquery-ui.min.js文件,而其它模块则不需要加载。

因为JavaScript-embedding, HTML-embedding嵌入功能在对</body>插入替换字符串时,html_body(), javascript_files(),embedded_javascript()将会按照倒序的方式插入到页面的底部,所以如果你有一个模块,你应该像这样去指定嵌入元素:

Code    View Copy Print
  1. class SampleModule(tornado.web.UIModule):   
  2.     def render(self, sample):   
  3.         return self.render_string(   
  4.             ”modules/sample.html”,   
  5.             sample=sample   
  6.         )   
  7.   
  8. def html_body(self):   
  9.     return ”<div class=\”addition\”><p>html_body()</p></div>”   
  10.   
  11. def embedded_javascript(self):   
  12.     return ”document.write(\”<p>embedded_javascript()</p>\”)”   
  13.   
  14. def embedded_css(self):   
  15.     return ”.addition {color: #A1CAF1}”   
  16.   
  17. def css_files(self):   
  18.     return ”/static/css/sample.css”   
  19.   
  20. def javascript_files(self):   
  21.     return ”/static/js/sample.js”  

 这个html_body()将会作为</body>之前的元素第一个写入,接下来会由embedded_javascript()去渲染他,最后执行的是javascript_files(),你可以在图3-7中看到它是如何实现的,请注意,你如果在其它地方没有导入这些请求的方法(比如使用JavaScript功能去替换一些文件),那么你的页面可能显示效果会与你期望的不同

总之,tornado允许你灵活地使用规范的格式去渲染模板,也允许你对某一个模块引入外部的样式表或功能规则进行渲染,通过使用module的一些特殊功能,你可以有效的加强代码的可重用性,让你的网站开发更简单更快速。

总结

正如我们看到的,tornado让你可以很轻松的对模板进行扩展,通过添加模块,你可以更精确地操作调用的文件、样式表和脚本。然而目前在我们的例子中经常用到python风格的数据类型,这会给程序带来许多硬编码的数据结构。接下来让我带你去看看如何通过动态的方式去处理数据持久化、存储服务等内容。

原创翻译,首发: http://blog.xihuan.de/tech/web/tornado/tornado_extending_templates.html

上一篇:翻译:introduce to tornado - form and template

下一篇:翻译:introduce to tornado - Databases

展开阅读全文
加载中
点击加入讨论🔥(1) 发布并加入讨论🔥
打赏
1 评论
13 收藏
0
分享
返回顶部
顶部