0%

Flask 筆記 | 樣板引擎

前面已經提到過,後端回應前端的方式包含字串、JSON、重新導向,而最後一個則是使用樣板(或稱為模板)。樣板引擎是一種把 Python 資料變成 HTML 頁面的工具。

Flask 預設使用的樣板引擎叫 Jinja2,可以讓我們:

  • 將變數嵌入 HTML 裡
  • 加入控制語句(if、for)
  • 抽出重複的區塊(如頁首、頁尾)
  • 讓程式與畫面分離

而樣板的檔案形式可以有很多種,從純文字到 HTML 檔皆可作為樣板。至於為何要使用樣板,從名字便可知一二:樣板提供網頁設計者僅針對需要動態修改的地方做更動,其他重複的內容一律都是樣板。

渲染樣板

建立樣板的方式很簡單,僅需在 app.py 同層(根目錄)中建立 templates 資料夾,將樣板檔案放在裡面即可完成前置作業。接著,需要在 Python 前面從 Flask 引入 render_template 函數:

1
from flask import render_template

接著即可開始使用,語法為:

1
2
3
4
@app.route("路徑")
def 處理函式名稱:
處理邏輯
return render_template(樣板檔案名稱)

純文字處理

我們在 templates 中新增一個純文字檔 greet.txt

1
您好,歡迎光臨

/greet 路徑如下使用:

1
2
3
@app.route("/greet")
def greet():
return render_template("greet.txt")

執行後打開網站進入 http://127.0.0.1:5000/greet 即可看到結果:

使用樣板渲染

圖 1:使用樣板渲染

傳入資料

為了傳入資料至樣板中,我們使用佔位符號告訴 Jinja2 這塊是需要特別處理的:

1
{{ 變數名 }}

例如我們在 templates 新增 greet.html 這份檔案,然後撰寫以下內容:

1
2
3
4
5
6
7
8
9
10
11
12
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Flask 網站</title>
</head>
<body>
<h1>您好,{{ name }},歡迎光臨!</h1>

<p>這是樣板的內容</p>
</body>
</html>

其中 {{ name }} 可以允許程式碼將變數傳入:

1
2
3
@app.route("/greet")
def greet():
return render_template("greet.html", name="Anthony")

執行後即可得到以下結果:

傳入變數至樣板

圖 2:傳入變數至樣板

樣板繼承

在一個網站中,許多網頁會有相同的區塊,例如頁首、導覽列、頁尾等。若每個樣板都要重複寫這些內容,不但麻煩也容易出錯。為了解決這個問題,Flask 的 Jinja2 提供樣板繼承機制,只需定義一次這些「框架」,其餘頁面可以繼承這個樣板並只填入各自不同的部分。簡單來說,就是建立一份「父樣板」(如 base.html),再讓其他樣板(如 index.html)繼承這份父樣板並改寫指定區塊。

建立父樣板

templates 資料夾中建立 base.html,作為整個網站的框架:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>{% block title %}My Website{% endblock %}</title>
</head>
<body>
<header>
<h1>我的網站</h1>
</header>

<main>
{% block content %}
<!-- 子樣板會覆寫這裡的內容 -->
{% endblock %}
</main>

<footer>
<p>版權所有 © 2025</p>
</footer>
</body>
</html>

建立子樣板

接著,我們建立一個子樣板 index.html,用來繼承 base.html 並改寫部分內容:

1
2
3
4
5
6
7
8
{% extends "base.html" %}

{% block title %}首頁{% endblock %}

{% block content %}
<h2>歡迎來到首頁!</h2>
<p>這是主內容區。</p>
{% endblock %}

這裡使用:

  • {% extends "base.html" %}:表示這份樣板會繼承父樣板
  • {% block title %}{% block content %}:覆寫父樣板對應的區塊

搭配 Python 使用

app.py 中對應的路由會寫成:

1
2
3
@app.route("/")
def home():
return render_template("index.html")
樣板繼承

圖 3:樣板繼承

打開網站首頁時,Flask 會自動渲染 index.html,再由 Jinja2 把它合併進 base.html 的框架裡,最後變成一個完整的 HTML 頁面。因此整體架構可以表示如下:

1
2
3
4
5
templates/

├── base.html ← 所有頁面共同框架
├── index.html ← 首頁內容,繼承 base.html
├── about.html ← 關於頁面,也可繼承 base.html

此外請注意:

  • 所有 {% block xxx %} 的名稱必須與父樣板相同,才能成功覆寫
  • 如果子樣板中沒有覆寫某個區塊,就會保留父樣板的預設內容

控制結構

在樣板中,我們經常需要根據條件顯示特定內容,或根據資料重複顯示一段 HTML。Jinja2 提供了常見的控制語法,包括 if 條件語句與 for 迴圈語句。這些語法都以 {% ... %} 包裹,不能用 {{ ... }},因為它們不會顯示值,而是負責控制流程

if 條件語句

我們可以根據變數是否存在、是否為特定值,來決定是否顯示某些區塊:

1
2
3
4
5
{% if user %}
<p>歡迎,{{ user }}!</p>
{% else %}
<p>請先登入。</p>
{% endif %}

在 Python 中傳入 user 變數:

1
2
3
@app.route("/profile")
def profile():
return render_template("profile.html", user="Anthony")

執行後會顯示:

控制語句-if

圖 4:控制語句-if (1)

若改為 user=None,則會顯示:

控制語句-if

圖 5:控制語句-if (2)

for 迴圈語句

當你有一個清單時,可以使用 for 來一一列出清單中的內容。例如在樣板中列出購物車的商品:

1
2
3
4
5
6
<h2>購物車內容</h2>
<ul>
{% for item in items %}
<li>{{ item }}</li>
{% endfor %}
</ul>

對應的路由程式碼如下:

1
2
3
@app.route("/cart")
def cart():
return render_template("cart.html", items=["牛奶", "麵包", "蛋"])

畫面顯示為:

控制語句-for

圖 6:控制語句-for (1)

也可以使用 loop.indexloop.last 等內建變數來取得當前迴圈資訊:

1
2
3
4
5
6
<h2>購物車內容</h2>
<ul>
{% for item in items %}
<p>{{ loop.index }}. {{ item }}</p>
{% endfor %}
</ul>

畫面就會顯示當前 item 的索引與值:

控制語句-for

圖 7:控制語句-for (2)

過濾器

在樣板中,我們有時希望對變數做一些小修改再顯示,例如:將字母變大寫、將數字四捨五入。Jinja2 提供了許多**過濾器(filter)**來做這類處理,語法為:

1
{{ 變數名 | 過濾器名稱 }}

這裡的 | 稱為管線符號 (pipe),意思是把變數送進這個過濾器裡做處理。

過濾器名稱 功能說明 範例
capitalize 首字母大寫 {{ name | capitalize }}
upper 全部轉大寫 {{ name | upper }}
lower 全部轉小寫 {{ name | lower }}
title 每個單字首字母大寫 {{ name | title }}
length 回傳長度(字數、項目數) {{ items | length }}
round(2) 四捨五入到小數點後 2 位 {{ price | round(2) }}

單一過濾器

我們可以格式化使用者輸入:

1
2
<h1>歡迎,{{ name | capitalize }}</h1>
<p>您目前的信用額度為:{{ credit | round(2) }} 元</p>

對應的路由寫法為:

1
2
3
@app.route("/welcome")
def welcome():
return render_template("welcome.html", name="aNthoNy", credit=5234.6789)

結果顯示:

單一過濾器

圖 8:單一過濾器

多重過濾器

如果需要將變數進行多重處理,可以連用多個過濾器,例如先變大寫再加上長度:

1
{{ name | upper | length }}

這樣就會將 name 全部轉為大寫後,取得字串長度。

自訂過濾器

除了使用 Jinja2 內建的過濾器外,我們也可以根據需求自己定義過濾器函式,並註冊到樣板引擎中。這個功能特別適合處理有客製化格式的輸出,例如文字轉換、特殊字串處理、資料替換等。在 Flask 中,自訂過濾器的流程包含兩個步驟:

  1. 撰寫 Python 處理邏輯(過濾器函式)
  2. 將它註冊到 Jinja2 樣板環境

例如我們想要實作反轉字串,讓樣板能夠使用 reverse 過濾器,把輸入的文字顛倒顯示。例如 hello 變成 olleh。首先在 Python 中定義這個函式:

1
2
def reverse_string(s):
return s[::-1]

接著註冊這個函式為一個過濾器名稱:

1
app.jinja_env.filters["reverse"] = reverse_string

這樣之後在任何樣板中就可以使用 | reverse

1
<p>{{ name | reverse }}</p>

例如我們可以在 /reverse 這個路徑中傳入 name 將其反轉:

1
2
3
@app.route("/reverse")
def show_reverse():
return render_template("reverse.html", name="Anthony")

執行後畫面就會顯示反轉後的結果 ynohtnA

自訂過濾器

圖 9:自訂過濾器

樣板復用

當我們的網站越做越大時,樣板的結構也會跟著變得複雜。如果每個頁面都手動重複寫 HTML 區塊(像按鈕、表格、卡片),不但麻煩,也容易出錯。

這時,我們可以利用:

  • 樣板引用 (include):把一段樣板拆成獨立檔案,在其他樣板中「插進來」
  • 樣板函式 (marcro):定義可以重複使用的「樣板函式」,就像 Python 的函式一樣

這兩種工具能讓我們的樣板更模組化、乾淨、好維護

樣板引用

例如我們可以把一段 HTML 存成獨立檔案(例如 header.html),然後在其他樣板中這樣插入:

1
{% include "模板名稱.html" %}

舉例來說,假設我們已經寫好頁首與頁尾的 HTML 檔案,只要在其他 HTML 檔案中套用上面的語法即可:

1
2
3
4
5
6
7
<body>
{% include "navbar.html" %}
<main>
{% block content %}{% endblock %}
</main>
{% include "footer.html" %}
</body>

日後要改頁首與頁尾,只需要改一個檔案,全部樣板都會更新。

樣板函式

如果你有一段重複出現但會變化的 HTML,可以寫成樣板函式,就像 Python 裡定義函式一樣。例如我們定義以下的樣板函式 render_item,當傳入 item 後,就會顯示品項標題與敘述:

1
2
3
4
5
6
{% macro render_item(item) %}
<div class="card">
<h3>{{ item.title }}</h3>
<p>{{ item.description }}</p>
</div>
{% endmacro %}

然後在其他樣板中使用:

1
2
3
4
5
6
{% from "macro.html" import render_item %}

<h1>商品清單</h1>
{% for product in products %}
{{ render_item(product) }}
{% endfor %}

在前面的購物車清單中,我們修改以下寫法:

1
2
3
4
5
6
7
8
@app.route("/cart")
def cart():
products = [
{"title": "牛奶", "description": "高品質鮮奶"},
{"title": "麵包", "description": "剛出爐的法國麵包"},
{"title": "蛋", "description": "放山雞有機雞蛋"}
]
return render_template("cart.html", products=products)

執行後畫面就會顯示品項與敘述了!

樣板函式

圖 10:樣板函式

這樣不管要渲染幾筆商品資料,都只要呼叫一次 render_item(),樣板會自動插好 HTML。

模板上下文

當我們使用 render_template() 時,會把某些變數從 Python 傳進樣板中,這些變數就會成為樣板的上下文 (context)。也就是說,樣板可以直接讀取你提供的變數,並根據它們的值來產生動態的 HTML。這個過程類似於 Python 函式呼叫時的參數傳遞,只是這裡是從後端傳給前端的樣板頁面。例如我們可以傳入一個字典物件:

1
2
3
4
5
6
7
@app.route("/profile/<username>")
def profile(username):
user = {
"name": username,
"age": 25
}
return render_template("profile.html", user=user)

這裡我們建立了一個名為 user 的字典物件,裡面包含兩個欄位:nameage,並將這個 user 傳入樣板中。然後在 profile.html 中這樣寫:

1
2
<h1>{{ user.name }}</h1>
<p>年齡:{{ user.age }}</p>

這裡 {{ user.name }} 就代表從 Python 傳進來的 user["name"],而 user.age 是存取字典的 age 欄位。

模板上下文

圖 11:模板上下文

注意:Jinja2 支援字典用 變數.屬性 的方式存取,當然也可以寫成 變數["name"],但大多數情況下會偏好前者,語意上更像物件。