第三章 模板(Templates)
編寫易于維護的程序的要點在于書寫干凈、良好結構的代碼。你以前所見的代碼都過于簡單無法演示這一點。但Flask試圖函數把兩個完全獨立的愿望混淆成一個,創建一個問題。
視圖函數的任務就是為請求生成響應,就先前面第二章你看到的那樣。對于簡單請求來說足夠了,但通常情況下,一個請求往往會觸發程序狀態的改變,視圖函數也是改變發生地地方。
舉例來說,一個用戶在網站上注冊一個新帳戶。該用戶在表單中輸入一個email地址和密碼,點擊提交按鈕。在服務器端,來自用戶的含有數據的請求到達,Flask將請求分配給以注冊的視圖函數進行處理。這個試圖函數需要與數據庫進行交互:添加一個新帳號,生成操作的響應并發送給瀏覽器。這兩類任務形式上分別被稱為商業邏輯和表現邏輯。
如果將商業邏輯和表現邏輯混合起來,就會導致代碼難以理解和維護。想象一下,你不得不使用HTML標記符號關聯相關從數據庫中取出的巨大的數據集,然后創建表格……把表現邏輯轉移到模板當中,有助于改進程序的可維護性。
一個模板是一個包含響應文本的文件,他通過僅在請求上下文中可見的占位符變量來替換動態部分。用實際值替換占位變量并返回最終響應字符串,這一過程被稱之為渲染(rendering)。Flask采用了被稱之為Jinjia2的模板引擎來完成這一渲染過程。
JInjia2模板引擎
在最簡單的表單中,JInjia2模板就是一個包含了一個相應字符串的文件。3-1示例展示了匹配2-1例子中index()視圖函數的響應的模板
3-1 teplates/index.html:jinjia2 template
<h1>Hello World!</h1>
下面的就是2-2例子中動態返回由變量提供的用戶名的視圖函數,與之對應的模板形式。
3-2. templates/user.html: Jinja2 template
<h1>Hello, {{ name }}!</h1>
渲染模板
Flask默認在應用程序的tempplates子文件夾里查找模板文件。在下面版本的hello.py中,你需要把提前定義好的模板文件index.html和user.html保存在一個新建的templates文件夾中。
如例子3-3所示,程序的視圖函數需要作一些修改,以便于渲染對應的模板。
Example 3-3. hello.py: Rendering a template
from flask import Flask, render_template
# ...
@app.route('/index')
def index():
return render_template('index.html')
@app.route('/user/<name>')
def user(name):
return render_template('user.html', name=name)
函數render_template是由Flask集成的擴展Jinja模板引擎提供的。它以模板的文件名作為自己的第一個參數,附加參數以鍵/值對的形式為在模板文件中引用的變量賦值。在本例中,第二個參數就是模板接收的name變量。
如果第一次使用,上例中的像name=name這樣的鍵值對參數可能難以理解。左側的name提供了參數名,也就是在模板中的占位符。右側的name是當前范圍的變量,它給同名的參數提供值。
變量
在例子3-2中的模板中使用的{{name}}結構引用了一個變量,這個特殊的占位符告訴模板引擎:在渲染模板時,向數據提供者獲取占位處的值。
Jinja2能識別各種數值類型,甚至復雜的如列表、字典和對象類型。下列是在模板中使用變量的示例:
<p>A value from a dictionary: {{ mydict['key'] }}.</p>
<p>A value from a list: {{ mylist[3] }}.</p>
<p>A value from a list, with a variable index: {{ mylist[myintvar] }}.</p>
<p>A value from an object's method: {{ myobj.somemethod() }}.</p>
變量還可以通過filters(過濾器)進行修改,在變量名稱后面添加豎線作為分割符。例如:下列模板代碼展示了對name變量進行首字母大寫:
Hello, {{name|capitalize}}
表格3-1列出了Jinja2中常用過濾器(filters)
Table 3-1. Jinja2 變量過濾器
| 過濾器名 | 描述 |
| safe | 返回未經轉換的值 |
| capitalize | 將值的首個字符大寫其他小寫 |
| lower | 將值轉換為小寫字符 |
| upper | 將值轉換成大寫字符 |
| title | 值得每個單詞都首字母大寫 |
| strim | 從值中去掉頭尾的空白字符 |
| striptags | 在渲染前從值中去除所有的html標記|
safe過濾器是值得注意的一個。出于安全考慮,Jinja2會默認對所有變量進行轉碼(escapes)。例如:如果一個變量的值被設置為'<h1>Hello</h1>'
,Jinja2將會把這個字符串渲染成 '<h1>Hello</h1>'
,這將把h1元素標記顯示成普通字符,而不是被瀏覽器解釋后顯示為html格式。大多數時候,如果需要把html代碼保存在變量里,就需要safe過濾器上場了。
警告
決不要把safe過濾器用在不可信的數據上,例如用戶提交的表單數據。
完整的過濾器列表可以參照Jinja2的官方文檔
控制結構
Jinja2為模板流程控制提供了幾種控制結構。本小節以簡單的例子介紹其中最常用的幾個。以下代碼展示了在模板中如何使用條件控制語句:
{% if user %}
Hello, {{ user }}!
{% else %}
Hello, Stranger!
{% endif %}
另外一個常見需求是渲染列表,這個例子說明了如何使用for循環來實現:
<ul>
{% for comment in comments %}
<li>{{ comment }}</li>
{% endfor %}
</ul>
Jinja2頁支持"宏",你可以使用Python代碼實現類似上例的功能,例如:
{% macro render_comment(comment) %}
<li>{{ comment }}</li>
{% endmacro %}
<ul>
{% for comment in comments %}
{{ render_comment(comment) }}
{% endfor %}
</ul>
為了盡可能的復用宏,你可以把它存儲在獨立文件中,在需要的時候再導入(imported)到模板中。
{% import 'macros.html' as macros %}
<ul>
{% for comment in comments %}
{{ macros.render_comment(comment) }}
{% endfor %}
</ul>
需要在多處重復使用的模板代碼可以部件化后保存到獨立文件中,需要的時候使用再包括進來(included):
{% include 'common.html' %}
另外復用代碼還有一個方法,就是通過模板繼承,這有些像Python代碼中的類繼承。首先,創建一個基礎模板命名為base.html:
<html>
<head>
{% block head %}
<title>{% block title %}{% endblock %} - My Application</title>
{% endblock %}
</head>
<body>
{% block body %}
{% endblock %}
</body>
</html>
此處的block標記定義了一個元素,可供后來派生的模板更改。在這個例子里,還有head,title,body塊;注意title是包含在head中的。下面是一個從基礎模板里派生的模板例子:
{% extends "base.html" %}
{% block title %}Index{% endblock %}
{% block head %}
{{ super() }}
<style>
</style>
{% endblock %}
{% block body %}
<h1>Hello, World!</h1>
{% endblock %}
extends指令聲明本模板從base.html派生而來。接下來在指定位置重新定義了基礎模板中的三個塊。注意,對于head塊的新定義,原來基礎模板已經有這一定義了,用了super()來更改原始內容(父類)。
稍后,在實際使用中,本節的所有控制結構都有展示,你有機會觀察它們是如何工作的。
使用Flask-Bootstrap集成Twitter Bootstrap
Bootstrap是Twitter提供的一個開源框架,通過它提供的用戶接口部件我們可以創建干凈、引人注目的網頁,且被所有現代瀏覽器兼容。
Bootstrap是個客戶端框架,所以在服務器端并不直接涉及它。服務器只是提供一個html響應,該響應包含了bootstrap的層疊樣式表、javascript并通過html,css和js代碼實例化各個部件。完成這一切的的理想之處就是模板。
把Bootstrap集成進程序的最常見做法是在模板里完成所有更改。最簡單的一個方案就是使用Flask擴展——Flask-Bootstrap。你可以通過pip安裝它:
(venv)$pip install flask-bootstrap
Flask擴展實例一般是隨程序實例創建而同時初始化的。示例3-4展示了Flask-Bootstrap的初始化:
from flask.ext.bootstrap import Bootstrap
# ...
bootstrap = Bootstrap(app)
就像在第2章中的Flask-Script擴展一樣,從Flask.ext命名空間中導入flask-bootstrap,然后將程序實例傳遞給它的構造函數從而完成初始化。
一旦初始化完成,程序就可以使用包含所有Bootstrap文件的基礎模板。這個模板利用Jinja2模板的繼承,擴展包含了從導入的bootstrap元素生成的基本頁面結構。例子3-5顯示了新版本的user.html如何從模板派生出來:
Example 3-5. templates/user.html: Template that uses Flask-Bootstrap
{% extends "bootstrap/base.html" %}
{% block title %}Flasky{% endblock %}
{% block navbar %}
<div class="navbar navbar-inverse" role="navigation">
<div class="container">
<div class="navbar-header">
<button type="button" class="navbar-toggle"
data-toggle="collapse" data-target=".navbar-collapse">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="navbar-brand" href="/">Flasky</a>
</div>
<div class="navbar-collapse collapse">
<ul class="nav navbar-nav">
<li><a href="/">Home</a></li>
</ul>
</div>
</div>
</div>
{% endblock %}
{% block content %}
<div class="container">
<div class="page-header">
<h1>Hello, {{ name }}!</h1>
</div>
</div>
{% endblock %}
Jinja2擴展從flask-Bootstrap引用Bootstrap/base.html實現模板繼承。Flask-Bootstrap的基礎模板提供了包含所有bootstrap的css和js文件的網頁骨架。
基礎模板定義了可以被繼承模板子類覆蓋的“塊”。block和endblock指令定義的內容將被添加到基礎模板中。
上面的user.html模板定義了三個塊,title,navar,content。他們有基礎模板定義,并供繼承模板重寫、重定義。title塊中的內容江出現在渲染后的html文檔的title標記中。navbar和content塊 提供了頁面導航和主體部分的內容。
在這個模板中,navbar塊利用bootstrap部件定義了一個簡單的導航欄。content塊包含<div>標記的頁頭部分。老版本的問候語現在放到頁頭(page header)部分了。圖3-1展示了程序外觀的變化。
Flask-Bootstrap的base.html還定義了其他一些可以被派生模板使用的塊,表格3-2是可用塊的完整列表:
塊名 說明
doc 整個HTML文檔
html_attribs <html> 標記的屬性
html <html>標記內的內容
head <head>標記內的內容
title <title>標記內的內容
metas <meta>標記內的列表項
styles 層疊樣式表定義
body_attribs <body>標記的屬性
body <body>標記內的內容
navbar 自定義導航欄
content 自定義頁面內容
scripts 文檔底部的JavaScript 聲明
上表中很多塊是 Flask-bootstrap自己使用的,所以如果直接覆蓋它們將導致報錯。例如:styles和scripts兩個塊聲明了bootstrap文件位置,如果程序需要向已存在的內容中添加新內容,必須使用Jinja2的super()函數。下面的例子顯示了在派生模板中如何重寫scripts塊來添加新的js文件:
{% block scripts %}
{{ super() }}
<script type="text/javascript" src="my-script.js"></script>
{% endblock %}
定制錯誤頁面
當瀏覽器試圖訪問無效地址時,你就會看到一個代碼為404的錯誤頁面目前這個錯誤頁面太干凈了,不吸引人。并且樣式跟其他使用了bootstrap的頁面不一致(根本沒有用上)。
Flask允許程序自己定義基于基礎模板的錯誤頁面,就像常規路由一樣。有兩個最常見的錯誤代碼分別是404和500:404會在客戶端請求一個不存在的頁面或路由是觸發;500錯誤則是出現了未被捕捉處理的錯誤是觸發。例子3-6展示了如何處理這兩個錯誤:
Example 3-6. hello.py: Custom error pages
@app.errorhandler(404)
def page_not_found(e):
return render_template('404.html'), 404
@app.errorhandler(500)
def internal_server_error(e):
return render_template('500.html'), 500
錯誤處理器就像視圖函數一樣返回一個響應。同時也返回了與錯誤代碼一致的數字代碼。
錯誤處理器引用的模板還沒有編寫。這兩個模板應該跟其他常規頁面布局一樣,所以在此例中同樣有導航欄和顯示錯誤信息的頁頭。
你可以直接了當的復制一下templates/user.html
重命名為templates/404.html
和 templates/500.html
。然后修改其中的page header元素以顯示錯誤信息。但這樣還會產生很多重復。
Jinja2的模板繼承能夠解決這一點。同樣,Flask-bootstrap提供了一個帶有基本頁面布局的基礎模板,程序可以定義自己更完成布局的基礎模板,包含導航欄并留下頁面內容供派生類自定義。例子3-7展示了templates/base.html
一個新的繼承自bootstrap/base.html
的帶有導航欄的基礎模板,這樣templates/user.html,templates/404,templates/500.html
都將以它基礎模板來繼承。
Example 3-7. templates/base.html: Base application template with navigation bar
{% extends "bootstrap/base.html" %}
{% block title %}Flasky{% endblock %}
{% block navbar %}
<div class="navbar navbar-inverse" role="navigation">
<div class="container">
<div class="navbar-header">
<button type="button" class="navbar-toggle"
data-toggle="collapse" data-target=".navbar-collapse">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="navbar-brand" href="/">Flasky</a>
</div>
<div class="navbar-collapse collapse">
<ul class="nav navbar-nav">
<li><a href="/">Home</a></li>
</ul>
</div>
</div>
</div>
{% endblock %}
{% block content %}
<div class="container">
{% block page_content %}{% endblock %}
</div>
{% endblock %}
在content塊中,一個div容器包裹了新的空白塊:page_content,它將在派生模板中被定義。
現在程序的模板不再直接繼承Flask-bootstrap的base.html,而是從這個模板繼承。例子3-8展示了從templates/base.html
繼承后很簡單就構建一個自定義的404錯誤頁面:
Example 3-8. templates/404.html: Custom code 404 error page using template
inheritance
{% extends "base.html" %}
{% block title %}Flasky - Page Not Found{% endblock %}
{% block page_content %}
<div class="page-header">
<h1>Not Found</h1>
</div>
{% endblock %}
圖3-2顯示在瀏覽器中的404頁面
templates/user.html
模板現在也可以從這個基礎模板來繼承,簡單修改如下例3-9:
Example 3-9. templates/user.html: Simplified page template using template
inheritance
{% extends "base.html" %}
{% block title %}Flasky{% endblock %}
{% block page_content %}
<div class="page-header">
<h1>Hello, {{ name }}!</h1>
</div>
{% endblock %}
連接
任何一個程序都不止一個路由,所以需要像在導航欄中那樣包含不同連接來在不同頁面間跳轉。
直接在模板中硬編碼URL連接,對簡單路由來說太繁瑣了,而對帶變量參數的動態路由來說,要想寫對更為困難。并且直接寫url會在路由定義代碼中創建一個不希望的依賴。如果對路由進行了更改,模板中的連接就會失效。
為了避免這些問題,Flask提供了一個url_for()輔助函數,可以借此根據url映射中的url信息來生成連接。
最簡單的用法,該函數獲取視圖函數名稱 (或者是app.add_url_route()定義的路由結束點(endpoint)名稱) 作為其唯一參數并返回其url。例如:在當前版本的hello.py中,調用url_for('index')
將返回 /
。調用url_for('index',_external=True)
則將返回一個絕對路徑,那就是http://localhost:5000
提醒
通常在程序不同路由間使用相對URl就可以,而對需要通過瀏覽器進行外部調用連接必須使用絕對URL路徑來生成,如通過email發送的連接。
動態URL可以通過給url_for函數傳遞一個動態鍵值對作為參數來創建。如:url_for('user',name='john',_external=True)
將返回http://localhost:5000/user/john
傳遞給url_for()的鍵值對參數無需局限于動態路由使用的參數。這個函數可以接受任意的擴展參數作為查詢子符串。如:url_for('index',page=2)
將返回/?page=2
。
靜態文件
web應用不僅僅有python代碼和模板構成,大多數程序還要在html中用到一些靜態文件,諸如圖片,js腳本和css樣式表等。
你可以再次調出第二章的hello.py程序的URL映射進行觀察。其中有一個叫做static的條目。這是因為對靜態文件的引用被當作一個特殊的路由定義 /static/<filename>
。如:url_for('static',fielname='css/styles.css',_external=True)
將返回 http://localhost:5000/static/css/styles.css
。
在默認配置中,Flask從程序根文件夾下的名為static的子文件夾中查找靜態文件。如果需要,還可以在此文件夾下再新建文件夾中存放靜態文件。在上例中服務器接受到url后將生成包含static/css/styles.css
文件內容的一個響應。
例子3-10展示了程序在默認模板中向瀏覽器地址欄添加favicon.ico圖標的方法。
Example 3-10. templates/base.html: favicon definition
{% block head %}
{{ super() }}
<link rel="shortcut icon" href="{{ url_for('static', filename = 'favicon.ico') }}"
type="image/x-icon">
<link rel="icon" href="{{ url_for('static', filename = 'favicon.ico') }}"
type="image/x-icon">
{% endblock %}
圖標引用聲明被插入在head塊的末尾。注意,此處使用super()來修改基礎模板中定義的原始內容。
使用Flask-Moment本地化時間和日期
當你的用戶遍及全球時,在web應用中處理日期和時間就不是一件小事情了。
服務器需要把各個獨立不同的地域用戶的時間統一規范,所以一般使用具有代表性的協調世界時(UTC)。但對用戶來說,要是看見的都用UTC格式的時間就會懵圈啦——用戶希望看到以自己所在國家/地域格式化的本地日期和時間。
簡潔的解決方案就是:允許服務器僅以UTC格式工作并發送給瀏覽器,由瀏覽器把他們轉換成本地時間和日期。因為瀏覽器能訪問本地時區和用戶的計算機設置,所以可以很漂亮的完成這個活。
開源的客戶端js腳本庫moment.js就是干這個的。而Flask-Moment擴展可以輕松將它集成到Jinja2模板中。你可以通過pip安裝:
(venv)$pip install flask-moment
例子3-11展示了如和初始化該擴展:
Example 3-11. hello.py: Initialize Flask-Moment
from flask.ext.moment import Moment
moment = Moment(app)
Flask-Moment依賴jquery.js庫(實際是moment.js依賴)。這兩個庫都需要在包含進html文檔中——可以直接引用(你可以自由選擇版本),或者通過擴展的輔助函數(你可以選用通過內容分發網絡<CDN>的測試版,無需下載)。由于Bootstrap已經包含了jquery.js,所以你只需將moment.js添加進來就行了。例子3-12展示了在基礎模板中通過script加載這個庫。
Example 3-12. templates/base.html: Import moment.js library
{% block scripts %}
{{ super() }}
{{ moment.include_moment() }}
{% endblock %}
在模板中你可以訪問moment類,以便于處理時間戳(timestamp)數據。例子3-13中就是傳遞了current_time變量給模板渲染:
Example 3-13. hello.py: Add a datetime variable
from datetime import datetime
@app.route('/')
def index():
return render_template('index.html',
current_time=datetime.utcnow())
例子3-14展示了在模板中渲染 current_time
Example 3-14. templates/index.html: Timestamp rendering with Flask-Moment
<p>The local date and time is {{ moment(current_time).format('LLL') }}.</p>
<p>That was {{ moment(current_time).fromNow(refresh=True) }}</p>
format('LLL')格式根據客戶端計算機的時區和本地設置來格式化接受到的日期和時間。參數決定了渲染格式,從'L'到'LLLL'詳細程度不同(different levels of verbosity?)。format()函數還可以自定義格式化參數。
fromNow()函數渲染結果顯示在第二行,它顯示一個相對化的時間,并根據時間刷新。初始顯示為"剛剛(a few seconds aog)",刷新功能將保持它隨時間更新,所以如果你離開頁面幾分鐘后在回來,你將看到文本就變成了"一會前(a minute ago)",或者 2minutes ago之類的。
Flask_Moment實現了format(),fromNow(),fromTime(),calendar(),valueOf()和unix()這幾個moment.js方法。你可以從moment.js的文檔中學習更多的格式化選項。
提醒
Flask-Moment 假設服務器端處理的時間戳是不包含時區信息的 naive datetime對象。你可以查看標準庫datetime中關于naive和waare兩種日期時間對象的文檔。
通過Flask-Moment可以將時間戳本地化渲染成多種語言格式。可以在模板中將語言代碼傳遞給lang()函數來選擇一種語言:
{{moment.lang('es')}}
通過學習本章設計的知識,你可以創建一個現代化的用戶友好的web頁面了。在下一章我們將深入研究模板:如何通過表單與用戶交互。
<<第二章 基本程序結構 第四章 Web表單>>