要想開發出易於維護的程序,關鍵在於編寫形式簡潔且結構良好的代碼。
當目前為止,你看到的示例都太簡單,無法說明這一點,但Flask視圖函數的兩個完全獨立的作用卻被融合在了一起,這就產生了一個問題。
視圖函數的作用很明確,即生成請求的響應。
如第2章中的示例,對簡單的請求來所,這就足夠了。
但一般而言,請求會改變程序的狀態,而這種變化也會在視圖函數中產生。
例如,用戶在網站中注冊一個一個新賬戶。
用戶在表單中輸入電子郵箱地址和密碼,然后點擊提交按鈕。
服務器接收到包含用戶輸入數據的請求,然后Flask把請求分發到處理注冊請求的視圖函數。
這個視圖函數需要訪問數據庫,添加新用戶,然后生成響應回送瀏覽器。
這兩個過程分別稱為業務邏輯和表現邏輯。
把業務邏輯和表現邏輯混在一起會導致代碼難以理解和維護。假設要為一個大型表格構建HTML代碼,表格中的數據由數據庫中讀取的數據以及必要的HTML字符連接在一起。把表現邏輯移到模板中能提升程序的可維護性。
模板是一個包含相應文本的文件,其中包含用占位變來那個表示的動態部分,其具體值只在請求的上下文中才能知道。
使用真實值替換變量,在返回最終的響應字符串,這一過程稱為渲染。
為了渲染模板,Flask使用了一個名為Jinja2的強大模板引擎。
3.1 Jinja2模板引擎
形式最簡單的Jinja2模板就是一個包含相應文本的文件。
示例 3-1 是一個Jinjia2模板,它和示例 2-1 中的index() 視圖函數的響應一樣。
示例 3-1 templates/index.html :Jinjia2模板
<h1>Hello World!</h1>
示例 2-2 中,視圖函數user() 返回的響應中包含了一個使用變量表示的動態部分。示例 3-2 實現了這個響應。
示例 3-2 template/user.html :Jinjia2模板
<h1>Hello,{{ name }}</h1>
3.1.1 渲染模板
默認情況下,Flask在程序文件夾中的templates子文件夾中尋找模板。
在下一個hello.py版本中,要把前面定義的模板保存在templates文件夾中,並分別命名為 index.html 和 user.html。
程序中的視圖函數需要修改一下,以便渲染這些模板。修改方法參見示例 3-3
示例3-3 hello.py :渲染模板
from flask import Flask,render_template # ... @app.route("/") def index(): return render_template('index.html') @app.route("/user/<name>") def user(name): return render_template('user.html',name=name)

from flask import Flask,render_template from flask import request from flask_script import Manager app = Flask(__name__) manager = Manager(app) @app.route("/") #路由 def index(): #視圖函數 return render_template('index.html') @app.route("/user/<name>") #動態路由 def user(name): return render_template('user.html',name=name) if __name__ == "__main__": manager.run()
Flask提供的render_template 函數把Jinjia2模板引擎集成到了程序中。
render_template 函數的一個參數是模板的文件名。
隨后的參數都是鍵值對,表示模板中變量對應的真實值。
在這段代碼中,第二個模板收到一個名為name的變量。
前例中的 name=name 是關鍵字參數,這類關鍵字參數很常見,但如果你不熟悉它們的話,可能會覺得迷惑且難以理解。左邊的“name” 表示參數名,就是模板中使用的占位符;右邊的“name”是當前作用域中的變量,表示同名參數的值。
3.1.2 變量
示例 3-2 在模板中使用 {{ name }} 結構便是一個變量,它是一種特殊的占位符,告訴模板引擎這個位置的值從渲染模板時使用的數據中獲取。
Jinjia2能是被所有類型的變量,設置是一些復雜的類型,例如列表、字典和對象。
在模板中使用變量的一些示例如下:
<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>
可以使用過濾器修改變量,過濾器名添加在變量名之后,中間使用豎線分隔。
例如,下述模板以首字母大寫形式顯示變量 name 的值:
Hello,{{ name|capitalize }}
表3-1 出了Jinjia2提供的部分常用過濾器。
過濾名 | 說明 |
safe | 渲染值時不轉義 |
capitalize | 把值的首字母裝換成大寫,其他字母轉換成小寫 |
lower | 把值轉換成小寫形式 |
upper | 把值轉換成大寫形式 |
title | 把值中的每個單詞的首字符都轉換成大寫 |
trim | 把值的收尾空格去掉 |
striptags | 渲染之前把值中所有的HTML標簽都刪掉 |
safe 過濾器值得特別說明一下。
默認情況下,出於安全考慮,Jinjia2會轉義所有變量。
例如,如果一個變量的值為 ‘ <h1>Hello</h1>’,Jinjia2會將其渲染成 ‘< h1>Hello</h1>’,瀏覽器能顯示這個h1元素,但不會進行解釋。
很多情況下需要顯示變量中存儲的HTML代碼,這時就可使用safe過濾器。
注:千萬別在不可信的值上用safe 過濾器,例如用戶在表單中輸入的文本。
完整的過濾器列表可在Jinjia2文檔(http://jinja.pocoo.org/docs/templates/#builtin-filters)中查看。
3.1.3 控制結構
Jinjia2提供了多種控制結構,可用來改變模板的渲染流程。
本節使用簡單的例子介紹其中最有用的控制結構。
下面這個例子展示了如何在模板中使用條件控制語句:
{% 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>
為了重復使用宏,我們還可以將其保存在單獨的文件中,然后再需要使用的模板中導入:
{% import 'macro.html' as macros %} <ul> {% for comment in comments %} {{ macros.render_comment(comment) }} {% endfor %} </ul>
需要在多處重復使用的模板代碼片段可以寫入單獨的文件,再包含在所有模塊中,以避免重復:
{% 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和 block 的塊。
注意,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。
在 extends 指令之后,基模板中的3個塊被重新定義,模板引擎會將其插入適當的位置。
注意新定義的 head 塊,在基模板中其內容不是空的,所以使用 super() 獲取原來的內容。
稍后會展示這些控制結構的具體用法,讓你了解一些它們的工作原理。
3.2 使用Flask-Bootstrap 集成 Twitter Bootstrap
BootStrap(http://getbootstrap.com/)是 Twitter 開發的一個開源框架。
它提供的用戶界面組件可用於創建整潔且具有吸引力的網頁,而且這些網頁還能兼容所有現代Web瀏覽器。
Bootstrap 是客戶端框架,因此不能直接涉及服務器。
服務器需要做的只是提供了引用Bootstrap層疊樣式表(CSS)和JavaScript文件的HTML相應,並在HTML、CSS和JavaScript 代碼中實例化所需組件。
這些操作最理想的執行場所就是模板。
要想在程序中集成Bootstrap,顯然要對模板做所有必要的改動。
不過,更簡單的方法是使用一個名為 Flask-Bootstrap 的Flask 擴展,簡化集成的過程。
Flask-Bootstrap使用pip安裝
pip install flask-bootstrap
Flask 擴展一般都在創建程序示例時初始化。
示例 3-4 是 Flask-Bootstrap的初始化方法。
示例 3-4 hello.py: 初始化 Flask-Bootstrap
from flask-bootstrap import Bootstrap #...... bootstrap = Bootstarp(app)
初始化Flask-Bootstrap 之后,就可以在程序中使用一個包含所有 Bootstrap 文件的基模板。
這個模板利用Jinja2的模板繼承機制,讓程序擴展一個具有基本頁面結構的基模板,其中就有用來引入Bootstrap的元素。
示例 3-5 templates/user.html : 使用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 中的 extends 指令從 Flask-Bootstrap 中導入 bootstrap/base.html ,從而實現模板繼承。
Flask-Bootstrap 中的基模板提供了一個網頁框架,引入了 Bootstrap總的所有CSS和JavaStript文件。
基模板中定義了可在衍生模板中重定義的塊。Block和endblock指令定義的塊中的內容可添加到基模板中。
上面這個 user.html 模板定義了 3個塊,分別名為 title、navbar和content。
這些塊都是及模板提供的,可在衍生模板中重新定義。
title 塊的作用很明顯,其中的內容會出現在渲染后的HTML文檔頭部,放在<title>標簽中。
navbar和content這兩個塊分別表示頁面中的導航條和主體內容。
在這個模板中,vavbar 塊使用Bootstrap 組件定義了一個簡單的導航條。
content 塊中有個<div>容器,其中包含一個頁面頭部。
之前版本的模板中的歡迎信息,現在就放在這個頁面頭部。
改動之后的程序,如圖:
Flask-Bootstrap 的 base.html 模板還定義了很多其他塊,都可以在衍生模板中使用。
Flask-Bootstrap基模板中定義的塊
塊名 | 說明 |
doc | 整個HTML文檔 |
html_attribs | <html>標簽的屬性 |
html | <html>標簽中的內容 |
head | <head>標簽中的內容 |
titlte | <title>標簽中的內容 |
metas | 一組<meta>標簽 |
styles | 層疊樣式表定義 |
body_attribs | <body>標簽的屬性 |
body | <body>標簽中的內容 |
navbar | 用戶定義的導航條 |
content | 用戶定義的頁面內容 |
scripts | 文檔底部的javaScript聲明 |
上表中很多塊都是Flask-Bootstrap自用的,如果直接重定義可能會導致一些問題。
例如,Bootsrap 所需的文件在styles 和 scripts 塊中聲明。
如果程序需要向已經有內容的塊中添加新內容,必須使用Jinja2提供的super() 函數。
例如,若果要在衍生模板中添加新的JavaScript文件,需要這么定義scripts塊。
{% block scripts %} {{ super() }} <script type="text/javascript" src="my-script.js"></script> {% endblock %}
3.3 自定義錯誤頁面
如果在瀏覽器的地址欄中輸入了不可用的路由,那么會顯示一個狀態碼為 404 的錯誤頁面。
現在這個錯誤頁面太簡陋、平庸,而且樣式和使用了 Bootstrap 的頁面不一致。
像常規路由一樣,Flask允許程序使用基於模板的自定義頁面。
最常見的錯誤代碼有兩個:
404 ,客戶端請求未知頁面或路由時顯示;
500 , 有未處理的異常時顯示。
為這兩個錯誤代碼指定自定義處理程序的方式如示例 3-6 所示。
示例3-6 hello.py :自定義錯誤頁面
@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 ,然后把這兩個文件中的頁面為頭部元素改為響應的錯誤消息。但這種方法會帶來很多重復勞動。
Jinja2 的模板繼承機制可以幫助我們解決這一問題。
Flask-Bootstrap 提供了一個具有頁面基本布局的基模板,同樣,程序可以定義一個具有完整頁面布局的基模板,其中包含導航條,而頁面內容則可留到衍生模板中定義。
示例 3-7 展示了 templates/base.html 的內容,這時一個繼承自bootstrap/base.html 的新模板,其中定義了導航條。這個模板本身也可作為其他模板的基模板,例如 templates/user.html、templates/404.html 和 templates/500.html 。
示例 3-7 templates/base.html :包含導航條的程序基模板
{% 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 的基模板。
通過集成 templates/base.html 模板編寫自定義的404錯誤頁面很簡單,如示例 3-8 所示
示例3-8 templates/404.html : 使用模板繼承機制自定義 404 錯誤頁面
{% extends "base.html" %} {% block title %}Flasky - Page Not Found{% endblock %} {% block page_content %} <div class="page-header"> <h1>Not Found</h1> </div> {% endblock %}
錯誤頁面在瀏覽器總顯示效果:
templates/user.html 現在可以通過繼承這個基模板來簡化內容。如示例 3-9 所示。
示例 3-9 templates/user.html : 使用模板繼承機制簡化頁面模板
{% extends "base.html" %} {% block title %}Flasky{% endblock%} {% block page_content %} <div class="page-header"> <h1>Hello,{{ name }}!</h1> </div> {% endblock %}
3.4 鏈接
任何具有多個路由的程序都需要可以連接不同頁面的鏈接,例如導航條。
在模板中直接編寫簡單的路由的URL鏈接不難,但對於包含可變部分的動態路由,在模板中構建正確的URL就很困難。
而且,直接編寫URL會對代碼中定義的路由產生不必要的依賴關系。
如果重新定義路由,模板中的鏈接可能會失效。
為了避免這些問題,Flask 提供了 url_for() 輔助函數,它可以使用程序URL映射中保存的信息生成URL。
url_for() 函數最簡單的用法是以視圖函數名(或者 app.add_rul_route() 定義路由時使用的端點名)作為參數,返回對應的URL。
例如,在當前版本的 hello.py 程序中調用 url_for('index' )得到的結果是 / 。
調用 url_for('index',_external=True) 返回的則是絕對地址,在這個示例中是 http://localhost:5000/ 。
生成連接程序內不同路由的鏈接時,使用相對地址就足夠了。
若要生成在瀏覽器之外使用的鏈接,則必須使用絕對地址,例如在電子郵件中發送的鏈接。
使用 url_for() 生成動態地址時,將動態部分作為關鍵字參數傳入。
例如, url_for('user',name='john',_external=True) 返回的結果是 http://localhost:5000/user/john 。
傳入 url_for() 的關鍵字參數不僅限於動態路由中的參數。
函數能將任何額外參數添加到查詢字符串中。
例如, url_for('index',page=2) 返回的是 /?page=2 。
3.5 靜態文件
Web程序不是僅由Python代碼和模板組成。
大多數程序還會使用靜態文件,例如HTML代碼中引用的圖片、JavaScript源碼文件和CSS。
你可能還記得在第2章中檢查hello.py 程序的URL映射時,其中有一個static路由。
這是因為對靜態文件的引用被當成一個特殊的路由。即 /static/<filename>。
例如,調用 url_for('static',filename='css/styles.css',_external=True)
得到的結果是 http://localhost:5000/static/css/styles.css
默認設置下,Flask在程序根目錄中名為static的文件夾中尋找靜態文件。
如果需要,可在static文件夾中使用子文件夾存放文件。
服務器收到前面那個URL后,會生成以響應,包含文件系統中 static/css/static.css 文件的內容。
示例 3-10 展示了如何在程序的基模板中放置 favicon.ico 圖標。這個圖標會顯示在瀏覽器的地址欄中。
示例 3-10 templates/base.html :定義收藏夾圖標
{% 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() 保留基模板中定義的塊的原始內容。
3.6 使用 Flask-Moment 本地化日期和時間
如果 Web 程序的用戶來時世界各地,那么處理日期和時間可不是一個簡單的任務。
服務器需要統一時間單位,這和用戶所在的地理位置無關,所以一般使用協調時間時(Coordinated Universal Time ,UTC)。
不過用戶看到UTC格式的時間會感到困惑,它們更希望看到當地時間,而且采用當地管用的格式。
要想在服務器上只使用UTC時間,一個優雅的解決方法是,把時間單位發送給Web瀏覽器,轉換成當地時間,然后渲染。
Web 瀏覽器可以更好地完成這一任務,因為它能獲取用戶電腦中的時區和區域設置。
有一個使用JavaScript 開發的優秀客戶端開源代碼庫,名為 moment.js (http://momentjs.com/),
它可以在瀏覽器中渲染日期和時間。
Flask-Moment 是一個Flask 程序擴展,能把moment.js 集成到Jinja2模板中。
Flask-Moment 可以使用 pip 安裝:
pip install flask-moment
這個擴展的初始化方法如示例3-11。
示例 3-11 hellp.py :初始化 Flask-Moment
from flask_moment import Moment moment = Moment(app)
除了 moment.js ,Flask-Moment 還依賴jquery.js。
要在HTML文檔的某個地方引入這兩個庫,可以直接引入,這樣可以選擇使用哪個版本,也可以使用擴展提供的輔助函數,從內容分發網絡(Content Delivery ,CDN)中引入通過測試的版本。
Bootstrap 已經引入了 jquery.js,因此只需要引入 moment.js 即可。
示例 3-12 展示了如何在基模板的scripts 塊中引入這個庫。
示例 3-12 templates/base.html :引入Moment.js 庫
{% block scripts %}
{{ super() }}
{{ moment.include_moment() }}
{% endblock %}
為了處理時間戳,Flask-Moment 向模板開放了moment 類。示例 3-13 中代碼把變量current_time 傳入模板進行熏染。
示例 3-13 hello.py:加入一個datetime變量
from datetime import datetime @app.route("/") def index(): return render_template('index.html',current_time=datetime.utcnow())
示例 3-14 展示了如何在模板中渲染 current_time。
示例 3-14 templates/index.html:使用 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’ 到 ‘LLL’ 分貝對應不同的復雜度。
format() 函數還可接受自定義的格式說明符。
第二行中的 fromNow() 渲染相對時間戳,而且會隨着時間的推移自動刷新顯示的時間。
這個時間戳最開始顯示為 “a few seconds ago”,但指定 refresh 參數后,其內容會隨着時間的推移個更新。
如果一直待在這個頁面,幾分鍾后,會看到顯示的文本變成 "a mimute age" "2 minutes age"等。
Flask-Moment 實現了 moment.js中的 format()、fromNow()、calendar()、valueOf()和 unix() 方法。
你可查閱文檔(http://momentjs.com/docs/#/displaying/)學習moment.js提供的全部格式化選項。
Flask-Moment 假定服務器端程序處理的時間戳是“純正的”datetime對象,且使用UTC表示。
關於純正和細致的日期和時間對象的說明,請閱讀標准庫中datetime包的文檔(https://docs.python.org/3/library/datetime.html)
( 純正的時間戳,英文為 navie time,指不包含時區的時間戳;
細致的時間戳,英文為 aware time,指包含時區的時間戳)
Flask-Moment 渲染的時間戳可實現多種語言的本地化。
語言可在模板中選擇,把語言代碼傳給 lang() 函數即可:
{{ moment.lang('es') }}
使用本章介紹的技術,你應該能為程序編寫出現代化且用戶友好的網頁。
下一章將介紹本章沒有涉及的一個模板功能,即如果通過Web 表單和用戶交互。