第三章、模板
視圖函數作用即生成請求的響應,如果把業務邏輯和表現邏輯混在一起會導致代碼難以理解和維護。吧表現邏輯轉移到模板中能夠提升程序的可維護性。
模板是一個響應文本的文件,其中包含用佔位變量表示的動態部分,其具體值只在請求的上下文才能知道。
使用真實值替換變量,在返回最終得到的響應字符串,這一過程稱爲渲染。
3.1、Jinja2模板引擎
3.1.1、渲染模板
在默認情況下,Flask程序會在templates子文件夾中尋找模板。在下一個hello.py版本中,要把前面定義的模板保存在templates文件夾中,並分別命名爲index.html和user.html。
from flask import Flask,render_template
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函數吧Jinja2模板引擎集成到程序中。render_template函數的第一個參數是模板的文件名,隨後的參數都是鍵值對,表示模板中變量對應的真實值。
3.1.2、變量
-
Jinja2能識別所有類型的變量,甚至是一些複雜的類型,例如列表、字典和對象。在模板中使用變量的一些示例如下:
<p>DICT {{ mydict['key'] }}</p> <p>LIST {{ mylist[3] }}</p> <p>list with a variable index: {{ mylist[myintvar]}}</p> <p> object's method: {{ myobj.somemethod() }} </p>
- 常用Jinja2 變量過濾器
- safe 渲染值時不轉義
- capitalize 把值的首字母轉換成大寫,其他字母轉換成小寫
- lower 把值轉換成小寫形式
- upper 把值轉換成大寫形式
- title 把值中每個單詞的首字母都轉換成大寫
- trim 把值的首尾空格去掉
- striptags 渲染之前把值中所有的HTML標籤都刪掉
3.1.3、控制結構
條件控制語句
{% if user %}
{% else %}
{% endif %}
for循環語句
{% for comment in comments %}
{% endfor %}
支持宏
{% marco render_comment(comment) %}
多處重複使用的模板代碼片段可以寫入單獨的文件,再包含在所有的模板中,以避免重複:
{ % include 'comment.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 bash.html %} {% block title %} Index {% endblock%} {% block head%} {{ super() }} <style> </style> {% endblock %} {% block body %} <h1>hello,world</h1> {% endblock %}
- extends指令聲明這個模板衍生自base.htmk,在extends指令之後,基礎模板中的3個板塊重新定義,模板引擎會賈汪其插入適當的位置。注意新定義head塊,在基礎模板中內容是空的,所以使用super()獲取原來的內容。
3.2、使用Flask-Bootstrap集成Twitter Bootstrap
Bootstrap是Twitter開發的一個開源框架,它提供的用戶界面組件可用於創建整潔且具有吸引力的網頁,並且這些網頁還能兼容所有現代的Web瀏覽器。
Bootstrap是客戶端框架,不會直接涉及服務器。要下在程序中繼承Bootstrap,顯然需要對模板做所有必要的改動,更簡單的方法就是安裝Flask-Bootstrap的Flask擴展,簡化集成的過程。
- Flask-Bootstrap使用pip方式安裝
pip install flask-bootstrap
-
Flask擴展一般在創建程序實例時初始化。
from flask.ext.bootstrap import Bootstrap bootstrap = Bootstrap(app)
- 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和JavaScript文件。
- 基礎模板中定義了可在衍生模板中重新定義的塊。block和endblock指令定義塊中的內容可添加到基模板中。
Flaks-Bootstrap基模板中定義的塊:
塊名 說明
- doc 整個HTML文檔
- html_attribs <html>標籤的屬性
- html <html>標籤的內容
- head <head>標籤中的內容
- title <title>標籤中的內容
- metas 一組<meta>標籤
- styles 層疊樣式表定義
- body_attribs <body>標籤的屬性
- body <body>標籤中的內容
- navbar 用戶定義的導航條
- content 用戶定義的頁面內容
- scripts 文檔底部的JavaScript聲明
3.3、自定義錯誤頁面
Flask允許程序使用基於模板的自定義錯誤頁面,最常見的錯誤代碼有兩個:
404,客戶端請求未知頁面或路由時顯示。
500,有未處理的異常時顯示。
- 自定義錯誤頁面:
@app.errorhandler(404)
def page_not_found(e):
return render_template('404.html'),404
@app.errorhandler(500)
def internal_server_error(e):
return render_templte('500.html'),500
- 和視圖函數一樣,錯誤處理程序也返回響應,它們還返回與該錯誤對應的數字狀態碼。
- 如果從Github上克隆了這個程序的Git倉庫,執行git checkout 3c簽出這個程序的這個版本。
3.4、鏈接
在模板中直接編寫簡單路由的URL連接不難,但對於包含可變部分的動態路由,在模板中構建正確的URL就很困難;並且直接編寫URL會對代碼中定義的路由產生不必要的依賴。
Flask提供了url_for()輔助函數,可以使用程序URL映射中保存的信息生成URL。
url_for()函數最簡單的用法是以視圖函數名(後者app.add_url_route()定義路由時使用的端點名)作爲參數,返回對應的URL。
使用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、靜態文件
默認設置下,Flask在程序根目錄中名爲static的子目錄中尋找靜態文件。如果需要,可在static文件夾中使用子文件夾存放文件。
3.6、使用Flask-Monment本地化日期和時間
問題背景:如果Web程序的用戶來自世界各地,那麼處理日期和時間就不是一個簡單的任務。
解決方法:通過使用JavaScript開發的優秀客戶端開源代碼庫,名爲moment.js,可以在瀏覽器中渲染日期和時間。Flask-Monment是一個Flask程序擴展。能把moment.js集成到Jinja2模板中。
-
Flask-Moment可以通過pip安裝:
pip install flask-moment
- 初始化Flask-Moment:
from flask.ext.moment import Moment moment = Moment(app)
- 除了moment.js,Flask-Moment還依賴jquery.js.Bootstrap已經引入了jquery.js,因此只需引入moment.js即可。
{% block scripts %} {{ super() }} {{ moment.include_moment() }} {% endblcok %}
- 爲了處理時間戳,Flask-Moment想模板開放了moment類,後臺可以將utc時間傳到前臺進行渲染。
from datetime improt datetime @app.route('/') def index(): return render_template('index.html',current_time=datetime.utcnow())
- 在模板中渲染current_time
<p>The local date and time is {{ moment(current_time).format('LLL) }}</p> <p>That was {{ moment(current_time).fromNow(refresh=True) }}</p>
更多moment.js用法:http://momentjs.com/docs/#/displaying/
Flask-Moment假定服務器端程序處理的時間是“純正的”datetime對象,且使用UTC表示。
第四章、Web表單
對於一些重複操作(生成表單的HTML代碼和驗證提交的表單數據),Flask-WTF擴展可以把處理Web表單的過程變成一種愉悅的體驗。這個擴展對獨立的WTForms包進行了包裝,方便集成到Flask程序中。
WTForms官網:http://wtforms.simplecodes.com
- Flask-WTF及其依賴可使用pip安裝:
pip install flaks-wtf
4.1、跨站請求僞造保護
默認情況,Flask-WTF能保護所有表單面授跨站請求僞造(CSRF)的***。爲了實現CSRF保護,Flask-WTF需要程序設置一個密鑰。Flask-WTF使用這個密鑰生成加密令牌,再用令牌驗證請求中表單數據的真僞。
示例代碼(設置Flask-WTF):
app = Flask(__name__)
app.config['SECRET_KEY'] = 'hard to guess string'
- 代碼詳解:app.config字典可用來存儲框架、擴展和程序本身的配置變量
- SECRET_KEY配置變量是通用密鑰,可在Flask和多個第三方擴展中使用。
4.2、表單類
使用Flask-WTF時,每個Web表單都由一個繼承自Form的類表示。
示例代碼(一個簡單的Web表單,包含一個文本字段和一個提交按鈕):
from flaks.ext.wtf import Form
from wtforms import StringField,SubmitField
from wtforms.validators import Required
class NameForm(Form):
name = StringField("What's your name?",validators=[Required()])
submit = SubmitField('Submit')
- 代碼詳解:StringField類表示屬性爲type="text"的<input>元素;SubmitField類表示屬性爲type="submit"的<input>元素。StringField構造函數中的可選參數validators指定一個由驗證函數組成的列表,在接收用戶提交的數據之前驗證數據。驗證函數Required()確保提交的字段不爲空
- Form基類由Flask-WTF擴展定義,從falsk.ext.wtf中導入。字段和驗證函數可以直接從WTForms包中導入。
WTForms支持的HTML標準字段
- StringField 文本字段
- TextAreaField 多行文本字段
- PasswordField 密碼文本字段
- HiddenField 隱藏文本字段
- DateField 文本字段,值爲 datetime.date 格式
- DateTimeField 文本字段,值爲 datetime.datetime 格式
- IntegerField 文本字段,值爲整數
- DecimalField 文本字段,值爲 decimal.Decimal
- FloatField 文本字段,值爲浮點數
- BooleanField 複選框,值爲 True 和 False
- RadioField 一組單選框
- SelectField 下拉列表
- SelectMultipleField 下拉列表,可選擇多個值
- FileField 文件上傳字段
- SubmitField 表單提交按鈕
- FormField 把表單作爲字段嵌入另一個表單
- FieldList 一組指定類型的字段
WTForms驗證函數
- Email 驗證電子郵件地址
- EqualTo 比較兩個字段的值;常用於要求輸入兩次密碼進行確認的情況
- IPAddress 驗證 IPv4 網絡地址
- Length 驗證輸入字符串的長度
- NumberRange 驗證輸入的值在數字範圍內
- Optional 無輸入值時跳過其他驗證函數
- Required 確保字段中有數據
- Regexp 使用正則表達式驗證輸入值
- URL 驗證 URL
- AnyOf 確保輸入值在可選值列表中
- NoneOf 確保輸入值不在可選值列表中
4.3、把表單渲染成HTML
示例代碼:(使用Flask-WTF和Flask-Bootstrap渲染表單)
{% extends 'base.html' %}
{% import "bootstrap/wtf.html" as wtf %}
{% block title %}Flasky{% endblock %}
{% block page_content %}
<div class="page-header">
<h1>hello,{% if name %}{{ name }}{% else %}Stranger{% endif%}</h1>
</div>
{{ wtf.quick_form(form)}}
{% endblock %}
- 代碼詳解:導入的bootstrap/wtf.html文件定義了一個使用Bootstrap渲染Flask-WTF表單對象的輔助函數。wtf.quick_form()函數的參數爲Flask_WTF表單對象,使用Bootstrap的默認樣式渲染傳入表單。
4.4、在視圖函數中處理表單
示例代碼:(視圖函數index()不僅要渲染表單,還要接受表單中的數據。)
@app.route('/',methods=['GET','POST'])
def index():
name = None
form = NameForm()
if form.validate_on_submit():
name = form.name.data
form.name.data = ''
return render_template('index.html',form=form,name=name)
- 代碼詳解:app.route修飾器中添加的methods參數告訴Flask在URL映射中把這個視圖函數註冊爲GET和POST請求的處理程序。如果沒指定methods參數,則默認把視圖函數註冊爲GET請求的處理程序。
- 用戶提交表單後,服務器會收到一個POST請求。validate_on_submit()會調用name字段上附屬的Required()驗證函數。如果名字不爲空,就能通過驗證,validate_on_submit()返回True。
4.5、重定向和用戶會話
問題背景:當用戶輸入名字後提交表單,再點擊瀏覽器的刷新按鈕,會看到一個警告,關於要求再次提交表單之前進行確認。之所以會出這種問題,是因爲刷新頁面時瀏覽器會重新發送之前已經發送過的最後一個請求。
解決方案:使用重定向作爲POST請求的響應,而不是使用常規響應。重定向是一種特殊的響應,響應內容是URL,而不是包含HTML代碼的字符串。瀏覽器收到這種響應,會向重定向的URL發起GET請求,顯示頁面的內容。
另一問題:如果使用上面的解決方案,程序在處理POST請求時,使用from.name.data獲取用戶輸入的名字,一旦請求結束,數據也就丟失了。所以需要程序將數據存儲到用戶會話中,在請求之間“記住”數據。用戶是一種私有存儲,存在每個連接到服務器的客戶端中。
示例代碼:
from flask import Flask,render_template,session,redirect,url_for
@app.route('/',methods=['GET','POST'])
def index():
form = NameForm()
if form.validate_on_submit():
session['name'] = form.name.data
return redirect(url_for('index'))
return render_template('index.html',form=form,name=session.get('name')))
- 代碼詳解:合法表單數據的請求最後會調用redirect()函數。redirect()是個輔助函數,用來生成HTTP重定向響應。redirect()函數參數是重定向的URL。url_for()生成URL,因爲這個函數使用URL映射生成URL,從而保證URL和定義的路由兼容,並且修改路由名字後依然可用。
4.6、Flash消息
問題背景:用戶提交了有一項錯誤的登錄表單,服務器發回的響應重新渲染了登錄表單,並在表單上面顯示信息,提示用戶名或密碼錯誤。
示例代碼:
from flask ipmort Flask,render_template,session,redirect,url_for,flash
@app.route('/',methods=['GET','POST'])
def index():
form = NameForm()
if form.validate_on_submit():
old_name = session.get('name')
if old_name is not None and old_name != form.name.data:
flash("Looks like you have changed your name")
session['name'] = olde_name
return redirect(url_for('index'))
return render_template('index.html',name=session.get('name'),form=form) ```
* 代碼詳解:代碼會將每一次提交的名字與上一次**存儲在會話中的名字**進行比較,如果兩者不一樣則會發給客戶端下一個響應中顯示一個信息。
* 僅調用flash()函數並不能把消息顯示出來,程序使用的模板要渲染這些信息,最好在基礎模板中渲染Flash消息,因爲這樣所有頁面都能使用這些消息。Flask把**get_flashed_messages()函數**開放給模板,用來獲取並渲染消息。
{% block content %}
<div class="container">
{% for message in get_flashed_messages() %}
<div class="alert alert-warning">
<button type="button" class="close" data-dismiss="alert">
×
</button>
{{ message }}
</div>
{% endfor %}
{% block page_content %}
{% endblock %}
</div>
{% endblock %}