Jinja2模板引擎簡介
模板
在前面的示例中,視圖函數的主要作用是生成請求的響應,這是最簡單的請求。實際上,視圖函數有兩個作用:處理業務邏輯和返回響應內容。在大型應用中,把業務邏輯和表現內容放在一起,會增加代碼的復雜度和維護成本。本節學到的模板,它的作用即是承擔視圖函數的另一個作用,即返回響應內容。
- 模板其實是一個包含響應文本的文件,其中用占位符(變量)表示動態部分,告訴模板引擎其具體的值需要從使用的數據中獲取
- 使用真實值替換變量,再返回最終得到的字符串,這個過程稱為“渲染”
- Flask是使用 Jinja2 這個模板引擎來渲染模板
使用模板的好處:
- 視圖函數只負責業務邏輯和數據處理(業務邏輯方面)
- 而模板則取到視圖函數的數據結果進行展示(視圖展示方面)
- 代碼結構清晰,耦合度低
Jinja2
兩個概念:
- Jinja2:是 Python 下一個被廣泛應用的模板引擎,是由Python實現的模板語言,他的設計思想來源於 Django 的模板引擎,並擴展了其語法和一系列強大的功能,其是Flask內置的模板語言。
- 模板語言:是一種被設計來自動生成文檔的簡單文本格式,在模板語言中,一般都會把一些變量傳給模板,替換模板的特定位置上預先定義好的占位變量名。
渲染模版函數
- Flask提供的 render_template 函數封裝了該模板引擎
- render_template 函數的第一個參數是模板的文件名,后面的參數都是鍵值對,表示模板中變量對應的真實值。
使用
- {{}} 來表示變量名,這種 {{}} 語法叫做變量代碼塊
<h1>{{ post.title }}</h1>
Jinja2 模版中的變量代碼塊可以是任意 Python 類型或者對象,只要它能夠被 Python 的 str() 方法轉換為一個字符串就可以,比如,可以通過下面的方式顯示一個字典或者列表中的某個元素:
{{your_dict['key']}} {{your_list[0]}}
- 用 {%%} 定義的控制代碼塊,可以實現一些語言層次的功能,比如循環或者if語句
{% if user %}
{{ user }}
{% else %}
hello!
<ul> {% for index in indexs %} <li> {{ index }} </li> {% endfor %} </ul>
注釋
- 使用 {# #} 進行注釋,注釋的內容不會在html中被渲染出來
{# {{ name }} #}
模板的使用
- 在項目下創建
templates文件夾,用於存放所有的模板文件,並在目錄下創建一個模板html文件temp_demo1.html
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> 我的模板html內容 </body> </html>
- 設置 templates 文件夾屬性以便能夠在代碼中有智能提示
- 設置 html 中的模板語言jinjia2,以便在 html 有智能提示
- 創建視圖函數,將該模板內容進行渲染返回
@app.route('/') def index(): return render_template('temp_demo1.html')
訪問:http://127.0.0.1:5000/ 運行測試
- 代碼中傳入字符串,列表,字典到模板中
@app.route('/') def index(): # 往模板中傳入的數據 my_str = 'Hello flask' my_int = 10 my_array = [3, 4, 2, 1, 7, 9] my_dict = { 'name': 'xiaoming', 'age': 18 } return render_template('temp_demo1.html', my_str=my_str, my_int=my_int, my_array=my_array, my_dict=my_dict )
- 模板中代碼
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> 我的模板html內容 <br/>{{ my_str }} <br/>{{ my_int }} <br/>{{ my_array }} <br/>{{ my_dict }} </body> </html>
- 運行效果
<!DOCTYPE html>
我的模板html內容
Hello flask
10
[3, 4, 2, 1, 7, 9]
{'name': 'xiaoming', 'age': 18}
- 相關運算,取值
<br/> my_int + 10 的和為:{{ my_int + 10 }} <br/> my_int + my_array第0個值的和為:{{ my_int + my_array[0] }} <br/> my_array 第0個值為:{{ my_array[0] }} <br/> my_array 第1個值為:{{ my_array.1 }} <br/> my_dict 中 name 的值為:{{ my_dict['name'] }} <br/> my_dict 中 age 的值為:{{ my_dict.age }}
- 結果
my_int + 10 的和為:20
my_int + my_array第0個值的和為:13
my_array 第0個值為:3
my_array 第1個值為:4
my_dict 中 name 的值為:xiaoming
my_dict 中 age 的值為:18
過濾器
過濾器的本質就是函數。有時候我們不僅僅只是需要輸出變量的值,我們還需要修改變量的顯示,甚至格式化、運算等等,而在模板中是不能直接調用 Python 中的某些方法,那么這就用到了過濾器。
使用方式:
- 過濾器的使用方式為:變量名 | 過濾器。
{{variable | filter_name(*args)}}
- 如果沒有任何參數傳給過濾器,則可以把括號省略掉
{{variable | filter_name}}
- 如:``,這個過濾器的作用:把變量variable 的值的首字母轉換為大寫,其他字母轉換為小寫
鏈式調用
在 jinja2 中,過濾器是可以支持鏈式調用的,示例如下:
{{ "hello world" | reverse | upper }}
常見內建過濾器
字符串操作
- safe:禁用轉義
<p>{{ '<em>hello</em>' | safe }}</p>
- capitalize:把變量值的首字母轉成大寫,其余字母轉小寫
<p>{{ 'hello' | capitalize }}</p>
- lower:把值轉成小寫
<p>{{ 'HELLO' | lower }}</p>
- upper:把值轉成大寫
<p>{{ 'hello' | upper }}</p>
- title:把值中的每個單詞的首字母都轉成大寫
<p>{{ 'hello' | title }}</p>
- reverse:字符串反轉
<p>{{ 'olleh' | reverse }}</p>
- format:格式化輸出
<p>{{ '%s is %d' | format('name',17) }}</p>
- striptags:渲染之前把值中所有的HTML標簽都刪掉
<p>{{ '<em>hello</em>' | striptags }}</p>
- truncate: 字符串截斷
<p>{{ 'hello every one' | truncate(9)}}</p>
列表操作
- first:取第一個元素
<p>{{ [1,2,3,4,5,6] | first }}</p>
- last:取最后一個元素
<p>{{ [1,2,3,4,5,6] | last }}</p>
- length:獲取列表長度
<p>{{ [1,2,3,4,5,6] | length }}</p>
- sum:列表求和
<p>{{ [1,2,3,4,5,6] | sum }}</p>
- sort:列表排序
<p>{{ [6,2,3,1,5,4] | sort }}</p>
語句塊過濾
{% filter upper %} #一大堆文字# {% endfilter %}
自定義過濾器
過濾器的本質是函數。當模板內置的過濾器不能滿足需求,可以自定義過濾器。自定義過濾器有兩種實現方式:
- 一種是通過Flask應用對象的 add_template_filter 方法
- 通過裝飾器來實現自定義過濾器
重要:自定義的過濾器名稱如果和內置的過濾器重名,會覆蓋內置的過濾器。
需求:添加列表反轉的過濾器
方式一
通過調用應用程序實例的 add_template_filter 方法實現自定義過濾器。該方法第一個參數是函數名,第二個參數是自定義的過濾器名稱:
def do_listreverse(li): # 通過原列表創建一個新列表 temp_li = list(li) # 將新列表進行返轉 temp_li.reverse() return temp_li app.add_template_filter(do_listreverse,'lireverse')
方式二
用裝飾器來實現自定義過濾器。裝飾器傳入的參數是自定義的過濾器名稱。
@app.template_filter('lireverse') def do_listreverse(li): # 通過原列表創建一個新列表 temp_li = list(li) # 將新列表進行返轉 temp_li.reverse() return temp_li
- 在 html 中使用該自定義過濾器
<br/> my_array 原內容:{{ my_array }} <br/> my_array 反轉:{{ my_array | lireverse }}
- 運行結果
my_array 原內容:[3, 4, 2, 1, 7, 9] my_array 反轉:[9, 7, 1, 2, 4, 3]
控制代碼塊
控制代碼塊主要包含兩個:
- if/else if /else / endif
- for / endfor
if語句
Jinja2 語法中的if語句跟 Python 中的 if 語句相似,后面的布爾值或返回布爾值的表達式將決定代碼中的哪個流程會被執行:
{%if user.is_logged_in() %}
<a href='/logout'>Logout</a>
{% else %}
<a href='/login'>Login</a>
{% endif %}
過濾器可以被用在 if 語句中:
{% if comments | length > 0 %} There are {{ comments | length }} comments {% else %} There are no comments {% endif %}
循環
- 我們可以在 Jinja2 中使用循環來迭代任何列表或者生成器函數
{% for post in posts %}
<div>
<h1>{{ post.title }}</h1>
<p>{{ post.text | safe }}</p>
</div>
{% endfor %}
- 循環和if語句可以組合使用,以模擬 Python 循環中的 continue 功能,下面這個循環將只會渲染post.text不為None的那些post:
{% for post in posts if post.text %}
<div>
<h1>{{ post.title }}</h1>
<p>{{ post.text | safe }}</p>
</div>
{% endfor %}
- 在一個 for 循環塊中你可以訪問這些特殊的變量:
| 變量 | 描述 |
|---|---|
| loop.index | 當前循環迭代的次數(從 1 開始) |
| loop.index0 | 當前循環迭代的次數(從 0 開始) |
| loop.revindex | 到循環結束需要迭代的次數(從 1 開始) |
| loop.revindex0 | 到循環結束需要迭代的次數(從 0 開始) |
| loop.first | 如果是第一次迭代,為 True 。 |
| loop.last | 如果是最后一次迭代,為 True 。 |
| loop.length | 序列中的項目數。 |
| loop.cycle | 在一串序列間期取值的輔助函數。見下面示例程序。 |
-
在循環內部,你可以使用一個叫做loop的特殊變量來獲得關於for循環的一些信息
- 比如:要是我們想知道當前被迭代的元素序號,並模擬Python中的enumerate函數做的事情,則可以使用loop變量的index屬性,例如:
{% for post in posts%} {{loop.index}}, {{post.title}} {% endfor %}
- 會輸出這樣的結果
1, Post title 2, Second Post
- cycle函數會在每次循環的時候,返回其參數中的下一個元素,可以拿上面的例子來說明:
{% for post in posts%} {{loop.cycle('odd','even')}} {{post.title}} {% endfor %}
- 會輸出這樣的結果:
odd Post Title even Second Post
示例程序

- 准備數據
# 只顯示4行數據,背景顏色依次為:黃,綠,紅,紫 my_list = [ { "id": 1, "value": "我愛工作" }, { "id": 2, "value": "工作使人快樂" }, { "id": 3, "value": "沉迷於工作無法自拔" }, { "id": 4, "value": "日漸消瘦" }, { "id": 5, "value": "以夢為馬,越騎越傻" } ]
- 模板代碼
{% for item in my_list if item.id != 5 %}
{% if loop.index == 1 %}
<li style="{{ item.value }}</li> {% elif loop.index == 2 %} <li style="{{ item.value }}</li> {% elif loop.index == 3 %} <li style="{{ item.value }}</li> {% else %} <li style="{{ item.value }}</li> {% endif %} {% endfor %}
模板代碼復用
在模板中,可能會遇到以下情況:
- 多個模板具有完全相同的頂部和底部內容
- 多個模板中具有相同的模板代碼內容,但是內容中部分值不一樣
- 多個模板中具有完全相同的 html 代碼塊內容
像遇到這種情況,可以使用 JinJa2 模板中的 宏、繼承、包含來進行實現
宏
對宏(macro)的理解:
- 把它看作 Jinja2 中的一個函數,它會返回一個模板或者 HTML 字符串
- 為了避免反復地編寫同樣的模板代碼,出現代碼冗余,可以把他們寫成函數以進行重用
- 需要在多處重復使用的模板代碼片段可以寫入單獨的文件,再包含在所有模板中,以避免重復
使用
- 定義宏
{% macro input(name,value='',type='text') %}
<input type="{{type}}" name="{{name}}"
value="{{value}}" class="form-control">
{% endmacro %}
- 調用宏
{{ input('name' value='zs')}}
- 這會輸出
<input type="text" name="name"
value="zs" class="form-control">
- 把宏單獨抽取出來,封裝成html文件,其它模板中導入使用,文件名可以自定義macro.html
{% macro function(type='text', name='', value='') %}
<input type="{{type}}" name="{{name}}"
value="{{value}}" class="form-control">
{% endmacro %}
- 在其它模板文件中先導入,再調用
{% import 'macro.html' as func %} {% func.function() %}
代碼演練
- 使用宏之前代碼
<form> <label>用戶名:</label><input type="text" name="username"><br/> <label>身份證號:</label><input type="text" name="idcard"><br/> <label>密碼:</label><input type="password" name="password"><br/> <label>確認密碼:</label><input type="password" name="password2"><br/> <input type="submit" value="注冊"> </form>
- 定義宏
{#定義宏,相當於定義一個函數,在使用的時候直接調用該宏,傳入不同的參數就可以了#}
{% macro input(label="", type="text", name="", value="") %}
<label>{{ label }}</label><input type="{{ type }}" name="{{ name }}" value="{{ value }}"> {% endmacro %}
- 使用宏
<form> {{ input("用戶名:", name="username") }}<br/> {{ input("身份證號:", name="idcard") }}<br/> {{ input("密碼:", type="password", name="password") }}<br/> {{ input("確認密碼:", type="password", name="password2") }}<br/> {{ input(type="submit", value="注冊") }} </form>
模板繼承
模板繼承是為了重用模板中的公共內容。一般Web開發中,繼承主要使用在網站的頂部菜單、底部。這些內容可以定義在父模板中,子模板直接繼承,而不需要重復書寫。
- 標簽定義的內容
{% block top %} {% endblock %}
- 相當於在父模板中挖個坑,當子模板繼承父模板時,可以進行填充。
- 子模板使用 extends 指令聲明這個模板繼承自哪個模板
- 父模板中定義的塊在子模板中被重新定義,在子模板中調用父模板的內容可以使用super()
父模板
- base.html
{% block top %}
頂部菜單
{% endblock top %}
{% block content %}
{% endblock content %}
{% block bottom %}
底部
{% endblock bottom %}
子模板
- extends指令聲明這個模板繼承自哪
{% extends 'base.html' %} {% block content %} 需要填充的內容 {% endblock content %}
- 模板繼承使用時注意點:
- 不支持多繼承
- 為了便於閱讀,在子模板中使用extends時,盡量寫在模板的第一行。
- 不能在一個模板文件中定義多個相同名字的block標簽。
- 當在頁面中使用多個block標簽時,建議給結束標簽起個名字,當多個block嵌套時,閱讀性更好。
包含
Jinja2模板中,除了宏和繼承,還支持一種代碼重用的功能,叫包含(Include)。它的功能是將另一個模板整個加載到當前模板中,並直接渲染。
- include的使用
{% include 'hello.html' %}
包含在使用時,如果包含的模板文件不存在時,程序會拋出TemplateNotFound異常,可以加上 ignore missing 關鍵字。如果包含的模板文件不存在,會忽略這條include語句。
- include 的使用加上關鍵字ignore missing
{% include 'hello.html' ignore missing %}
小結
- 宏(Macro)、繼承(Block)、包含(include)均能實現代碼的復用。
- 繼承(Block)的本質是代碼替換,一般用來實現多個頁面中重復不變的區域。
- 宏(Macro)的功能類似函數,可以傳入參數,需要定義、調用。
- 包含(include)是直接將目標模板文件整個渲染出來。
模板中特有的變量和函數
你可以在自己的模板中訪問一些 Flask 默認內置的函數和對象
config
你可以從模板中直接訪問Flask當前的config對象:
{{config.SQLALCHEMY_DATABASE_URI}}
sqlite:///database.db
request
就是flask中代表當前請求的request對象:
{{request.url}}
http://127.0.0.1
session
為Flask的session對象
{{session.new}}
True
g變量
在視圖函數中設置g變量的 name 屬性的值,然后在模板中直接可以取出
{{ g.name }}
url_for()
url_for會根據傳入的路由器函數名,返回該路由對應的URL,在模板中始終使用url_for()就可以安全的修改路由綁定的URL,則不比擔心模板中渲染出錯的鏈接:
{{url_for('home')}} /
如果我們定義的路由URL是帶有參數的,則可以把它們作為關鍵字參數傳入url_for(),Flask會把他們填充進最終生成的URL中:
{{ url_for('post', post_id=1)}} /post/1
get_flashed_messages()
這個函數會返回之前在flask中通過flask()傳入的消息的列表,flash函數的作用很簡單,可以把由Python字符串表示的消息加入一個消息隊列中,再使用get_flashed_message()函數取出它們並消費掉:
{%for message in get_flashed_messages()%} {{message}} {%endfor%}
Web表單
Web 表單是 Web 應用程序的基本功能。
它是HTML頁面中負責數據采集的部件。表單有三個部分組成:表單標簽、表單域、表單按鈕。表單允許用戶輸入數據,負責HTML頁面數據采集,通過表單將用戶輸入的數據提交給服務器。
在Flask中,為了處理web表單,我們可以使用 Flask-WTF 擴展,它封裝了 WTForms,並且它有驗證表單數據的功能
WTForms支持的HTML標准字段
| 字段對象 | 說明 |
|---|---|
| StringField | 文本字段 |
| TextAreaField | 多行文本字段 |
| PasswordField | 密碼文本字段 |
| HiddenField | 隱藏文件字段 |
| DateField | 文本字段,值為 datetime.date 文本格式 |
| DateTimeField | 文本字段,值為 datetime.datetime 文本格式 |
| IntegerField | 文本字段,值為整數 |
| DecimalField | 文本字段,值為decimal.Decimal |
| FloatField | 文本字段,值為浮點數 |
| BooleanField | 復選框,值為True 和 False |
| RadioField | 一組單選框 |
| SelectField | 下拉列表 |
| SelectMutipleField | 下拉列表,可選擇多個值 |
| FileField | 文件上傳字段 |
| SubmitField | 表單提交按鈕 |
| FormField | 把表單作為字段嵌入另一個表單 |
| FieldList | 一組指定類型的字段 |
WTForms常用驗證函數
| 驗證函數 | 說明 |
|---|---|
| DataRequired | 確保字段中有數據 |
| EqualTo | 比較兩個字段的值,常用於比較兩次密碼輸入 |
| Length | 驗證輸入的字符串長度 |
| NumberRange | 驗證輸入的值在數字范圍內 |
| URL | 驗證URL |
| AnyOf | 驗證輸入值在可選列表中 |
| NoneOf | 驗證輸入值不在可選列表中 |
使用 Flask-WTF 需要配置參數 SECRET_KEY。
CSRF_ENABLED是為了CSRF(跨站請求偽造)保護。 SECRET_KEY用來生成加密令牌,當CSRF激活的時候,該設置會根據設置的密匙生成加密令牌。
代碼驗證
使用 html 自帶的表單
- 創建模板文件
login.html,在其中直接寫form表單:
<form method="post"> <label>用戶名:</label><input type="text" name="username" placeholder="請輸入用戶名"><br/> <label>密碼:</label><input type="password" name="password" placeholder="請輸入密碼"><br/> <label>確認密碼:</label><input type="password" name="password2" placeholder="請輸入確認密碼"><br/> <input type="submit" value="注冊"> </form> {% for message in get_flashed_messages() %} {{ message }} {% endfor %}
- 視圖函數中獲取表單數據驗證登錄邏輯:
@app.route('/demo1', methods=["get", "post"]) def demo1(): if request.method == "POST": # 取到表單中提交上來的三個參數 username = request.form.get("username") password = request.form.get("password") password2 = request.form.get("password2") if not all([username, password, password2]): # 向前端界面彈出一條提示(閃現消息) flash("參數不足") elif password != password2: flash("兩次密碼不一致") else: # 假裝做注冊操作 print(username, password, password2) return "success" return render_template('temp_register.html')
使用 Flask-WTF 實現表單
- 配置參數,關閉 CSRF 校驗
app.config['WTF_CSRF_ENABLED'] = False
CSRF:跨站請求偽造,后續會講到
模板頁面:
<form method="post"> {{ form.username.label }} {{ form.username }}<br/> {{ form.password.label }} {{ form.password }}<br/> {{ form.password2.label }} {{ form.password2 }}<br/> {{ form.submit }} </form>
視圖函數:
from flask import Flask,render_template, flash #導入wtf擴展的表單類 from flask_wtf import FlaskForm #導入自定義表單需要的字段 from wtforms import SubmitField,StringField,PasswordField #導入wtf擴展提供的表單驗證器 from wtforms.validators import DataRequired,EqualTo app = Flask(__name__) app.config['SECRET_KEY']='SECRET_KEY' #自定義表單類,文本字段、密碼字段、提交按鈕 class RegisterForm(FlaskForm): username = StringField("用戶名:", validators=[DataRequired("請輸入用戶名")], render_kw={"placeholder": "請輸入用戶名"}) password = PasswordField("密碼:", validators=[DataRequired("請輸入密碼")]) password2 = PasswordField("確認密碼:", validators=[DataRequired("請輸入確認密碼"), EqualTo("password", "兩次密碼不一致")]) submit = SubmitField("注冊") #定義根路由視圖函數,生成表單對象,獲取表單數據,進行表單數據驗證 @app.route('/demo2', methods=["get", "post"]) def demo2(): register_form = RegisterForm() # 驗證表單 if register_form.validate_on_submit(): # 如果代碼能走到這個地方,那么就代碼表單中所有的數據都能驗證成功 username = request.form.get("username") password = request.form.get("password") password2 = request.form.get("password2") # 假裝做注冊操作 print(username, password, password2) return "success" else: if request.method == "POST": flash("參數有誤或者不完整") return render_template('temp_register.html', form=register_form) if __name__ == '__main__': app.run(debug=True)
