昨天我們用 Jinja2 創造了一個網頁模板,讓我們可以快速地寫出一系列的外觀相似的網頁。但其實 Jinja2 的強大之處可不只於此。讓我們先看一下 Jinja2 官方說明手冊➀提供的範例:
<title>{% block title %}{% endblock %}</title>
<ul>
{% for user in users %}
<li><a href="{{ user.url }}">{{ user.username }}</a></li>
{% endfor %}
</ul>
第一行:<title>{% block title %}{% endblock %}</title>
這就是我們昨天學到的,在<title>
的標籤當中,挖一個洞,讓套用此模板的不同網頁可以填入不同的內容。
第二行:<ul>
這我看得懂,利用<ul>
標籤宣告一個無序列清單(unordered list)的開始。
第三行開始到第五行結束,官方說明文件向我們展示了 Jinja2 另一個強大之處:就是在 HTML 5 的檔案當中,運用類似 Python 的語法,快速的寫出 HTML 5 程式碼。
第三行:{% for user in users %}
就像是在 Python 當中,我們會用for 項目 in 可數(ˇ)數(ˋ)清單:
一樣,開啟了一個for
迴圈。
第四行: <li><a href="{{ user.url }}">{{ user.username }}</a></li>
隨著for
迴圈的進行,而一行一行的在 HTML 5 檔案中加入這行程式碼。其中,{{ user.url }}
跟{{ user.username }}
是變數,隨著每次迴圈的不同而改變。在這個範例當中,user
應該是一個 Python 的字典物件,而url
、username
則是這個字典物件的鑰匙,透過{{ user.url }}
、{{ user.username }}
將內容傳到 HTML 5 的檔案當中。
第五行:{% endfor %}
結束這個for
迴圈。在 Python 中,程式會依照縮排來判斷迴圈的範圍。但在 HTML 5 當中,縮排不重要,因此需要這一行宣告for
迴圈的範圍。就像 HTML 5 的標籤是成對使用一樣<tag></tag>
,一個宣告開始,另一個宣告結束。
用實際一點的例子再說明一次好了,假設我們手邊的 Python 檔案中有如下資料:
users = [{"username": "我", "url": "我-APP-的名字.herokuapp.com"},
{"username": "你", "url": "你-APP-的名字.herokuapp.com"}]
而我們在 HTML 的檔案中寫下:
<ul>
{% for user in users %}
<li><a href="{{ user.url }}">{{ user.username }}</a></li>
{% endfor %}
</ul>
透過 Flask 的render_template()
將可數(ˇ)數(ˋ)物件users
從 Python 的檔案中傳送到 HTML 5 檔案:
@app.route("/jinja2_example")
def jinja2_example():
return render_template("jinja2_example.html", users=users)
得到user
物件的 HTML 5 檔案,在 Jinja2 的作用之下,就可以寫出如下方的 HTML 5程式碼:
<ul>
<li><a href="我-APP-的名字.herokuapp.com">我</a></li>
<li><a href="你-APP-的名字.herokuapp.com">你</a></li>
</ul>
怎麼樣,Jinja2 很厲害吧。有些時候我們根本不可能一個一個在 HTML 5 的檔案中將所有的程式碼都寫出來,這個時候 Jinja2 就是個最棒的幫手了。具體來說是什麼時候呢?比如說我們想要查詢草泥馬的訓練紀錄的時候。我們在第 18 天的時候有提到,藉由 psycopg2 的幫助,我們可以請貼心的 LINE 聊天機器人幫忙查詢草泥馬的訓練紀錄。不過,受限於介面的關係,當訓練紀錄一多了以後,在 LINE 的聊天室窗中觀看密密麻麻的資料實在不是太好的使用者體驗。
現在,我們就利用 psycopg2 跟 Jinja2 這兩樣強大的工具,讓我們可以在網頁裡愜意的欣賞這一筆一筆的訓練資料吧!
一樣,從 Python 當中藉由 psycopg2 寫一個查資料的函數:
def web_select_overall():
DATABASE_URL = os.environ['DATABASE_URL']
conn = psycopg2.connect(DATABASE_URL, sslmode='require')
cursor = conn.cursor()
postgres_select_query = f"""SELECT * FROM alpaca_training ORDER BY record_no;"""
cursor.execute(postgres_select_query)
message = []
while True:
temp = cursor.fetchmany(10)
if temp:
message.extend(temp)
else:
break
cursor.close()
conn.close()
return message
接著,在 flask 中做出一個路由:
@app.route("/show_records")
def show_records():
python_records = web_select_overall()
return render_template("show_records.html", html_records=python_records)
意思是說,當有使用者請求"你-APP-的名字.herokuapp.com/show_records" 的時候,show_records()
這個函數會執行,並呼叫我們剛才寫好的web_select_overall()
,將資料庫中的表格alpaca_training
所有訓練紀錄都調出來,並貼上records
的標籤。最後,在render_template()
的地方,把python_records
傳給show_records.html
這個 HTML 5 檔案中的變數html_records
,經過 Jinja2 的詮釋,把完整的"show_records.html"傳給請求網頁的使用者。
大概有概念了嗎?那麼最後我們要來寫的就是"show_records.html"
:
{% extends "base.html" %}
{% block title %}賴田捕手:訓練紀錄{% endblock %}
{% block main %}
<div class="container">
<div class="row align-items-start">
<div class="col">
<h1>訓練紀錄</h1>
</div>
<!-- 將所有草泥馬的訓練紀錄列在下面 -->
{% for record in html_records %}
<div class="row">
<div class="col">
<p>{{ record[0] }}<p>
</div>
<div class="col">
<p>{{ record[1] }}<p>
</div>
<div class="col">
<p>{{ record[2] }}<p>
</div>
<div class="col">
<p>{{ record[3] }}<p>
</div>
<div class="col">
<p>{{ record[4] }}<p>
</div>
</div>
{% endfor %}
<!-- 將所有草泥馬的訓練紀錄列在上面 -->
</div>
{% endblock %}
上面那段程式碼,我利用{% for record in html_record %}
,將html_record
當中的項目(不可變更清單物件,tuple)一個一個取出來,接著,在執行每一個for
迴圈中,將不可變清單當中的 5 個項目,分別是record[0]
、record[1]
、record[2]
、record[3]
、record[4]
,一個一個放到指定的位置上。如此,我們就可以利用 "你-APP-的名字.herokuapp.com/show_records" 這個網頁,查詢所有草泥馬的訓練資料囉。
圖一、草泥馬訓練全紀錄
開始對於 Jinja2 有點熟悉的你,是不是已經在猜想,上面那段用 Jinja2 寫出來的程式碼,是不是還可以再更簡單一些呢?讓我們來試試:
{% extends "base.html" %}
{% block title %}賴田捕手:訓練紀錄{% endblock %}
{% block main %}
<div class="container">
<div class="row align-items-start">
<div class="col">
<h1>訓練紀錄</h1>
</div>
<!-- 將所有草泥馬的訓練紀錄列在下面 -->
{% for record in html_records %}
<div class="row">
{% for item in record %}
<div class="col">
<p>{{ item }}<p>
</div>
{% endfor %}
</div>
{% endfor %}
<!-- 將所有草泥馬的訓練紀錄列在上面 -->
</div>
{% endblock %}
太棒了,真的可以!
這邊為了詳細說明 Jinja2 在 HTML 5 檔案中的運作,所以列出了所有草泥馬訓練紀錄的網頁上,除了一開始斗大的的「訓練紀錄」之外,我沒有再特別加上其他說明文字。但是實際觀賞起來,沒有欄位的說明,看起來果然還是很怪。那就讓我們試著加上欄位說明,順便見識 Jinja2 究竟能讓我們有多偷懶吧。
{% macro easy_row(data, tag) -%}
{% for record in data %}
<div class="row">
{% for item in record %}
<div class="col">
<{{ tag }}>{{ item }}</{{ tag }}>
</div>
{% endfor %}
</div>
{% endfor %}
{%- endmacro %}
首先,我們動用了一個在 Jinja2 裡面叫做巨集(macro)的東西,其實就可以類比成 Python 裡面的函數。當我們發現同一段程式碼會不停的在我們的網頁中重複出現時,就可以考慮使用巨集。而跟 Python 的函數一樣,在創造一個函數時,需要先用
def 函數的名稱(函數的參數):
來做宣告。Jinja2 在創造巨集時,則需使用
{% macro 巨集的名稱(巨集的參數) -%}
來開頭,並在巨集結束的地方,用
{%- endmacro %}
當作結尾。
完整地來看一看怎麼使用巨集吧:
<!-- 宣告一個巨集 -->
{% macro easy_row(data, tag) -%}
{% for record in data %}
<div class="row">
{% for item in record %}
<div class="col">
<{{ tag }}>{{ item }}</{{ tag }}>
</div>
{% endfor %}
</div>
{% endfor %}
{%- endmacro %}
<!-- 宣告一些等等會用來創造欄位說明的變數 -->
{% set col_names = (("record_no", "alpaca_name", "training", "duration", "date"),) %}
<!-- 用巨集快速創造欄位名稱 -->
{{ easy_row(col_names, "h2") }}
<!-- 將所有草泥馬的訓練紀錄列在下面 -->
{{ easy_row(html_records, "p") }}
首先我們宣告一個巨集easy_row()
,它會接受兩個參數data
跟tag
。data
是用來填入欄位的數據,而tag
則是用來控制每一列文字的格式。接著,為了填入一列欄位說明的資料,我在 HTML 5 的檔案中創造了一個變數col_names
,就像在 Python 中創造變數的方法類似,不過必須多一個set
開頭:
{% set 變數名稱 = 變數內容 %}
把創造的代表欄位名稱的變數col_names
丟進easy_row()
巨集當中,並且希望欄位名稱用<h2>
作為格式。結果如圖二。
圖二、利用巨集快速的寫出漂亮的表格
天啊,簡單不簡單,只靠短短幾行程式碼就造就了如此漂亮的表格,你能不喜歡 Jinja2 嗎?
今天關於 Jinja2 的介紹就差不多到這裡了。除了今天介紹的{% for %}{% endfor %}
、{% macro -%}{%- endmacro %}
、{% set %}
等等,Jinja2 也提供{% if %}{% endif %}
這類的判斷句。有興趣的可以參考這些網路上的資源➁➂,包括介紹十分詳細用心的 Jinja2 官方使用手冊➀。今天用到的程式碼我也會放在 Github 上,大家有興趣的可以上去參考看看,謝謝大家。另外,如果覺得內容有哪些地方解釋得不夠清楚,或是想更加深入討論的,也非常歡迎大家在文章下面留言,我會盡量回覆的!祝大家有個愉快的夜晚囉!
➀ Jinja2 官方說明手冊
➁ Flask pythonise Jinja 變數傳送
➂ Flask Jinja2 讀書筆記
註:對於此系列文有興趣的讀者,歡迎參考由此系列文擴編成書的 LINE Bot by Python,以及最新的系列文《賴田捕手:追加篇》
第 31 天 初始化 LINE BOT on Heroku
第 32 天 快速回覆 QuickReply 介紹
第 33 天 妥善運用 Heroku APP 暫存空間
第 34 天 妥善運用 LINE Notify 免費推播
第 35 天 製造 Deploy to Heroku 按鈕