本文 AI 產出,尚未審核

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️⃣ TemplateResponserequest 參數

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) 需要先轉成 dictmodel.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>

重點

  1. 使用 Depends 注入全域變數或設定。
  2. url_for('static', path='...') 只能在傳入 request 後正確解析。
  3. 透過條件渲染({% if %})控制 UI 顯示,避免前端硬編碼。

常見陷阱與最佳實踐

陷阱 說明 解決方案 / 最佳實踐
忘記傳入 request TemplateResponse 若缺少 requesturl_for 會失效,導致靜態資源 404。 必須context 中加入 {"request": request}
直接傳遞大型物件 把整個 ORM 實例或大量資料一次傳入,會增加序列化與記憶體負擔。 在路由層 先過濾、轉換 為必要欄位(如 model.dict()),或使用 Pydantic schema
模板內寫過多商業邏輯 會降低可讀性,難以測試。 保持模板只負責呈現,所有計算與判斷留在 Python 端。
使用可變預設參數 def func(data: list = []),多個請求會共用同一個列表。 使用 None 為預設值,並在函式內初始化。
路徑穿越 (Path Traversal) 如果直接使用使用者提供的檔名作為模板名稱,可能被惡意讀取其他檔案。 嚴格驗證白名單 模板名稱,絕不直接拼接未經過濾的字串。
未設定 static 路徑 靜態檔案無法被載入,前端樣式、腳本失效。 確認 app.mount("/static", StaticFiles(...)) 已正確執行,且目錄結構與 templates 同層或自行調整。

其他最佳實踐

  1. 使用 BaseModel 作為模板資料的結構

    • 定義一個 ResponseModel,保證傳入的欄位與型別一致,減少跑時錯誤。
  2. 分層管理模板

    • templates/ 內依功能建立子目錄(如 users/, admin/),避免檔名衝突。
  3. 全域變數與環境設定

    • 利用 Dependency InjectionDepends)提供 settingsdebugcurrent_user 等全域資訊,讓模板可以直接使用。
  4. 快取模板

    • Jinja2 內建快取機制,對於不變的模板可以設定 loader=FileSystemLoader("templates", followlinks=True) 並啟用 auto_reload=False,提升效能。
  5. 安全性

    • 預設 Jinja2 會自動 HTML 轉義,但若使用 {{ variable|safe }} 必須確保內容已經過信任或淨化,避免 XSS。

實際應用場景

場景 為什麼需要模板參數傳遞? 範例實作
使用者個人化儀表板 每位使用者的資料、通知、權限都不同,需要在同一個 HTML 中動態呈現。 參考 範例 5,使用 Depends 注入使用者資訊與全域設定。
商品列表與分頁 從資料庫撈出分頁結果,需將 itemspagetotal_pages 等資訊傳給前端。 在路由中計算分頁資訊,僅傳遞必要欄位給 list.html
表單驗證錯誤回傳 表單提交失敗時,需要把錯誤訊息與原始輸入值一起回傳,以免使用者重新填寫。 使用 Form 取得資料,驗證失敗時 TemplateResponse 中帶入 errorsold_data
多語系網站 依使用者語系載入不同的文字資源或日期格式。 request.state.locale 中保存語系,透過模板過濾器 (`{{ date
即時聊天或通知面板 需要在頁面渲染時注入 WebSocket URL、使用者 ID 等參數。 dashboard.html 中使用 {{ ws_url }}{{ user_id }},由路由一次性提供。

總結

FastAPI 中,模板參數傳遞 是將後端資料與前端呈現緊密結合的關鍵環節。透過 Jinja2TemplatesTemplateResponserequest 的正確配合,我們可以:

  • 安全且彈性 地將字串、列表、字典、Pydantic Model 甚至自訂物件傳入模板。
  • 保持模板乾淨:所有商業邏輯與資料處理應在路由層完成,模板只負責 UI 呈現。
  • 避免常見陷阱:忘記傳 request、直接使用可變預設參數、在模板內寫過多程式碼等,都會影響效能與安全性。
  • 運用 Dependency Injection 提供全域設定、使用者資訊或環境變數,使模板更具可測試性與可維護性。

掌握以上概念與實務技巧後,你就能在 FastAPI 專案中快速構建 動態、可維護且安全 的網頁介面,從簡單的歡迎頁面到複雜的管理後台,都能游刃有餘。祝開發順利,期待看到你用 FastAPI 打造出更多精彩的 Web 應用! 🚀