1. 程式人生 > >Flask框架——模板:分離資料與檢視

Flask框架——模板:分離資料與檢視

作者:chen_h
微訊號 & QQ:862251340
微信公眾號:coderpai

Flask 框架學習目錄

1. 概述

Flask框架的基本定位是開發服務端的動態網頁應用。因此,模板是必不可少的環節。 Flask使用的是Jinja2模板引擎,因此本節課程中的模板語法,基本遵從Jinja的文件。

在本節課程中,主要從以下幾個方面講解Flask框架中的模板:

  • Flask的模板引擎

  • 兩種模板渲染的方法

  • 在模板中使用變數和表示式

  • 在模板中使用全域性物件

  • 自定義模板全域性物件

  • 在模板中使用過濾器

  • 自定義模板過濾器

  • 模板變數的轉義問題

  • 在模板中使用迴圈結構

  • 在模板中使用遞迴迴圈結構

  • 在模板中使用條件結構

2. 模板引擎

在Flask中,檢視函式的返回值為響應的正文被送往前端瀏覽器。毫無疑問,一個實用 的檢視函式需要在服務端根據請求的不同動態構造這個內容。然而手工拼接一段冗長 的HTML串是乏味而且相當容易出錯。

這正是模板引擎發揮威力的地方,只需要將模板資料送入模板引擎,我們就告 別了那些那些拼接、轉義之類的瑣碎之事,輕鬆得到一個渲染後的字串:

在Flask中,使用模板引擎基本就是分這三個步驟:

第一、宣告資料

uname = 'WHOAMI'
````

第二、編寫模板





<div class="se-preview-section-delimiter"
></div>

tpl = ‘

Welcome,{{username}}


第三、提交模板引擎渲染





<div class="se-preview-section-delimiter"></div>

render_template_string(tpl,username=uname)


實驗程式碼如下:





<div class="se-preview-section-delimiter"></div>

-- coding:utf-8 --

from flask import Flask,render_template_string
app = Flask(name

)
@app.route(‘/’)
def v_index():
uname = ‘WHOAMI’
tpl = ‘

Welcome,{{username}}


return render_template_string(tpl,username=uname)

app.run(host=’0.0.0.0’,port=8080)


實驗頁面如下:

![](http://upload-images.jianshu.io/upload_images/1155267-1d7d3436ebd3e84e.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)






<div class="se-preview-section-delimiter"></div>

## 3. 模板渲染

Flask基於Jinja2模板引擎,提供了兩個渲染函式,分別使用字串或單獨的檔案儲存模板內容:

* render_template_string(sourcestr,**context) - 使用sourcestr作為模板字串

* render_template(filename,**context) - 使用filename指定的檔案內容作為模板字串

**render_template_string** :下面的示例使用一個相當簡單的模板,向不同的使用者回 送個性化的歡迎資訊:





<div class="se-preview-section-delimiter"></div>

@app.route(‘/user/’)
def show_user_profile(uname):
return render_template_string(‘

Welcome,{{ uname }}

‘,uname=uname)

在Jinja2的語法中,*{{varibale}}*表示一個輸出命令,每當渲染引擎發現一個輸出命令,它就 在渲染結果中,使用模板資料上下文中變數*variable*的值替換原始的輸出命令。

**render_template** :在生產環境中,在程式碼裡寫模板字串不是什麼好主意。正經的方法是將模板寫在 單獨的模板檔案裡,使用render_template()函式進行渲染:





<div class="se-preview-section-delimiter"></div>

@app.route(‘/user/’)
def v_user(username):
return render_template(‘user.html’,username=username)


預設情況下,Flask使用當前模組資料夾下的*templates*子目錄作為模板目錄,user.html檔案 應當放置在這個資料夾下:





<div class="se-preview-section-delimiter"></div>

/app
/web.py
/templates
/user.html


user.html的內容如下:





<div class="se-preview-section-delimiter"></div>

Welcome, {{ username }}







<div class="se-preview-section-delimiter"></div>

## 4. 變數與表示式

模板變數在模板被渲染時通過*上下文字典*傳遞給模板。下面的示例中,在模板中使用了 變數*name*和*age*,當呼叫*render_template_string()*渲染模板時,通過關鍵字引數 將兩個變數的值傳遞進來:





<div class="se-preview-section-delimiter"></div>

tpl = ‘name : {{ name }} age : {{age}} ’
print render_template_string(tpl,name=’Marion5’,age=12)


**變數成員** :如果傳入模板的變數是不是Python簡單型別,而是比如字典或物件型別, 那麼在模板中可以向Python中一樣的方式訪問其成員屬性或方法。

稍有不同的是,對於字典變數,除了可以使用*[]*方式訪問其成員,還可以使用*.*:





<div class="se-preview-section-delimiter"></div>

tpl = ‘name: {{u[“name”]}} name again:{{u.name}}’
print render_template_string(tpl,u={‘name’:’Marion5’,’age’:12})


同樣的,對於物件變數,除了使用*.*訪問屬性值,還可以使用*[]*:





<div class="se-preview-section-delimiter"></div>

class User:
def init(self,name,age):
self.name = name
self.age = age

tpl = ‘name : {{u.name}} name again:{{u[“name”]}}’
print render_template_string(tpl,u=User(‘Mary’,20))


**表示式** :變數還可以應用表示式,比如進行數學運算,那些常用的數學 操作符( + - * / // % ** )都是有效的:





<div class="se-preview-section-delimiter"></div>

data = {‘x’:12,’y’:13}
tpl = ‘{{x}} + {{y}} = {{ x+y }}’
print render_template_string(tpl,**data)


或者進行比較或邏輯運算( == != > >= < <= and or not):





<div class="se-preview-section-delimiter"></div>

data = {‘x’:12,’y’:13,’z’:11}
tpl = ‘{{x}} > {{y}} : {{ x>y }}’
print render_template_string(tpl,**data)


**函式呼叫** :在輸出命令中,可以對變數或常量進行函式呼叫:





<div class="se-preview-section-delimiter"></div>

tpl = ‘{{ range(10) }} ’
print render_template_string(tpl)


需要注意的是,模板有自己的*全域性域/globals*,因此這裡的*range()*函式並不是Python 應用中的函式。





<div class="se-preview-section-delimiter"></div>

## 5. 全域性物件

Jinja2內建的全域性物件包括:

* range([start, ]stop[, step])

* lipsum(n=5, html=True, min=20, max=100)

* dict(**items)

* class cycler(*items)

* class joiner(sep=', ')

Flask向Jinja2模板注入了以下全域性物件,可以在模板中直接訪問:

* config - 當前Flask應用例項的配置物件

* request - 當前HTTP請求物件

* session - 當前HTTP請求的會話物件

* g - 當前HTTP請求週期內的全域性物件

* url_for() - URL生成函式

* get_flashed_messages() - 閃信函式

下面的示例中,從session中提取當前使用者名稱:





<div class="se-preview-section-delimiter"></div>

@app.route(‘/’)
def v_index():
tpl = ‘welcome back, {{ session.username }}’
return render_template_string(tpl)






<div class="se-preview-section-delimiter"></div>

## 6. 自定義全域性物件

可以使用應用物件的context_processor裝飾器向引擎注入額外的全域性物件。 下面的示例向模板全域性域中注入*vendor*變數,其值為*hubwiz*:





<div class="se-preview-section-delimiter"></div>

@app.context_processor
def vendor_processor():
return dict(vendor=’hubwiz’)


這時我們可以在模板中直接使用*vendor*變量了:





<div class="se-preview-section-delimiter"></div>

@app.route(‘/’)
def v_index():
tpl = ‘powered by {{vendor}}’
return render_template_string(tpl)


當然,同樣的方法可以用於注入全域性函式。下面的示例向模板全域性域中注入*format_price* 函式:





<div class="se-preview-section-delimiter"></div>

@app.context_processor
def utility_processor():
def format_price(amount, currency=u’€’):
return u’{0:.2f}{1}’.format(amount, currency)
return dict(format_price=format_price)






<div class="se-preview-section-delimiter"></div>

## 7. 過濾器

模板中可以使用過濾器*|*來修改變數的值。下面的示例使用內建的*title*過濾器 將*name*變數中每個單詞的首字母轉換為大寫:





<div class="se-preview-section-delimiter"></div>

tpl = ‘{{ name|title }}’
print render_template_string(tpl,name=’jimi hendrix’) #Jimi Hendrix


**過濾器級聯** :可以將多個過濾器串聯起來,構成過濾流水線。下面的示例對name 變數依次使用了兩個過濾器,*scriptags*過濾器用來除去name變數中的HTML標籤, *title*過濾器將傳入的字串中每個單詞的首字母轉換為大寫:





<div class="se-preview-section-delimiter"></div>

tpl = ‘{{ name|striptags|title }}’
print render_template_string(tpl,name=’

jimi hendrix

‘) #Jimi Hendrix

**過濾器引數** :可以使用小括號為過濾器傳入額外的引數。下面的示例將列表型變數 的多個成員使用*join*過濾器連線起來:





<div class="se-preview-section-delimiter"></div>

tpl = ‘{{ seq | join(“-“) }}’
print render_template_string(tpl,seq=[1,2,3]) # 1-2-3


在Jinja2中,一個過濾器其實就是一個函式,第一個引數用來接收前序環節傳入的值,而 返回值則作為後續環節過濾器函式的第一個引數:

![](http://upload-images.jianshu.io/upload_images/1155267-7d9c560a0e0a41ea.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

Jinja2內建了很多過濾器,在其官網[文件頁](http://jinja.pocoo.org/docs/dev/templates/#builtin-filters) 可以瞭解詳細情況。





<div class="se-preview-section-delimiter"></div>

## 8. 定製過濾器

我們已經知道,過濾器其實就是一個函式。在Flask中,可以使用*Flask.template_filter* 裝飾器建立自己的過濾器。下面的示例建立了一個名為*reverse*的串反轉過濾器,它總是 將輸入的字串逆向重排:





<div class="se-preview-section-delimiter"></div>

@app.template_filter(‘reverse’)
def reverse_filter(s):
return s[::-1]


下面的示例演示瞭如何呼叫我們自制的過濾器:





<div class="se-preview-section-delimiter"></div>

@app.route(‘/’)
def index():
return render_template_string(‘{{ greeting | reverse }}’,greeting=’Hello, Jinja2’ )


另一種等價地建立定製過濾器的方法是將過濾器函式新增到Flask應用例項的*jinja_env*字典中:





<div class="se-preview-section-delimiter"></div>

def reverse_filter(s):
return s[::-1]
app.jinja_env.filters[‘reverse’] = reverse_filter






<div class="se-preview-section-delimiter"></div>

## 9. 塊過濾器

塊過濾器可以在整塊內容上應用指定的過濾器:





<div class="se-preview-section-delimiter"></div>

{% filter [filtername] %}

{% endfilter %}


下面的示例將filter塊內的模板內容使用*upper*過濾器全部轉換成大寫:





<div class="se-preview-section-delimiter"></div>

tpl = ”’
{% filter upper %}

Filter sections allow you to apply regular Jinja2 filters on a block
of template data. Just wrap the code in the special filter section


{% endfilter %}
”’
render_template_string(tpl)

在塊過濾器上也可以使用多個過濾器進行*級聯*。下面的示例首先對filter塊內容使用*escape* 過濾器進行轉義,然後使用*upper*過濾器將其轉換為大寫:





<div class="se-preview-section-delimiter"></div>

tpl = ”’
{% filter escape | upper %}

Filter sections allow you to apply regular Jinja2 filters on a block
of template data. Just wrap the code in the special filter section


{% endfilter %}
”’
render_template_string(tpl)





<div class="se-preview-section-delimiter"></div>

## 10. 模板中的變數風險

模板引擎的基本工作是依據給出的*未知的*資料上下文,結合模板生成HTML串。 考慮到結果HTML串將要在客戶端的瀏覽器中執行,這裡面存在著諸多的隱患。

**XSS** :如果模板變數來自於使用者輸入,那麼存在被惡意訪問者注入用於跨站攻擊指令碼的風險。

在下面的示例中,資料上下文*user*的內容來自於資料庫中,而暱稱/nickname 的值是允許使用者自己修改的。一個惡意的訪問者在自己的暱稱中摻雜了指令碼,當 任意使用者訪問該使用者的個人頁面時,都將被彈窗:





<div class="se-preview-section-delimiter"></div>

user = {‘id’:123,’nickname’:’haha‘}
tpl = ‘

homepage of {{nickname}}


render_template_string(tpl,**user)

**頁面變形** : 另一種輕微一些但更常見的問題是使用者提交的資料中包含具有 特殊意義的HTML字元,比如*<*、*>*、*&*等。下面的示例中,使用者 的暱稱恰好看起來恰好是一個HTML標籤,導致其個人頁面中不能顯示暱稱:





<div class="se-preview-section-delimiter"></div>

user = {‘id’:123,’nickname’:’< IAMKING>’}
tpl = ‘

homepage of {{nickname}}


render_template_string(tpl,**user)





<div class="se-preview-section-delimiter"></div>

## 11. 變數轉義

解決這些問題的辦法就是對變數執行*轉義*操作,將變數中的具有特殊含義的HTML字元 使用HTML實體碼錶示。例如:*<IAMKING>*將被轉換為*<IAMKING>*。

**自動轉義** : 在模板中使用*autoescape*標籤可以開啟或關閉模板引擎的自動轉義 功能。在開啟自動轉義功能時,模板引擎將對轉義塊內的所有變數自動執行轉義操作。

下面的示例中,使用*autoescape*標籤開啟了自動轉義:





<div class="se-preview-section-delimiter"></div>

user = {‘id’:123,’nickname’:’< IAMKING>’}
tpl = ”’
{% autoescape true %}


{% endautoescape %}
”’
render_template_string(tpl,**user)

但是自動轉義開啟的時候,會對轉義塊內所有的變數執行轉義操作,即是這些變數壓根 不可能包含HTML字元,或者其內容可控。當變數數量很多時,這將造成不必要的效能損失。

我們可以使用*safe*過濾器將這些可控的變數標記為安全的,渲染引擎將不再對其進行轉義。 下面的示例中,使用*safe*標籤取消*id*變數的轉義操作:





<div class="se-preview-section-delimiter"></div>

user = {‘id’:123,’nickname’:’< IAMKING>’}
tpl = ”’
{% autoescape true %}


{% endautoescape %}
”’
render_template_string(tpl,**user)

**手動轉義** :和自動轉義對應的就是手動的對變數執行轉義操作。方法是使用*escape* 過濾器,可以簡寫為*e*。

下面的示例中,對模板中的*nickname*變數執行手動轉義:





<div class="se-preview-section-delimiter"></div>

user = {‘id’:123,’nickname’:’< IAMKING>’}
tpl = ‘


render_template_string(tpl,**user)





<div class="se-preview-section-delimiter"></div>

## 12. 迴圈結構

假設我們有一組使用者資料如下:





<div class="se-preview-section-delimiter"></div>

data = [
{‘name’ : ‘John’,’age’ : 20,},
{‘name’ : ‘Linda’,’age’ : 21},
{‘name’ : ‘Mary’,’age’ : 30},
{‘name’ : ‘Cook’,’age’ : 40}
]


可以使用迴圈結構,對一組資料使用單一模板進行渲染:





<div class="se-preview-section-delimiter"></div>

{% for [loop condition] %}

{% endfor%}


下面的示例對列表中的每一個物件生成一個*<li>*標籤:





<div class="se-preview-section-delimiter"></div>

tpl = ”’


    {{% for user in users %}}
  • {{ user.name }}
  • {{% endfor %}}

”’
render_template_string(tpl,users=data)

**迭代過濾** :Jinja2的for迴圈不能像Python一樣中途*退出/break*或*跳過/continue*, 但是它支援在迭代時進行*條件過濾*。下面的示例模板只為年齡大於25的使用者生成列表項:





<div class="se-preview-section-delimiter"></div>

tpl = ”’


    {{% for user in users if user.age > 25 %}}
  • {{ user.name }}
  • {{% endfor %}}

”’
render_template_string(tpl,users=data)

**預設輸出塊** :如果沒有執行至少一次迴圈(比如列表為空,或者被過濾了), 可以使用*else*塊生成預設的輸出。下面的示例模板將在沒有使用者匹配時輸出*not found*:





<div class="se-preview-section-delimiter"></div>

tpl = ”’


    {{% for user in users if user.age > 50 %}}
  • {{ user.name }}
  • {{% else %}}
  • not found!
  • {{% endfor %}}

”’
render_template_string(tpl,users=data)





<div class="se-preview-section-delimiter"></div>

## 13. 遞迴迴圈

有些資料是具有不確定層次的遞迴資料,比如檔案系統,目錄裡還有目錄:





<div class="se-preview-section-delimiter"></div>

/application —— 目錄
/app.py —— 檔案
/static —— 目錄
/main.css —— 檔案
/jquery.min.css —— 檔案
/templates —— 目錄
/user.html —— 檔案


其對應的資料表達參見示例中的*tree*物件。Jinja2的迴圈結構支援*遞迴*呼叫。使用方法如下:





<div class="se-preview-section-delimiter"></div>

#### 13.1 使用*recursive*關鍵字宣告迴圈為*遞迴*迴圈





<div class="se-preview-section-delimiter"></div>

{% for item in data recursive}

{% endfor %}






<div class="se-preview-section-delimiter"></div>

#### 13.2 在迴圈內部,使用*loop()*函式呼叫子節點





<div class="se-preview-section-delimiter"></div>

{{ loop(item.children) }}






<div class="se-preview-section-delimiter"></div>

## 14. 迴圈塊中的特殊變數for迴圈塊中,Jinja2提供了關於迴圈的一些特殊變數:

**loop.index** :當次執行的迴圈序號,從1開始。下面的示例將輸出110:





<div class="se-preview-section-delimiter"></div>

{% for i in range(10) %}
{{ loop.index }}
{% endfor %}


**loop.index0** :當前執行的迴圈序號,從0開始。

**loop.revindex** :當前執行的迴圈反序序號,從1開始。下面的示例將輸出101:





<div class="se-preview-section-delimiter"></div>

{% for i in range(10) %}
{{ loop.revindex }}
{% endfor %}


**loop.revindex0** :當前執行的迴圈反序序號,從0開始

**loop.first** :如果當次執行是迴圈中的首次,則值為True。下面的示例將輸出TrueFalseFalse....





<div class="se-preview-section-delimiter"></div>

{% for i in range(10) %}
{{ loop.index }}
{% endfor %}


**loop.last** :如果檔次執行時迴圈中的最後一次,則值為True

**loop.length** :列表中的元素數量

**loop.cycle(*args)** :從一個列表中迴圈取值。下面的示例將迴圈輸出c1、c2、c3、c1、c2、c3...





<div class="se-preview-section-delimiter"></div>

{% for i in range(10) %}
{{ loop.cycle(‘c1’,’c2’,’c3’) }}
{% endfor %}


**loop.depth** :遞迴迴圈的層深,從1開始

**loop.depth0** :遞迴迴圈的層深,從0開始





<div class="se-preview-section-delimiter"></div>

## 15. 條件結構

在Jinja2中,可以使用*條件塊*設定模板內容的輸出條件。只有當指定的條件 滿足時,條件塊內的模板內容才會被渲染輸出:





<div class="se-preview-section-delimiter"></div>

{% if [condition] %}

{% endif %}


下面的示例中,只有當用戶的年齡不小於18歲時,才輸出適合成人觀看的內容:





<div class="se-preview-section-delimiter"></div>

data = {‘name’:’Obama’,age:62}
tpl = ”’
{% if user.age >= 18 %}

some adult content…
{% endif %}
”’
render_template_string(tpl,user=data)

**elif** :可以為條件塊新增使用*elif*新增多重條件判斷分支:





<div class="se-preview-section-delimiter"></div>

{% if [condition] %}

{% elif [condition2] %}

{% elif [condition3] %}

{% endif%}


下面的示例中,當用戶的年齡大於60歲時,輸出養生節目,大於18歲而小於60歲時,輸出成人節目:





<div class="se-preview-section-delimiter"></div>

data = {‘name’:’Obama’,age:62}
tpl = ”’
{% if user.age >= 60%}

some health preserving content…
{% elif user.age >= 18 %}
some adult content…
{% endif %}
”’
render_template_string(tpl,user=data)
“`

else :當條件塊中的所有條件都不滿足時,可以使用else新增預設輸出塊: