物聯網第三步: Tornado-接口與模版

3.3 輸出

1. write(chunk)

將chunk數據寫到輸出緩衝區。如我們在之前的示例代碼中寫的:

class IndexHandler(RequestHandler):
    def get(self):
        self.write("hello itcast!")

想一想,可不可以在同一個處理方法中多次使用write方法?

下面的代碼會出現什麼效果?

class IndexHandler(RequestHandler):
    def get(self):
        self.write("hello itcast 1!")
        self.write("hello itcast 2!")
        self.write("hello itcast 3!")

write方法是寫到緩衝區的,我們可以像寫文件一樣多次使用write方法不斷追加響應內容,最終所有寫到緩衝區的內容一起作爲本次請求的響應輸出。

想一想,如何利用write方法寫json數據?

import json

class IndexHandler(RequestHandler):
    def get(self):
        stu = {
            "name":"zhangsan",
            "age":24,
            "gender":1,
        }
        stu_json = json.dumps(stu)
        self.write(stu_json)

實際上,我們可以不用自己手動去做json序列化,當write方法檢測到我們傳入的chunk參數是字典類型後,會自動幫我們轉換爲json字符串。

class IndexHandler(RequestHandler):
    def get(self):
        stu = {
            "name":"zhangsan",
            "age":24,
            "gender":1,
        }
        self.write(stu)

兩種方式有什麼差異?

對比一下兩種方式的響應頭header中Content-Type字段,自己手動序列化時爲Content-Type:text/html; charset=UTF-8,而採用write方法時爲Content-Type:application/json; charset=UTF-8

write方法除了幫我們將字典轉換爲json字符串之外,還幫我們將Content-Type設置爲application/json; charset=UTF-8

2. set_header(name, value)

利用set_header(name, value)方法,可以手動設置一個名爲name、值爲value的響應頭header字段。

用set_header方法來完成上面write所做的工作。

import json

class IndexHandler(RequestHandler):
    def get(self):
        stu = {
            "name":"zhangsan",
            "age":24,
            "gender":1,
        }
        stu_json = json.dumps(stu)
        self.write(stu_json)
        self.set_header("Content-Type", "application/json; charset=UTF-8")

3. set_default_headers()

該方法會在進入HTTP處理方法前先被調用,可以重寫此方法來預先設置默認的headers。注意:在HTTP處理方法中使用set_header()方法會覆蓋掉在set_default_headers()方法中設置的同名header。

class IndexHandler(RequestHandler):
    def set_default_headers(self):
        print "執行了set_default_headers()"
        # 設置get與post方式的默認響應體格式爲json
        self.set_header("Content-Type", "application/json; charset=UTF-8")
        # 設置一個名爲itcast、值爲python的header
        self.set_header("itcast", "python")

    def get(self):
        print "執行了get()"
        stu = {
            "name":"zhangsan",
            "age":24,
            "gender":1,
        }
        stu_json = json.dumps(stu)
        self.write(stu_json)
        self.set_header("itcast", "i love python") # 注意此處重寫了header中的itcast字段

    def post(self):
        print "執行了post()"
        stu = {
            "name":"zhangsan",
            "age":24,
            "gender":1,
        }
        stu_json = json.dumps(stu)
        self.write(stu_json)

終端中打印出的執行順序:

get請求方式的響應header:

post請求方式的響應header:

4. set_status(status_code, reason=None)

爲響應設置狀態碼。

參數說明:

  • status_code int類型,狀態碼,若reason爲None,則狀態碼必須爲下表中的。
  • reason string類型,描述狀態碼的詞組,若爲None,則會被自動填充爲下表中的內容。
Code Enum Name Details
100 CONTINUE HTTP/1.1 RFC 7231, Section 6.2.1
101 SWITCHING_PROTOCOLS HTTP/1.1 RFC 7231, Section 6.2.2
102 PROCESSING WebDAV RFC 2518, Section 10.1
200 OK HTTP/1.1 RFC 7231, Section 6.3.1
201 CREATED HTTP/1.1 RFC 7231, Section 6.3.2
202 ACCEPTED HTTP/1.1 RFC 7231, Section 6.3.3
203 NON_AUTHORITATIVE_INFORMATION HTTP/1.1 RFC 7231, Section 6.3.4
204 NO_CONTENT HTTP/1.1 RFC 7231, Section 6.3.5
205 RESET_CONTENT HTTP/1.1 RFC 7231, Section 6.3.6
206 PARTIAL_CONTENT HTTP/1.1 RFC 7233, Section 4.1
207 MULTI_STATUS WebDAV RFC 4918, Section 11.1
208 ALREADY_REPORTED WebDAV Binding Extensions RFC 5842, Section 7.1 (Experimental)
226 IM_USED Delta Encoding in HTTP RFC 3229, Section 10.4.1
300 MULTIPLE_CHOICES HTTP/1.1 RFC 7231, Section 6.4.1
301 MOVED_PERMANENTLY HTTP/1.1 RFC 7231, Section 6.4.2
302 FOUND HTTP/1.1 RFC 7231, Section 6.4.3
303 SEE_OTHER HTTP/1.1 RFC 7231, Section 6.4.4
304 NOT_MODIFIED HTTP/1.1 RFC 7232, Section 4.1
305 USE_PROXY HTTP/1.1 RFC 7231, Section 6.4.5
307 TEMPORARY_REDIRECT HTTP/1.1 RFC 7231, Section 6.4.7
308 PERMANENT_REDIRECT Permanent Redirect RFC 7238, Section 3 (Experimental)
400 BAD_REQUEST HTTP/1.1 RFC 7231, Section 6.5.1
401 UNAUTHORIZED HTTP/1.1 Authentication RFC 7235, Section 3.1
402 PAYMENT_REQUIRED HTTP/1.1 RFC 7231, Section 6.5.2
403 FORBIDDEN HTTP/1.1 RFC 7231, Section 6.5.3
404 NOT_FOUND HTTP/1.1 RFC 7231, Section 6.5.4
405 METHOD_NOT_ALLOWED HTTP/1.1 RFC 7231, Section 6.5.5
406 NOT_ACCEPTABLE HTTP/1.1 RFC 7231, Section 6.5.6
407 PROXY_AUTHENTICATION_REQUIRED HTTP/1.1 Authentication RFC 7235, Section 3.2
408 REQUEST_TIMEOUT HTTP/1.1 RFC 7231, Section 6.5.7
409 CONFLICT HTTP/1.1 RFC 7231, Section 6.5.8
410 GONE HTTP/1.1 RFC 7231, Section 6.5.9
411 LENGTH_REQUIRED HTTP/1.1 RFC 7231, Section 6.5.10
412 PRECONDITION_FAILED HTTP/1.1 RFC 7232, Section 4.2
413 REQUEST_ENTITY_TOO_LARGE HTTP/1.1 RFC 7231, Section 6.5.11
414 REQUEST_URI_TOO_LONG HTTP/1.1 RFC 7231, Section 6.5.12
415 UNSUPPORTED_MEDIA_TYPE HTTP/1.1 RFC 7231, Section 6.5.13
416 REQUEST_RANGE_NOT_SATISFIABLE HTTP/1.1 Range Requests RFC 7233, Section 4.4
417 EXPECTATION_FAILED HTTP/1.1 RFC 7231, Section 6.5.14
422 UNPROCESSABLE_ENTITY WebDAV RFC 4918, Section 11.2
423 LOCKED WebDAV RFC 4918, Section 11.3
424 FAILED_DEPENDENCY WebDAV RFC 4918, Section 11.4
426 UPGRADE_REQUIRED HTTP/1.1 RFC 7231, Section 6.5.15
428 PRECONDITION_REQUIRED Additional HTTP Status Codes RFC 6585
429 TOO_MANY_REQUESTS Additional HTTP Status Codes RFC 6585
431 REQUEST_HEADER_FIELDS_TOO_LARGE Additional HTTP Status Codes RFC 6585
500 INTERNAL_SERVER_ERROR HTTP/1.1 RFC 7231, Section 6.6.1
501 NOT_IMPLEMENTED HTTP/1.1 RFC 7231, Section 6.6.2
502 BAD_GATEWAY HTTP/1.1 RFC 7231, Section 6.6.3
503 SERVICE_UNAVAILABLE HTTP/1.1 RFC 7231, Section 6.6.4
504 GATEWAY_TIMEOUT HTTP/1.1 RFC 7231, Section 6.6.5
505 HTTP_VERSION_NOT_SUPPORTED HTTP/1.1 RFC 7231, Section 6.6.6
506 VARIANT_ALSO_NEGOTIATES Transparent Content Negotiation in HTTP RFC 2295, Section 8.1 (Experimental)
507 INSUFFICIENT_STORAGE WebDAV RFC 4918, Section 11.5
508 LOOP_DETECTED WebDAV Binding Extensions RFC 5842, Section 7.2 (Experimental)
510 NOT_EXTENDED An HTTP Extension Framework RFC 2774, Section 7 (Experimental)
511 NETWORK_AUTHENTICATION_REQUIRED Additional HTTP Status Codes RFC 6585, Section 6
class Err404Handler(RequestHandler):
    """對應/err/404"""
    def get(self):
        self.write("hello itcast")
        self.set_status(404) # 標準狀態碼,不用設置reason

class Err210Handler(RequestHandler):
    """對應/err/210"""
    def get(self):
        self.write("hello itcast")
        self.set_status(210, "itcast error") # 非標準狀態碼,設置了reason

class Err211Handler(RequestHandler):
    """對應/err/211"""
    def get(self):
        self.write("hello itcast")
        self.set_status(211) # 非標準狀態碼,未設置reason,錯誤

5. redirect(url)

告知瀏覽器跳轉到url。

class IndexHandler(RequestHandler):
    """對應/"""
    def get(self):
        self.write("主頁")

class LoginHandler(RequestHandler):
    """對應/login"""
    def get(self):
        self.write('<form method="post"><input type="submit" value="登陸"></form>')

    def post(self):
        self.redirect("/")

6. send_error(status_code=500, **kwargs)

拋出HTTP錯誤狀態碼status_code,默認爲500,kwargs爲可變命名參數。使用send_error拋出錯誤後tornado會調用write_error()方法進行處理,並返回給瀏覽器處理後的錯誤頁面。

class IndexHandler(RequestHandler):
    def get(self):
        self.write("主頁")
        self.send_error(404, content="出現404錯誤")

注意:默認的write\_error()方法不會處理send\_error拋出的kwargs參數,即上面的代碼中content="出現404錯誤"是沒有意義的。

嘗試下面的代碼會出現什麼問題?

class IndexHandler(RequestHandler):
    def get(self):
        self.write("主頁")
        self.send_error(404, content="出現404錯誤")
        self.write("結束") # 我們在send_error再次向輸出緩衝區寫內容

注意:使用send_error()方法後就不要再向輸出緩衝區寫內容了!

7. write_error(status_code, **kwargs)

用來處理send_error拋出的錯誤信息並返回給瀏覽器錯誤信息頁面。可以重寫此方法來定製自己的錯誤顯示頁面。

class IndexHandler(RequestHandler):
    def get(self):
        err_code = self.get_argument("code", None) # 注意返回的是unicode字符串,下同
        err_title = self.get_argument("title", "")
        err_content = self.get_argument("content", "")
        if err_code:
            self.send_error(err_code, title=err_title, content=err_content)
        else:
            self.write("主頁")

    def write_error(self, status_code, **kwargs):
        self.write(u"<h1>出錯了,程序員GG正在趕過來!</h1>")
        self.write(u"<p>錯誤名:%s</p>" % kwargs["title"])
        self.write(u"<p>錯誤詳情:%s</p>" % kwargs["content"])

3.4 接口與調用順序

下面的接口方法是由tornado框架進行調用的,我們可以選擇性的重寫這些方法。

1. initialize()

對應每個請求的處理類Handler在構造一個實例後首先執行initialize()方法。在講輸入時提到,路由映射中的第三個字典型參數會作爲該方法的命名參數傳遞,如:

class ProfileHandler(RequestHandler):
    def initialize(self, database):
        self.database = database

    def get(self):
        ...

app = Application([
    (r'/user/(.*)', ProfileHandler, dict(database=database)),
    ])

此方法通常用來初始化參數(對象屬性),很少使用。

2. prepare()

預處理,即在執行對應請求方式的HTTP方法(如get、post等)前先執行,注意:不論以何種HTTP方式請求,都會執行prepare()方法

以預處理請求體中的json數據爲例:

import json

class IndexHandler(RequestHandler):
    def prepare(self):
        if self.request.headers.get("Content-Type").startswith("application/json"):
            self.json_dict = json.loads(self.request.body)
        else:
            self.json_dict = None

    def post(self):
        if self.json_dict:
            for key, value in self.json_dict.items():
                self.write("<h3>%s</h3><p>%s</p>" % (key, value))

    def put(self):
        if self.json_dict:
            for key, value in self.json_dict.items():
                self.write("<h3>%s</h3><p>%s</p>" % (key, value))

用post方式發送json數據時:

用put方式發送json數據時:

3. HTTP方法

方法 描述
get 請求指定的頁面信息,並返回實體主體。
head 類似於get請求,只不過返回的響應中沒有具體的內容,用於獲取報頭
post 向指定資源提交數據進行處理請求(例如提交表單或者上傳文件)。數據被包含在請求體中。POST請求可能會導致新的資源的建立和/或已有資源的修改。
delete 請求服務器刪除指定的內容。
patch 請求修改局部數據。
put 從客戶端向服務器傳送的數據取代指定的文檔的內容。
options 返回給定URL支持的所有HTTP方法。

4. on_finish()

在請求處理結束後調用,即在調用HTTP方法後調用。通常該方法用來進行資源清理釋放或處理日誌等。注意:請儘量不要在此方法中進行響應輸出。

5. set_default_headers()

6. write_error()

7. 調用順序

我們通過一段程序來看上面這些接口的調用順序。

class IndexHandler(RequestHandler):

    def initialize(self):
        print "調用了initialize()"

    def prepare(self):
        print "調用了prepare()"

    def set_default_headers(self):
        print "調用了set_default_headers()"

    def write_error(self, status_code, **kwargs):
        print "調用了write_error()"

    def get(self):
        print "調用了get()"

    def post(self):
        print "調用了post()"
        self.send_error(200)  # 注意此出拋出了錯誤

    def on_finish(self):
        print "調用了on_finish()"

在正常情況未拋出錯誤時,調用順序爲:

  1. set_defautl_headers()
  2. initialize()
  3. prepare()
  4. HTTP方法
  5. on_finish()

在有錯誤拋出時,調用順序爲:

  1. set_default_headers()
  2. initialize()
  3. prepare()
  4. HTTP方法
  5. set_default_headers()
  6. write_error()
  7. on_finish()

3.5 練習

  1. 將Application的設置參數(目前只學習了debug)抽離爲一個字典類型變量settings,並在構造Application對象時使用settings。

  2. 熟練使用RequestHandler的各種輸入輸出方法。

  3. 嘗試抽象出BaseHandler基類,繼承自RequestHandler,並在此基類中實現prepare(解析json數據)、write_error兩個接口。

4 模板

知識點

  • 靜態文件配置
    • static_path
    • StaticFileHandler
  • 模板使用
    • 變量與表達式
    • 控制語句
    • 函數

4.1 靜態文件

現在有一個預先寫好的靜態頁面文件 (下載靜態文件資源), 我們來看下如何用tornado提供靜態文件。

static_path

我們可以通過向web.Application類的構造函數傳遞一個名爲static_path的參數來告訴Tornado從文件系統的一個特定位置提供靜態文件,如:

app = tornado.web.Application(
    [(r'/', IndexHandler)],
    static_path=os.path.join(os.path.dirname(__file__), "statics"),
)

在這裏,我們設置了一個當前應用目錄下名爲statics的子目錄作爲static_path的參數。現在應用將以讀取statics目錄下的filename.ext來響應諸如/static/filename.ext的請求,並在響應的主體中返回。

對於靜態文件目錄的命名,爲了便於部署,建議使用static

對於我們提供的靜態文件資源,可以通過http://127.0.0.1/static/html/index.html來訪問。而且在index.html中引用的靜態資源文件,我們給定的路徑也符合/static/...的格式,故頁面可以正常瀏覽。

<link href="/static/plugins/bootstrap/css/bootstrap.min.css" rel="stylesheet">
<link href="/static/plugins/font-awesome/css/font-awesome.min.css" rel="stylesheet">
<link href="/static/css/reset.css" rel="stylesheet">
<link href="/static/css/main.css" rel="stylesheet">
<link href="/static/css/index.css" rel="stylesheet">

<script src="/static/js/jquery.min.js"></script>
<script src="/static/plugins/bootstrap/js/bootstrap.min.js"></script>
<script src="/static/js/index.js"></script>

StaticFileHandler

我們再看剛剛訪問頁面時使用的路徑http://127.0.0.1/static/html/index.html,這中url顯然對用戶是不友好的,訪問很不方便。我們可以通過tornado.web.StaticFileHandler來自由映射靜態文件與其訪問路徑url。

tornado.web.StaticFileHandler是tornado預置的用來提供靜態資源文件的handler。

import os

current_path = os.path.dirname(__file__)
app = tornado.web.Application(
    [
        (r'^/()$', StaticFileHandler, {"path":os.path.join(current_path, "statics/html"), "default_filename":"index.html"}),
        (r'^/view/(.*)$', StaticFileHandler, {"path":os.path.join(current_path, "statics/html")}),
    ],
    static_path=os.path.join(current_path, "statics"),
)
  • path 用來指明提供靜態文件的根路徑,並在此目錄中尋找在路由中用正則表達式提取的文件名。
  • default_filename 用來指定訪問路由中未指明文件名時,默認提供的文件。

現在,對於靜態文件statics/html/index.html,可以通過三種方式進行訪問:

  1. http://127.0.0.1/static/html/index.html
  2. http://127.0.0.1/
  3. http://127.0.0.1/view/index.html

4.2 使用模板

1. 路徑與渲染

使用模板,需要仿照靜態文件路徑設置一樣,向web.Application類的構造函數傳遞一個名爲template_path的參數來告訴Tornado從文件系統的一個特定位置提供模板文件,如:

app = tornado.web.Application(
    [(r'/', IndexHandler)],
    static_path=os.path.join(os.path.dirname(__file__), "statics"),
    template_path=os.path.join(os.path.dirname(__file__), "templates"),
)

在這裏,我們設置了一個當前應用目錄下名爲templates的子目錄作爲template_path的參數。在handler中使用的模板將在此目錄中尋找。

現在我們將靜態文件目錄statics/html中的index.html複製一份到templates目錄中,此時文件目錄結構爲:

.
├── statics
│   ├── css
│   │   ├── index.css
│   │   ├── main.css
│   │   └── reset.css
│   ├── html
│   │   └── index.html
│   ├── images
│   │   ├── home01.jpg
│   │   ├── home02.jpg
│   │   ├── home03.jpg
│   │   └── landlord01.jpg
│   ├── js
│   │   ├── index.js
│   │   └── jquery.min.js
│   └── plugins
│       ├── bootstrap
│       │   └─...
│       └── font-awesome
│           └─...
├── templates
│   └── index.html
└── test.py

在handler中使用render()方法來渲染模板並返回給客戶端

class IndexHandler(RequestHandler):
    def get(self):
        self.render("index.html") # 渲染主頁模板,並返回給客戶端。



current_path = os.path.dirname(__file__)
app = tornado.web.Application(
    [
        (r'^/$', IndexHandler),
        (r'^/view/(.*)$', StaticFileHandler, {"path":os.path.join(current_path, "statics/html")}),
    ],
    static_path=os.path.join(current_path, "statics"),
    template_path=os.path.join(os.path.dirname(__file__), "templates"),
)

2. 模板語法

2-1 變量與表達式

在tornado的模板中使用{{}}作爲變量或表達式的佔位符,使用render渲染後佔位符{{}}會被替換爲相應的結果值。

我們將index.html中的一條房源信息記錄

<li class="house-item">
    <a href=""><img src="/static/images/home01.jpg"></a>
    <div class="house-desc">
        <div class="landlord-pic"><img src="/static/images/landlord01.jpg"></div>
        <div class="house-price">¥<span>398</span>/晚</div>
        <div class="house-intro">
            <span class="house-title">寬窄巷子+160平大空間+文化保護區雙地鐵</span>
            <em>整套出租 - 5分/6點評 - 北京市豐臺區六裏橋地鐵</em>
        </div>
    </div>
</li>

改爲模板:

<li class="house-item">
    <a href=""><img src="/static/images/home01.jpg"></a>
    <div class="house-desc">
        <div class="landlord-pic"><img src="/static/images/landlord01.jpg"></div>
        <div class="house-price">¥<span>{{price}}</span>/晚</div>
        <div class="house-intro">
            <span class="house-title">{{title}}</span>
            <em>整套出租 - {{score}}分/{{comments}}點評 - {{position}}</em>
        </div>
    </div>
</li>

渲染方式如下:

class IndexHandler(RequestHandler):
    def get(self):
        house_info = {
            "price": 398,
            "title": "寬窄巷子+160平大空間+文化保護區雙地鐵",
            "score": 5,
            "comments": 6,
            "position": "北京市豐臺區六裏橋地鐵"
        }
        self.render("index.html", **house_info)

{{}}不僅可以包含變量,還可以是表達式,如:

<li class="house-item">
    <a href=""><img src="/static/images/home01.jpg"></a>
    <div class="house-desc">
        <div class="landlord-pic"><img src="/static/images/landlord01.jpg"></div>
        <div class="house-price">¥<span>{{p1 + p2}}</span>/晚</div>
        <div class="house-intro">
            <span class="house-title">{{"+".join(titles)}}</span>
            <em>整套出租 - {{score}}分/{{comments}}點評 - {{position}}</em>
        </div>
    </div>
</li>
class IndexHandler(RequestHandler):
    def get(self):
        house_info = {
            "p1": 198,
            "p2": 200,
            "titles": ["寬窄巷子", "160平大空間", "文化保護區雙地鐵"],
            "score": 5,
            "comments": 6,
            "position": "北京市豐臺區六裏橋地鐵"
        }
        self.render("index.html", **house_info)

2-2 控制語句

可以在Tornado模板中使用Python條件和循環語句。控制語句以{\%和\%}包圍,並以類似下面的形式被使用:

{% if page is None %}

{% if len(entries) == 3 %}

控制語句的大部分就像對應的Python語句一樣工作,支持if、for、while,注意end:

{% if ... %} ... {% elif ... %} ... {% else ... %} ... {% end %}
{% for ... in ... %} ... {% end %}
{% while ... %} ... {% end %}

再次修改index.html:

<ul class="house-list">
    {% if len(houses) > 0 %}
        {% for house in houses %}
        <li class="house-item">
            <a href=""><img src="/static/images/home01.jpg"></a>
            <div class="house-desc">
                <div class="landlord-pic"><img src="/static/images/landlord01.jpg"></div>
                <div class="house-price">¥<span>{{house["price"]}}</span>/晚</div>
                <div class="house-intro">
                    <span class="house-title">{{house["title"]}}</span>
                    <em>整套出租 - {{house["score"]}}分/{{house["comments"]}}點評 - {{house["position"]}}</em>
                </div>
            </div>
        </li>
        {% end %}
    {% else %}
        對不起,暫時沒有房源。
    {% end %}
</ul>

python中渲染語句爲:

class IndexHandler(RequestHandler):
    def get(self):
        houses = [
        {
            "price": 398,
            "title": "寬窄巷子+160平大空間+文化保護區雙地鐵",
            "score": 5,
            "comments": 6,
            "position": "北京市豐臺區六裏橋地鐵"
        },
        {
            "price": 398,
            "title": "寬窄巷子+160平大空間+文化保護區雙地鐵",
            "score": 5,
            "comments": 6,
            "position": "北京市豐臺區六裏橋地鐵"
        },
        {
            "price": 398,
            "title": "寬窄巷子+160平大空間+文化保護區雙地鐵",
            "score": 5,
            "comments": 6,
            "position": "北京市豐臺區六裏橋地鐵"
        },
        {
            "price": 398,
            "title": "寬窄巷子+160平大空間+文化保護區雙地鐵",
            "score": 5,
            "comments": 6,
            "position": "北京市豐臺區六裏橋地鐵"
        },
        {
            "price": 398,
            "title": "寬窄巷子+160平大空間+文化保護區雙地鐵",
            "score": 5,
            "comments": 6,
            "position": "北京市豐臺區六裏橋地鐵"
        }]
        self.render("index.html", houses=houses)

2-3 函數

static_url()

Tornado模板模塊提供了一個叫作static_url的函數來生成靜態文件目錄下文件的URL。如下面的示例代碼:

<link rel="stylesheet" href="{{ static_url("style.css") }}">

這個對static_url的調用生成了URL的值,並渲染輸出類似下面的代碼:

<link rel="stylesheet" href="/static/style.css?v=ab12">

優點:

  • static_url函數創建了一個基於文件內容的hash值,並將其添加到URL末尾(查詢字符串的參數v)。這個hash值確保瀏覽器總是加載一個文件的最新版而不是之前的緩存版本。無論是在你應用的開發階段,還是在部署到生產環境使用時,都非常有用,因爲你的用戶不必再爲了看到你的靜態內容而清除瀏覽器緩存了。
  • 另一個好處是你可以改變你應用URL的結構,而不需要改變模板中的代碼。例如,可以通過設置static_url_prefix來更改Tornado的默認靜態路徑前綴/static。如果使用static_url而不是硬編碼的話,代碼不需要改變。

轉義

我們新建一個表單頁面new.html

<!DOCTYPE html>
<html>
    <head>
        <title>新建房源</title>
    </head>
    <body>
        <form method="post">
            <textarea name="text"></textarea>
            <input type="submit" value="提交">
        </form>
        {{text}}
    </body>
</html>

對應的handler爲:

class NewHandler(RequestHandler):

    def get(self):
        self.render("new.html", text="")

    def post(self):
        text = self.get_argument("text", "") 
        print text
        self.render("new.html", text=text)

當我們在表單中填入如下內容時:

<script>alert("hello!");</script>

 

寫入的js程序並沒有運行,而是顯示出來了:

我們查看頁面源代碼,發現<、>、"等被轉換爲對應的html字符。

&lt;script&gt;alert(&quot;hello!&quot;);&lt;/script&gt;

這是因爲tornado中默認開啓了模板自動轉義功能,防止網站受到惡意攻擊。

我們可以通過raw語句來輸出不被轉義的原始格式,如:

{% raw text %}

注意:在Firefox瀏覽器中會直接彈出alert窗口,而在Chrome瀏覽器中,需要set_header("X-XSS-Protection", 0)

若要關閉自動轉義,一種方法是在Application構造函數中傳遞autoescape=None,另一種方法是在每頁模板中修改自動轉義行爲,添加如下語句:

{% autoescape None %}

escape()

關閉自動轉義後,可以使用escape()函數來對特定變量進行轉義,如:

{{ escape(text) }}

自定義函數

在模板中還可以使用一個自己編寫的函數,只需要將函數名作爲模板的參數傳遞即可,就像其他變量一樣。

我們修改後端如下:

def house_title_join(titles):
    return "+".join(titles)

class IndexHandler(RequestHandler):
    def get(self):
        house_list = [
        {
            "price": 398,
            "titles": ["寬窄巷子", "160平大空間", "文化保護區雙地鐵"],
            "score": 5,
            "comments": 6,
            "position": "北京市豐臺區六裏橋地鐵"
        },
        {
            "price": 398,
            "titles": ["寬窄巷子", "160平大空間", "文化保護區雙地鐵"],
            "score": 5,
            "comments": 6,
            "position": "北京市豐臺區六裏橋地鐵"
        }]
        self.render("index.html", houses=house_list, title_join = house_title_join)

前段模板我們修改爲:

<ul class="house-list">
    {% if len(houses) > 0 %}
        {% for house in houses %}
        <li class="house-item">
            <a href=""><img src="/static/images/home01.jpg"></a>
            <div class="house-desc">
                <div class="landlord-pic"><img src="/static/images/landlord01.jpg"></div>
                <div class="house-price">¥<span>{{house["price"]}}</span>/晚</div>
                <div class="house-intro">
                    <span class="house-title">{{title_join(house["titles"])}}</span>
                    <em>整套出租 - {{house["score"]}}分/{{house["comments"]}}點評 - {{house["position"]}}</em>
                </div>
            </div>
        </li>
        {% end %}
    {% else %}
        對不起,暫時沒有房源。
    {% end %}
</ul>

2-4 塊

我們可以使用塊來複用模板,塊語法如下:

{% block block_name %} {% end %}

現在,我們對模板index.html進行抽象,抽離出父模板base.html如下:

<!DOCTYPE html>
<html>
<head> 
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
    {% block page_title %}{% end %}
    <link href="{{static_url('plugins/bootstrap/css/bootstrap.min.css')}}" rel="stylesheet">
    <link href="{{static_url('plugins/font-awesome/css/font-awesome.min.css')}}" rel="stylesheet">
    <link href="{{static_url('css/reset.css')}}" rel="stylesheet">
    <link href="{{static_url('css/main.css')}}" rel="stylesheet">
    {% block css_files %}{% end %}
</head>
<body>
    <div class="container">
        <div class="top-bar">
            {% block header %}{% end %}
        </div>
        {% block body %}{% end %}
        <div class="footer">
            {% block footer %}{% end %}
        </div>
    </div>

    <script src="{{static_url('js/jquery.min.js')}}"></script>
    <script src="{{static_url('plugins/bootstrap/js/bootstrap.min.js')}}"></script>
    {% block js_files %}{% end %}
</body>
</html>

而子模板index.html使用extends來使用父模板base.html,如下:

{% extends "base.html" %}

{% block page_title %}
    <title>愛家-房源</title>
{% end %}

{% block css_files %}
    <link href="{{static_url('css/index.css')}}" rel="stylesheet">
{% end %} 

{% block js_files %}
    <script src="{{static_url('js/index.js')}}"></script>
{% end %}

{% block header %}
    <div class="nav-bar">
        <h3 class="page-title">房 源</h3>
    </div>
{% end %}

{% block body %}
    <ul class="house-list">
    {% if len(houses) > 0 %}
        {% for house in houses %}
        <li class="house-item">
            <a href=""><img src="/static/images/home01.jpg"></a>
            <div class="house-desc">
                <div class="landlord-pic"><img src="/static/images/landlord01.jpg"></div>
                <div class="house-price">¥<span>{{house["price"]}}</span>/晚</div>
                <div class="house-intro">
                    <span class="house-title">{{title_join(house["titles"])}}</span>
                    <em>整套出租 - {{house["score"]}}分/{{house["comments"]}}點評 - {{house["position"]}}</em>
                </div>
            </div>
        </li>
        {% end %}
    {% else %}
        對不起,暫時沒有房源。
    {% end %}
    </ul>
{% end %}

{% block footer %}
    <p><span><i class="fa fa-copyright"></i></span>愛家租房&nbsp;&nbsp;享受家的溫馨</p>
{% end %}

4.3 練習

  1. 對比Django模板與Tornado模板的異同。

  2. 練習使用Tornado模板的語法。

4.2 使用模板

1. 路徑與渲染

使用模板,需要仿照靜態文件路徑設置一樣,向web.Application類的構造函數傳遞一個名爲template_path的參數來告訴Tornado從文件系統的一個特定位置提供模板文件,如:

app = tornado.web.Application(
    [(r'/', IndexHandler)],
    static_path=os.path.join(os.path.dirname(__file__), "statics"),
    template_path=os.path.join(os.path.dirname(__file__), "templates"),
)

在這裏,我們設置了一個當前應用目錄下名爲templates的子目錄作爲template_path的參數。在handler中使用的模板將在此目錄中尋找。

現在我們將靜態文件目錄statics/html中的index.html複製一份到templates目錄中,此時文件目錄結構爲:

.
├── statics
│   ├── css
│   │   ├── index.css
│   │   ├── main.css
│   │   └── reset.css
│   ├── html
│   │   └── index.html
│   ├── images
│   │   ├── home01.jpg
│   │   ├── home02.jpg
│   │   ├── home03.jpg
│   │   └── landlord01.jpg
│   ├── js
│   │   ├── index.js
│   │   └── jquery.min.js
│   └── plugins
│       ├── bootstrap
│       │   └─...
│       └── font-awesome
│           └─...
├── templates
│   └── index.html
└── test.py

在handler中使用render()方法來渲染模板並返回給客戶端

class IndexHandler(RequestHandler):
    def get(self):
        self.render("index.html") # 渲染主頁模板,並返回給客戶端。



current_path = os.path.dirname(__file__)
app = tornado.web.Application(
    [
        (r'^/$', IndexHandler),
        (r'^/view/(.*)$', StaticFileHandler, {"path":os.path.join(current_path, "statics/html")}),
    ],
    static_path=os.path.join(current_path, "statics"),
    template_path=os.path.join(os.path.dirname(__file__), "templates"),
)

2. 模板語法

2-1 變量與表達式

在tornado的模板中使用{{}}作爲變量或表達式的佔位符,使用render渲染後佔位符{{}}會被替換爲相應的結果值。

我們將index.html中的一條房源信息記錄

<li class="house-item">
    <a href=""><img src="/static/images/home01.jpg"></a>
    <div class="house-desc">
        <div class="landlord-pic"><img src="/static/images/landlord01.jpg"></div>
        <div class="house-price">¥<span>398</span>/晚</div>
        <div class="house-intro">
            <span class="house-title">寬窄巷子+160平大空間+文化保護區雙地鐵</span>
            <em>整套出租 - 5分/6點評 - 北京市豐臺區六裏橋地鐵</em>
        </div>
    </div>
</li>

改爲模板:

<li class="house-item">
    <a href=""><img src="/static/images/home01.jpg"></a>
    <div class="house-desc">
        <div class="landlord-pic"><img src="/static/images/landlord01.jpg"></div>
        <div class="house-price">¥<span>{{price}}</span>/晚</div>
        <div class="house-intro">
            <span class="house-title">{{title}}</span>
            <em>整套出租 - {{score}}分/{{comments}}點評 - {{position}}</em>
        </div>
    </div>
</li>

渲染方式如下:

class IndexHandler(RequestHandler):
    def get(self):
        house_info = {
            "price": 398,
            "title": "寬窄巷子+160平大空間+文化保護區雙地鐵",
            "score": 5,
            "comments": 6,
            "position": "北京市豐臺區六裏橋地鐵"
        }
        self.render("index.html", **house_info)

{{}}不僅可以包含變量,還可以是表達式,如:

<li class="house-item">
    <a href=""><img src="/static/images/home01.jpg"></a>
    <div class="house-desc">
        <div class="landlord-pic"><img src="/static/images/landlord01.jpg"></div>
        <div class="house-price">¥<span>{{p1 + p2}}</span>/晚</div>
        <div class="house-intro">
            <span class="house-title">{{"+".join(titles)}}</span>
            <em>整套出租 - {{score}}分/{{comments}}點評 - {{position}}</em>
        </div>
    </div>
</li>
class IndexHandler(RequestHandler):
    def get(self):
        house_info = {
            "p1": 198,
            "p2": 200,
            "titles": ["寬窄巷子", "160平大空間", "文化保護區雙地鐵"],
            "score": 5,
            "comments": 6,
            "position": "北京市豐臺區六裏橋地鐵"
        }
        self.render("index.html", **house_info)

2-2 控制語句

可以在Tornado模板中使用Python條件和循環語句。控制語句以{\%和\%}包圍,並以類似下面的形式被使用:

{% if page is None %}

{% if len(entries) == 3 %}

控制語句的大部分就像對應的Python語句一樣工作,支持if、for、while,注意end:

{% if ... %} ... {% elif ... %} ... {% else ... %} ... {% end %}
{% for ... in ... %} ... {% end %}
{% while ... %} ... {% end %}

再次修改index.html:

<ul class="house-list">
    {% if len(houses) > 0 %}
        {% for house in houses %}
        <li class="house-item">
            <a href=""><img src="/static/images/home01.jpg"></a>
            <div class="house-desc">
                <div class="landlord-pic"><img src="/static/images/landlord01.jpg"></div>
                <div class="house-price">¥<span>{{house["price"]}}</span>/晚</div>
                <div class="house-intro">
                    <span class="house-title">{{house["title"]}}</span>
                    <em>整套出租 - {{house["score"]}}分/{{house["comments"]}}點評 - {{house["position"]}}</em>
                </div>
            </div>
        </li>
        {% end %}
    {% else %}
        對不起,暫時沒有房源。
    {% end %}
</ul>

python中渲染語句爲:

class IndexHandler(RequestHandler):
    def get(self):
        houses = [
        {
            "price": 398,
            "title": "寬窄巷子+160平大空間+文化保護區雙地鐵",
            "score": 5,
            "comments": 6,
            "position": "北京市豐臺區六裏橋地鐵"
        },
        {
            "price": 398,
            "title": "寬窄巷子+160平大空間+文化保護區雙地鐵",
            "score": 5,
            "comments": 6,
            "position": "北京市豐臺區六裏橋地鐵"
        },
        {
            "price": 398,
            "title": "寬窄巷子+160平大空間+文化保護區雙地鐵",
            "score": 5,
            "comments": 6,
            "position": "北京市豐臺區六裏橋地鐵"
        },
        {
            "price": 398,
            "title": "寬窄巷子+160平大空間+文化保護區雙地鐵",
            "score": 5,
            "comments": 6,
            "position": "北京市豐臺區六裏橋地鐵"
        },
        {
            "price": 398,
            "title": "寬窄巷子+160平大空間+文化保護區雙地鐵",
            "score": 5,
            "comments": 6,
            "position": "北京市豐臺區六裏橋地鐵"
        }]
        self.render("index.html", houses=houses)

2-3 函數

static_url()

Tornado模板模塊提供了一個叫作static_url的函數來生成靜態文件目錄下文件的URL。如下面的示例代碼:

<link rel="stylesheet" href="{{ static_url("style.css") }}">

這個對static_url的調用生成了URL的值,並渲染輸出類似下面的代碼:

<link rel="stylesheet" href="/static/style.css?v=ab12">

優點:

  • static_url函數創建了一個基於文件內容的hash值,並將其添加到URL末尾(查詢字符串的參數v)。這個hash值確保瀏覽器總是加載一個文件的最新版而不是之前的緩存版本。無論是在你應用的開發階段,還是在部署到生產環境使用時,都非常有用,因爲你的用戶不必再爲了看到你的靜態內容而清除瀏覽器緩存了。
  • 另一個好處是你可以改變你應用URL的結構,而不需要改變模板中的代碼。例如,可以通過設置static_url_prefix來更改Tornado的默認靜態路徑前綴/static。如果使用static_url而不是硬編碼的話,代碼不需要改變。

轉義

我們新建一個表單頁面new.html

<!DOCTYPE html>
<html>
    <head>
        <title>新建房源</title>
    </head>
    <body>
        <form method="post">
            <textarea name="text"></textarea>
            <input type="submit" value="提交">
        </form>
        {{text}}
    </body>
</html>

對應的handler爲:

class NewHandler(RequestHandler):

    def get(self):
        self.render("new.html", text="")

    def post(self):
        text = self.get_argument("text", "") 
        print text
        self.render("new.html", text=text)

當我們在表單中填入如下內容時:

<script>alert("hello!");</script>

寫入的js程序並沒有運行,而是顯示出來了:

我們查看頁面源代碼,發現<、>、"等被轉換爲對應的html字符。

&lt;script&gt;alert(&quot;hello!&quot;);&lt;/script&gt;

這是因爲tornado中默認開啓了模板自動轉義功能,防止網站受到惡意攻擊。

我們可以通過raw語句來輸出不被轉義的原始格式,如:

{% raw text %}

注意:在Firefox瀏覽器中會直接彈出alert窗口,而在Chrome瀏覽器中,需要set_header("X-XSS-Protection", 0)

若要關閉自動轉義,一種方法是在Application構造函數中傳遞autoescape=None,另一種方法是在每頁模板中修改自動轉義行爲,添加如下語句:

{% autoescape None %}

escape()

關閉自動轉義後,可以使用escape()函數來對特定變量進行轉義,如:

{{ escape(text) }}

自定義函數

在模板中還可以使用一個自己編寫的函數,只需要將函數名作爲模板的參數傳遞即可,就像其他變量一樣。

我們修改後端如下:

def house_title_join(titles):
    return "+".join(titles)

class IndexHandler(RequestHandler):
    def get(self):
        house_list = [
        {
            "price": 398,
            "titles": ["寬窄巷子", "160平大空間", "文化保護區雙地鐵"],
            "score": 5,
            "comments": 6,
            "position": "北京市豐臺區六裏橋地鐵"
        },
        {
            "price": 398,
            "titles": ["寬窄巷子", "160平大空間", "文化保護區雙地鐵"],
            "score": 5,
            "comments": 6,
            "position": "北京市豐臺區六裏橋地鐵"
        }]
        self.render("index.html", houses=house_list, title_join = house_title_join)

前段模板我們修改爲:

<ul class="house-list">
    {% if len(houses) > 0 %}
        {% for house in houses %}
        <li class="house-item">
            <a href=""><img src="/static/images/home01.jpg"></a>
            <div class="house-desc">
                <div class="landlord-pic"><img src="/static/images/landlord01.jpg"></div>
                <div class="house-price">¥<span>{{house["price"]}}</span>/晚</div>
                <div class="house-intro">
                    <span class="house-title">{{title_join(house["titles"])}}</span>
                    <em>整套出租 - {{house["score"]}}分/{{house["comments"]}}點評 - {{house["position"]}}</em>
                </div>
            </div>
        </li>
        {% end %}
    {% else %}
        對不起,暫時沒有房源。
    {% end %}
</ul>

2-4 塊

我們可以使用塊來複用模板,塊語法如下:

{% block block_name %} {% end %}

現在,我們對模板index.html進行抽象,抽離出父模板base.html如下:

<!DOCTYPE html>
<html>
<head> 
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
    {% block page_title %}{% end %}
    <link href="{{static_url('plugins/bootstrap/css/bootstrap.min.css')}}" rel="stylesheet">
    <link href="{{static_url('plugins/font-awesome/css/font-awesome.min.css')}}" rel="stylesheet">
    <link href="{{static_url('css/reset.css')}}" rel="stylesheet">
    <link href="{{static_url('css/main.css')}}" rel="stylesheet">
    {% block css_files %}{% end %}
</head>
<body>
    <div class="container">
        <div class="top-bar">
            {% block header %}{% end %}
        </div>
        {% block body %}{% end %}
        <div class="footer">
            {% block footer %}{% end %}
        </div>
    </div>

    <script src="{{static_url('js/jquery.min.js')}}"></script>
    <script src="{{static_url('plugins/bootstrap/js/bootstrap.min.js')}}"></script>
    {% block js_files %}{% end %}
</body>
</html>

而子模板index.html使用extends來使用父模板base.html,如下:

{% extends "base.html" %}

{% block page_title %}
    <title>愛家-房源</title>
{% end %}

{% block css_files %}
    <link href="{{static_url('css/index.css')}}" rel="stylesheet">
{% end %} 

{% block js_files %}
    <script src="{{static_url('js/index.js')}}"></script>
{% end %}

{% block header %}
    <div class="nav-bar">
        <h3 class="page-title">房 源</h3>
    </div>
{% end %}

{% block body %}
    <ul class="house-list">
    {% if len(houses) > 0 %}
        {% for house in houses %}
        <li class="house-item">
            <a href=""><img src="/static/images/home01.jpg"></a>
            <div class="house-desc">
                <div class="landlord-pic"><img src="/static/images/landlord01.jpg"></div>
                <div class="house-price">¥<span>{{house["price"]}}</span>/晚</div>
                <div class="house-intro">
                    <span class="house-title">{{title_join(house["titles"])}}</span>
                    <em>整套出租 - {{house["score"]}}分/{{house["comments"]}}點評 - {{house["position"]}}</em>
                </div>
            </div>
        </li>
        {% end %}
    {% else %}
        對不起,暫時沒有房源。
    {% end %}
    </ul>
{% end %}

{% block footer %}
    <p><span><i class="fa fa-copyright"></i></span>愛家租房&nbsp;&nbsp;享受家的溫馨</p>
{% end %}

4.3 練習

  1. 對比Django模板與Tornado模板的異同。

  2. 練習使用Tornado模板的語法

前端模板 騰訊模板https://github.com/aui/art-template

 

 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章