2025年7月13日 星期日

Python 學習筆記 : 用 Flask 架站 (二) 模板與靜態檔案

這篇筆記大概是 2019 年開始學 Flask 時寫的, 寫了一半就擱著改學 Django 了. 最近因為在忙  Mapleboard 架站, 打算用 Flask app 作為 Line Bot webhook 後台, 所以就把 2023 年買的 "實戰 Python Flask 開發" 這本書找出來看, 順便將這篇老筆記寫完. 

本篇旨在測試 Flask 的 Jinja2 模板與靜態檔案用法. 

本系列之前的文章參考 :


參考書籍 : 
  1. 實戰 Python Flask 開發 (碁峰, 2022, 佐藤昌基等)
  2. 超圖解 Python 程式設計入門 (旗標, 2019,  趙英傑) 第 9 章
  3. Python 網頁框架超集合 (深智, 2022, 劉長銘) 第 8 章
模板檔案是在 htm/html 檔案中嵌入模板語法使用模板可以讓應用程式與網頁 UI 分離, 這也是 MTV (Model-Template-View) 架構的精神所在, 參考 :  



一. 建立模板與靜態檔案資料夾 : 

Flask 本身自帶 Jinja2 模板引擎, 首先必須在網站專案目錄底下建立一個 templates 目錄來存放模板網頁檔案; 其次要建立 static 子目錄來存放靜態檔案, 例如圖檔, 樣式檔 (.css) 與 Javascript 程式檔 (.js), 結構如下 : 

myapp 
      |___  app.py
      |___ static 
      |            |___ js 
      |            |          |___ app.js
      |            |___ css
      |            |          |___ style.css
      |            |___ images
      |                       |___ logo.ico
      |___  templates
                   |___ index.htm
                   |___ hello.htm
                   .....
 
模板網頁檔全部放在 templates 子目錄下; 靜態檔案如果不多就不用分 js, css, 與 images 子目錄, 全部放在 static 子目錄下即可.    


二. Jinja2 模板引擎語法 : 

Flask 是在 Werkzeug 開發伺服器與 Jinja2 範本引擎基礎上建立起來的, 而 Jinja2 又是從 Django 範本引擎發展而來, 效能更好, 其語法幾乎與 Django 模板語言雷同, 以下摘要整理 Jinja2 語法 : 


1. 模板語法的三元素 :

模板網頁中可以嵌入如下三種元素 : 變數, 標籤, 以及註解 : 


 模板語法元素 說明
 {{ 變數 }} 來自 views.py 的變數用來直接替換該位置內容
 {% 標籤 %} 標籤用來控制資料顯示邏輯 (判斷 & 迴圈)
 {# 註解 #} 註解會被模板引擎忽略



2. 分支 (判斷) 語法 :


 單一分支 雙重分支 多重分支
 {% if 條件式 %}
 輸出網頁
 (% endif %}
 {% if 條件式 %}
 輸出網頁1
 {% else %}
 輸出網頁2
 (% endif %}
 {% if 條件式 %}
 輸出網頁1
 {% elif %}
 輸出網頁2
 {% elif %}
 輸出網頁3
 {% else %}
 輸出網頁4
 (% endif %}


注意, endif 的 end 與 if 之間沒有空格, elif 的 el 與 if 之間也是, 若有空格會造成語法錯誤.


3. 迴圈語法 :

模板語言迴圈透過遍歷一個可迭代物件來輸出網頁, 其關鍵字包括 for, in, endfor, empty, 與 reversed 標籤, 語法如下 : 


 順序迭代 倒序迭代
 {% for 變數 in 可迭代變數 %}
 輸出網頁 1
 {% empty %}
 迴圈為空時之輸出網頁 2
 {% endfor %}
 {% for 變數 in 可迭代變數 reversed %}
 輸出網頁 1
 {% empty %}
 迴圈為空時之輸出網頁 2
 {% endfor %}


注意, empty 標籤必須放在 for 迴圈的最後面 (即 endfor 標籤前面), 當迴圈為空 (即可迭代變數元素個數為 0) 時, 網頁 1 將不會輸出, 而是輸出夾在 empty 與 endfor 標籤中間的網頁 2.

另外還有用來控制迴圈的 loop 物件屬性值 (Django 使用 forloop) :


 迴圈變數 loop 的屬性 說明
 loop.counter 迴圈計數器 (1 起始)
 loop.counter0 迴圈計數器 (0 起始)
 loop.revcounter 倒數之迴圈計數器 (由總圈數遞減至 1)
 loop.revcounter0 倒數之迴圈計數器 (由總圈數遞減至 0)
 loop.first 是否為第一個迴圈 (True/False)
 loop.last 是否為最後一個迴圈 (True/False)
 loop.parentloop 上一層迴圈的 loop 變數


4. 過濾器 (filter) :

過濾器其實就是模板引擎內建的函數, 用來在輸出變數前依據需要對變數內容或格式做修正或運算. 用法是在變數後面以管線符號 "|" 串接過濾器名稱, 可同時串接多個過濾器, 模板引擎會依照順序處理 :

{{ 變數 | 過濾器1 | 過濾器2  | 過濾器3 | ... }} 

常用的過濾器如下表 :


 常用模板過濾器 說明 範例
 add 加上一個數值或做字串串接 {{ var | add:"2" }} 
 addslashes 跳脫特殊字元 (前面加上倒斜線 \) {{ var | addslashes }} 
 capfirst 將字串變數的首字元轉為大寫 {{ var | capfirst }}
 center  將字串變數冠上指定空格後置中 {{ var | center: "5" }}
 cut 從字串變數中刪除指定子字串 {{ var | cut: " " }} 刪除空白字元
 date 將 datetime 變數以指定格式顯示 {{ var | date: "D d M Y" }}
 default 當變數為空字串時輸出預設字串 {{ var | default: "預設值" }}
 dictsort 將串列變數中的字典依指定鍵遞增排序 {{ var | dictsort: "鍵" }}
 dictsortreversed 將串列變數中的字典依指定鍵遞減排序 {{ var | dictsortreversed: "鍵" }}
 divisibleby 測試數值變數是否可被指定數整除 {{ var | divisibleby: "7" }} 
 escape 跳脫字串變數內的 HTML 標籤 {{ var | escape }}
 filesizeformat 以 KB/MB 等單位顯示檔案大小 {{ var | fileformat }}
 first 取出串列變數中的第一個元素 {{ var | first }}
 join 將串列變數中的元素以指定字串串接 {{ var | join: "," }} 
 last 取出串列變數中的最後一個元素 {{ var | last }} 
 length 傳回串列變數的元素個數 (長度) {{ var | length }} 
 length_is 測試變數的長度是否為指定之長度 {{ var | length_is: "5" }} 
 linebreaks 將字串中的 \n 轉成 HTML 之 br 與 p {{ var | linebreaks }} 
 linebreaksbr 將字串中的 \n 轉成 HTML 之 br {{ var | linebreaksbr }} 
 linenumbers 將顯示之文字加上行號 {{ var | linenumbers }} 
 ljust 將字串變數冠上指定空格後置左 {{ var | ljust: "5" }}
 lower 將字串變數轉成小寫 {{ var | lower }}
 make_list 將字串拆成字元串列 {{ var | make_list }}
 random 隨機取出串列中的一個元素 {{ var | random }}
 rjust 將字串變數冠上指定空格後置右 {{ var | rjust: "5" }} 
 safe 以 HTML 格式讀取字串 (不須跳脫) {{ var | safe }} 
 slice 取出字串中的指定切片 {{ var | slice:":2" }} 
 slugify 將字串中的空白以 dash '-' 取代 {{ var | slugify }} 
 stringformat 以科學表示法顯示數字 {{ var | stringformat: "E" }} 
 striptags 移除字串中的 HTML 標籤 {{ var | striptags }} 
 title 將字串中每個字的第一字元大寫 {{ var | title }} 
 truncatechars 將字串中超出指定長度部分以 ... 取代 {{ var | truncatechars:"5" }} 
 upper 將字串轉成大寫 {{ var | upper }} 
 wordcount 傳回字串中的字數 {{ var | wordcount }} 
 yesno 依變數值為 True/False/None 轉成指定值 {{ var | yesno:"是, 否, 取消" }} 


過濾器用法摘要如下 :
  1. wordcount 是計算 "字" 數而非 "字元" 數, 亦即以空格為單位做區隔, 例如 "您好嗎?" 的 wordcount 是 1; 而 "您 好 嗎 ?" 的 wordcount 是 4. 
  2. 若直接輸出變數 {{ var }}, 模板引擎會自動將 HTML 標籤的角括號轉成 > 與 <, 但可以用 safe 過濾器阻止自動轉換. 
  3. linebreaksbr 會將字串變數中的跳行字元 "\n" 改為 "<br>", 例如 "Hello\nWorld" 會被轉成 "Hello<br>World". 而 linebreaks 則除了做此轉換外還會在外面套上 p 標籤, 例如 "Hello\nWorld" 會被轉成 "<p>Hello<br>World</p>". 參考 :
    # https://docs.djangoproject.com/en/2.2/ref/templates/builtins/#date
  4. date 過濾器格式與 PHP 的極類似, 如果要製作 "2019-10-03 08:43:12" 這樣的輸出, 則其格式為 date:"Y-m-d H:i:s", 
  5. slice 的切片用法與 Python 字串或串列切片的用法一樣. 

三. 模板網頁的繼承 : 

因為 HTML 網頁頭尾都有重複的部分, 因此模板引擎提供 extends 標籤, 可以讓子模板網頁繼承父模板網頁以簡化網頁內容, 其概念與物件導向中物件的繼承類似. 模板可以連續繼承, 所以只有最上層父模板才具有形式上完整的 HTML 語法架構, 子模板透過繼承當然最終也是完整網頁, 只是形式上簡化了而已. 

在父模板中定義一個 (待填入) 區塊的語法為 :

{% block block_name %}{% endblock %}   

子模板會以具體的網頁片段填入 block 與 endblock 之間. 

子模板繼承父模板時使用 extends 標籤, 指令如下 :

{% extends "father_template.htm" %}

注意, extends 指令須放在模板網頁檔的第一行, 且父模板檔名需用引號括起來 (單引號或雙引號均可). 繼承父模板後就可以在子模板中以具體的網頁片段填入指定的區塊中 :

{% block block_name %} 網頁片段 {% endblock %}

最上層的父模板具有完整的網頁結構, 通常取名為 base.htm, 例如 :

<!--base.htm-->
<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width,initial-scale=1">
  <title>{% block title %}{% endblock %}</title>
  {% block link %}{% endblock %}
  {% block script %}{% endblock %}
  <style>
  {% block style %}{% endblock %}
  </style>
</head>
<body>
  {% block body %}{% endblock %}
</body>
</html>

此基礎父模板將 HTML 網頁中會變動的部分作成可嵌入之區塊, 主要是 head 裡面的 title, link, script, 以及 style, 以及 body 等五個元素. 繼承 base.htm 的子模版就可以寫成如下的 htm 檔 :

{% extends "base.htm" %}
{% block title %}
   網頁標題
{% endblock %}
{% block link %}
   link 元素群 (樣式檔)
{% endblock %}
{% block script %}
   script 元素群 (程式檔)
{% endblock %}
{% block body %}
   網頁內容 + script 元素群
{% endblock %}

如果要套用網頁框架專案, 例如 Bootstrap, 則其模版網頁可以寫成 : 

<!--bootstrap.htm-->
{% extends "base.htm" %}
{% block link %}
  <link rel="stylesheet" href="https://unpkg.com/bootstrap@3.3.7/dist/css/bootstrap.min.css">
  <link rel="stylesheet" href="https://unpkg.com/bootstrap@3.3.7/dist/css/bootstrap-theme.min.css">
  <link rel="stylesheet" href="https://unpkg.com/bootstrap-table@1.15.4/dist/bootstrap-table.min.css">
{% endblock %}
{% block script %}
  <script src="https://unpkg.com/jquery@3.4.1/dist/jquery.min.js"></script>
  <script src="https://unpkg.com/bootstrap@3.3.7/dist/js/bootstrap.min.js"></script>
  <script src="https://unpkg.com/bootstrap-table@1.15.4/dist/bootstrap-table.min.js"></script>
{% endblock %}

專案中的其他子模板就可以繼承此 boostrap.htm, 這樣整個專案就會有一致的 Bootstrap 風格了. 

參考 : 


沒有留言 :