FastAPI 教學 – 靜態檔案與模板(Static & Templates)
主題:模板參數傳遞
簡介
在 FastAPI 建立的 Web 應用程式中,除了提供 JSON API 之外,往往也需要產生 HTML 頁面給使用者瀏覽。這時 模板引擎(如 Jinja2)就扮演關鍵角色:它讓我們可以把資料動態塞入 HTML,產生客製化的畫面。
然而,僅僅會渲染模板還不夠,如何把資料正確、有效率地傳遞給模板 才是真正影響開發體驗與程式碼品質的重點。掌握參數傳遞的技巧,能讓你的頁面更具彈性、維護成本更低,也更容易與前端團隊協作。
本篇文章將從概念說明、實作範例、常見陷阱與最佳實踐,逐步帶你了解在 FastAPI 中 傳遞模板參數 的完整流程,適合剛接觸 FastAPI 的新手,也能為已有經驗的開發者提供實務參考。
核心概念
1️⃣ FastAPI 與 Jinja2 的基本整合
FastAPI 並未內建模板引擎,但提供了 Jinja2Templates 這個輔助類別,讓我們可以輕鬆掛載 Jinja2。基本步驟如下:
# main.py
from fastapi import FastAPI, Request
from fastapi.templating import Jinja2Templates
from fastapi.staticfiles import StaticFiles
app = FastAPI()
# 1️⃣ 設定靜態目錄(CSS、JS、圖片等)
app.mount("/static", StaticFiles(directory="static"), name="static")
# 2️⃣ 設定模板目錄
templates = Jinja2Templates(directory="templates")
StaticFiles用來提供 靜態資源,必須先掛載才能在 HTML 中使用{{ url_for('static', path='...') }}。Jinja2Templates只需要給予模板根目錄路徑,之後就能呼叫templates.TemplateResponse產生回應。
2️⃣ TemplateResponse 與 request 參數
TemplateResponse 必須接受 request 物件,這是 Jinja2 能正確產生 URL 反向解析(url_for)的前提。最簡單的範例:
# main.py(續)
@app.get("/", name="home")
async def read_home(request: Request):
"""
回傳 index.html,並傳遞一個簡單的字串變數。
"""
return templates.TemplateResponse(
"index.html",
{"request": request, "title": "歡迎使用 FastAPI"}
)
在 index.html 中,我們可以直接使用 {{ title }} 取得傳入的值。
3️⃣ 常見的資料型別傳遞方式
| 資料型別 | 範例說明 | 注意事項 |
|---|---|---|
| 字串 / 數字 | {"name": "Alice", "age": 30} |
直接傳遞即可 |
| 列表 (list) | {"items": ["蘋果", "香蕉", "櫻桃"]} |
在模板中使用 for 迴圈 |
| 字典 (dict) | {"user": {"username": "bob", "email": "bob@example.com"}} |
可在模板中使用點記法或索引 |
| Pydantic Model | User(name="Tom", age=25) |
需要先轉成 dict(model.dict())或使用 model 本身(Jinja2 支援屬性存取) |
| 自訂物件 | 自訂類別實例 | 確保屬性是公開的,避免使用私有屬性 (_xxx) |
程式碼範例
以下提供 5 個實用範例,涵蓋從最簡單的字串傳遞到複雜的物件與條件渲染,並附上詳細註解。
範例 1️⃣:傳遞單一字串與數字
# main.py
@app.get("/greeting", name="greeting")
async def greeting(request: Request):
"""
例子說明:將文字與數字傳入模板,展示在同一行。
"""
return templates.TemplateResponse(
"greeting.html",
{
"request": request,
"message": "哈囉,世界!",
"counter": 42
}
)
{# templates/greeting.html #}
<!DOCTYPE html>
<html lang="zh-Hant">
<head><title>Greeting</title></head>
<body>
<h1>{{ message }}</h1>
<p>目前計數值:{{ counter }}</p>
</body>
</html>
範例 2️⃣:傳遞列表與迴圈渲染
# main.py
@app.get("/fruits", name="fruits")
async def list_fruits(request: Request):
fruits = ["蘋果", "香蕉", "葡萄", "奇異果"]
return templates.TemplateResponse(
"fruits.html",
{"request": request, "fruits": fruits}
)
{# templates/fruits.html #}
<!DOCTYPE html>
<html lang="zh-Hant">
<head><title>水果清單</title></head>
<body>
<h2>我最喜歡的水果</h2>
<ul>
{% for fruit in fruits %}
<li>{{ fruit }}</li>
{% else %}
<li>目前尚無資料</li>
{% endfor %}
</ul>
</body>
</html>
小技巧:使用
{% else %}可以在列表為空時提供備案訊息,提升使用者體驗。
範例 3️⃣:傳遞字典或 Pydantic Model
# models.py
from pydantic import BaseModel
class User(BaseModel):
username: str
email: str
is_active: bool = True
# main.py
from models import User
@app.get("/profile/{username}", name="profile")
async def user_profile(request: Request, username: str):
# 假設從資料庫撈出以下資料
user = User(username=username, email=f"{username}@example.com")
return templates.TemplateResponse(
"profile.html",
{
"request": request,
"user": user # 直接傳遞 Pydantic Model
}
)
{# templates/profile.html #}
<!DOCTYPE html>
<html lang="zh-Hant">
<head><title>使用者資料</title></head>
<body>
<h1>使用者:{{ user.username }}</h1>
<p>電子信箱:{{ user.email }}</p>
<p>帳號狀態:{{ "啟用" if user.is_active else "停用" }}</p>
</body>
</html>
說明:Jinja2 能直接存取
BaseModel的屬性,無需額外dict()轉換。
範例 4️⃣:傳遞自訂物件與方法結果
# utils.py
class Calculator:
def __init__(self, a: int, b: int):
self.a = a
self.b = b
def add(self) -> int:
return self.a + self.b
def multiply(self) -> int:
return self.a * self.b
# main.py
from utils import Calculator
@app.get("/calc/{a}/{b}", name="calc")
async def calculate(request: Request, a: int, b: int):
calc = Calculator(a, b)
# 只傳遞需要的屬性或方法結果,避免在模板內做過多運算
return templates.TemplateResponse(
"calc.html",
{
"request": request,
"a": a,
"b": b,
"sum": calc.add(),
"product": calc.multiply()
}
)
{# templates/calc.html #}
<!DOCTYPE html>
<html lang="zh-Hant">
<head><title>計算結果</title></head>
<body>
<h2>計算 {{ a }} 與 {{ b }} 的結果</h2>
<p>加總:{{ sum }}</p>
<p>乘積:{{ product }}</p>
</body>
</html>
最佳實踐:盡量在路由函式內完成業務邏輯,只把最終結果傳給模板,避免在模板裡寫程式碼,保持「模板只負責呈現」的原則。
範例 5️⃣:結合靜態檔案、條件渲染與全域變數
# main.py
from fastapi import Depends
# 假設有一個全域設定,用於控制是否顯示測試訊息
def get_show_debug():
return True # 在真實環境會從設定檔或環境變數取得
@app.get("/dashboard", name="dashboard")
async def dashboard(request: Request, show_debug: bool = Depends(get_show_debug)):
user = {"name": "Alice", "role": "admin"}
notifications = [
{"type": "info", "msg": "系統維護將於今晚 12:00 開始"},
{"type": "warning", "msg": "磁碟空間不足"},
]
return templates.TemplateResponse(
"dashboard.html",
{
"request": request,
"user": user,
"notifications": notifications,
"show_debug": show_debug
}
)
{# templates/dashboard.html #}
<!DOCTYPE html>
<html lang="zh-Hant">
<head>
<meta charset="UTF-8">
<title>Dashboard</title>
<link rel="stylesheet" href="{{ url_for('static', path='css/style.css') }}">
</head>
<body>
<h1>歡迎,{{ user.name }}!</h1>
{% if notifications %}
<section class="noti">
{% for n in notifications %}
<div class="alert {{ n.type }}">{{ n.msg }}</div>
{% endfor %}
</section>
{% endif %}
{% if show_debug %}
<footer>
<small>Debug mode is <strong>ON</strong></small>
</footer>
{% endif %}
</body>
</html>
重點:
- 使用
Depends注入全域變數或設定。url_for('static', path='...')只能在傳入request後正確解析。- 透過條件渲染(
{% if %})控制 UI 顯示,避免前端硬編碼。
常見陷阱與最佳實踐
| 陷阱 | 說明 | 解決方案 / 最佳實踐 |
|---|---|---|
忘記傳入 request |
TemplateResponse 若缺少 request,url_for 會失效,導致靜態資源 404。 |
必須在 context 中加入 {"request": request}。 |
| 直接傳遞大型物件 | 把整個 ORM 實例或大量資料一次傳入,會增加序列化與記憶體負擔。 | 在路由層 先過濾、轉換 為必要欄位(如 model.dict()),或使用 Pydantic schema。 |
| 模板內寫過多商業邏輯 | 會降低可讀性,難以測試。 | 保持模板只負責呈現,所有計算與判斷留在 Python 端。 |
| 使用可變預設參數 | 如 def func(data: list = []),多個請求會共用同一個列表。 |
使用 None 為預設值,並在函式內初始化。 |
| 路徑穿越 (Path Traversal) | 如果直接使用使用者提供的檔名作為模板名稱,可能被惡意讀取其他檔案。 | 嚴格驗證或 白名單 模板名稱,絕不直接拼接未經過濾的字串。 |
未設定 static 路徑 |
靜態檔案無法被載入,前端樣式、腳本失效。 | 確認 app.mount("/static", StaticFiles(...)) 已正確執行,且目錄結構與 templates 同層或自行調整。 |
其他最佳實踐
使用
BaseModel作為模板資料的結構- 定義一個
ResponseModel,保證傳入的欄位與型別一致,減少跑時錯誤。
- 定義一個
分層管理模板
- 在
templates/內依功能建立子目錄(如users/,admin/),避免檔名衝突。
- 在
全域變數與環境設定
- 利用 Dependency Injection(
Depends)提供settings、debug、current_user等全域資訊,讓模板可以直接使用。
- 利用 Dependency Injection(
快取模板
- Jinja2 內建快取機制,對於不變的模板可以設定
loader=FileSystemLoader("templates", followlinks=True)並啟用auto_reload=False,提升效能。
- Jinja2 內建快取機制,對於不變的模板可以設定
安全性
- 預設 Jinja2 會自動 HTML 轉義,但若使用
{{ variable|safe }}必須確保內容已經過信任或淨化,避免 XSS。
- 預設 Jinja2 會自動 HTML 轉義,但若使用
實際應用場景
| 場景 | 為什麼需要模板參數傳遞? | 範例實作 |
|---|---|---|
| 使用者個人化儀表板 | 每位使用者的資料、通知、權限都不同,需要在同一個 HTML 中動態呈現。 | 參考 範例 5,使用 Depends 注入使用者資訊與全域設定。 |
| 商品列表與分頁 | 從資料庫撈出分頁結果,需將 items、page、total_pages 等資訊傳給前端。 |
在路由中計算分頁資訊,僅傳遞必要欄位給 list.html。 |
| 表單驗證錯誤回傳 | 表單提交失敗時,需要把錯誤訊息與原始輸入值一起回傳,以免使用者重新填寫。 | 使用 Form 取得資料,驗證失敗時 TemplateResponse 中帶入 errors 與 old_data。 |
| 多語系網站 | 依使用者語系載入不同的文字資源或日期格式。 | 在 request.state.locale 中保存語系,透過模板過濾器 (`{{ date |
| 即時聊天或通知面板 | 需要在頁面渲染時注入 WebSocket URL、使用者 ID 等參數。 | 在 dashboard.html 中使用 {{ ws_url }}、{{ user_id }},由路由一次性提供。 |
總結
在 FastAPI 中,模板參數傳遞 是將後端資料與前端呈現緊密結合的關鍵環節。透過 Jinja2Templates、TemplateResponse 與 request 的正確配合,我們可以:
- 安全且彈性 地將字串、列表、字典、Pydantic Model 甚至自訂物件傳入模板。
- 保持模板乾淨:所有商業邏輯與資料處理應在路由層完成,模板只負責 UI 呈現。
- 避免常見陷阱:忘記傳
request、直接使用可變預設參數、在模板內寫過多程式碼等,都會影響效能與安全性。 - 運用 Dependency Injection 提供全域設定、使用者資訊或環境變數,使模板更具可測試性與可維護性。
掌握以上概念與實務技巧後,你就能在 FastAPI 專案中快速構建 動態、可維護且安全 的網頁介面,從簡單的歡迎頁面到複雜的管理後台,都能游刃有餘。祝開發順利,期待看到你用 FastAPI 打造出更多精彩的 Web 應用! 🚀