本文 AI 產出,尚未審核

FastAPI – 靜態檔案與模板(Static & Templates)

主題:Jinja2Templates 整合


簡介

在開發 Web 應用時,HTML 模板是將後端資料渲染成使用者可見頁面的關鍵。FastAPI 本身是一個以 ASGI 為基礎、支援非同步的高效能框架,雖然它的核心聚焦在 API 開發,但同時也提供了與 Jinja2 這類成熟模板引擎的無縫整合,讓開發者可以快速建立動態網頁。

本單元將說明如何在 FastAPI 中設定 靜態檔案(CSS、JS、圖片)與 Jinja2Templates,並透過實作範例展示從路由到模板渲染的完整流程。掌握這些技巧後,你就能在同一個專案裡同時提供 RESTful API 與傳統的網頁介面,提升開發效率與使用者體驗。


核心概念

1. 為什麼選擇 Jinja2?

  • 表達力強:支援條件判斷、迴圈、過濾器等常見模板語法。
  • 與 Python 完全相容:可以直接在模板中呼叫 Python 函式或變數。
  • 廣泛使用:許多大型框架(如 Flask、Django)都採用 Jinja2,學會後可輕鬆遷移。

2. FastAPI 與 Jinja2 的整合方式

FastAPI 透過 fastapi.templating.Jinja2Templates 類別將 Jinja2 包裝起來,只要把模板目錄傳入即可。再配合 fastapi.staticfiles.StaticFiles,即可同時提供靜態資源與動態頁面。

from fastapi import FastAPI, Request
from fastapi.staticfiles import StaticFiles
from fastapi.templating import Jinja2Templates

app = FastAPI()

# 1️⃣ 設定靜態檔案路徑(預設會掛載在 /static)
app.mount("/static", StaticFiles(directory="static"), name="static")

# 2️⃣ 設定模板目錄(預設是 /templates)
templates = Jinja2Templates(directory="templates")

小技巧StaticFiles 必須在 app.mount() 之後才能被路由使用,否則會產生 404。

3. 基本的模板渲染流程

  1. 建立 Request 物件:FastAPI 會自動把 Request 注入到路由函式中。
  2. 呼叫 templates.TemplateResponse:傳入模板檔名與 context(字典),其中必須包含 request 鍵,讓 Jinja2 能取得 URL 生成器等資訊。
  3. 返回 Response:FastAPI 會自動將 HTML 回傳給瀏覽器。
@app.get("/", response_class=HTMLResponse)
async def read_root(request: Request):
    # context 內可放任意資料供模板使用
    context = {"request": request, "title": "歡迎使用 FastAPI"}
    return templates.TemplateResponse("index.html", context)

4. 範例:從零建立靜態資源與模板

以下示範一個完整的目錄結構與程式碼,適合作為新手的起點。

my_fastapi_app/
│
├─ app.py               # 主程式
├─ static/
│   ├─ css/
│   │   └─ style.css
│   └─ img/
│       └─ logo.png
└─ templates/
    ├─ base.html
    └─ index.html

4.1 app.py

# app.py
from fastapi import FastAPI, Request
from fastapi.responses import HTMLResponse
from fastapi.staticfiles import StaticFiles
from fastapi.templating import Jinja2Templates

app = FastAPI()

# 靜態檔案設定
app.mount("/static", StaticFiles(directory="static"), name="static")

# 模板設定
templates = Jinja2Templates(directory="templates")


@app.get("/", response_class=HTMLResponse)
async def home(request: Request):
    """
    主要頁面,渲染 index.html 並傳入變數
    """
    data = {
        "request": request,
        "title": "FastAPI + Jinja2 範例",
        "user": {"name": "Alice", "age": 30},
        "items": ["蘋果", "香蕉", "橘子"]
    }
    return templates.TemplateResponse("index.html", data)

4.2 templates/base.html(基礎版型)

{# templates/base.html #}
<!DOCTYPE html>
<html lang="zh-Hant">
<head>
    <meta charset="UTF-8">
    <title>{% block title %}{{ title }}{% endblock %}</title>
    <link rel="stylesheet" href="{{ url_for('static', path='css/style.css') }}">
</head>
<body>
    <header>
        <h1>{% block header %}FastAPI Demo{% endblock %}</h1>
    </header>

    <main>
        {% block content %}{% endblock %}
    </main>

    <footer>
        <p>© 2025 My FastAPI App</p>
    </footer>
</body>
</html>

4.3 templates/index.html(實際渲染頁面)

{# templates/index.html #}
{% extends "base.html" %}

{% block title %}{{ title }}{% endblock %}

{% block header %}
    <img src="{{ url_for('static', path='img/logo.png') }}" alt="logo" height="50">
    {{ title }}
{% endblock %}

{% block content %}
    <h2>使用者資訊</h2>
    <p>姓名:{{ user.name }}</p>
    <p>年齡:{{ user.age }}</p>

    <h2>水果清單</h2>
    <ul>
        {% for fruit in items %}
            <li>{{ loop.index }}. {{ fruit }}</li>
        {% else %}
            <li>目前尚無資料</li>
        {% endfor %}
    </ul>
{% endblock %}

4.4 static/css/style.css(簡單樣式)

/* static/css/style.css */
body { font-family: Helvetica, Arial, sans-serif; margin: 0; padding: 0; }
header { background: #4CAF50; color: white; padding: 1rem; text-align: center; }
main { padding: 2rem; }
footer { background: #f1f1f1; text-align: center; padding: 0.5rem; }

說明

  • url_for('static', path='...') 是 FastAPI 為 Jinja2 提供的輔助函式,會自動產生正確的靜態檔案 URL。
  • extends{% block %} 讓多頁面共享同一版型,維護成本大幅降低。

5. 進階範例:表單提交與資料回傳

以下示範如何使用 Jinja2 與 FastAPI 處理簡易的 POST 表單,並把使用者輸入回傳至同一頁面。

# app.py(接續前面的程式碼)
from fastapi import Form

@app.get("/contact", response_class=HTMLResponse)
async def contact_form(request: Request):
    """
    顯示聯絡表單
    """
    return templates.TemplateResponse("contact.html", {"request": request, "msg": ""})


@app.post("/contact", response_class=HTMLResponse)
async def submit_contact(
    request: Request,
    name: str = Form(...),
    email: str = Form(...),
    message: str = Form(...)
):
    """
    處理表單送出,回傳感謝訊息
    """
    # 這裡可以把資料寫入資料庫、發送 Email 等
    thank_msg = f"感謝 {name}!我們已收到您的訊息。"
    return templates.TemplateResponse(
        "contact.html",
        {"request": request, "msg": thank_msg, "name": name, "email": email, "message": message},
    )

templates/contact.html

{# templates/contact.html #}
{% extends "base.html" %}

{% block title %}聯絡我們{% endblock %}

{% block content %}
<h2>聯絡表單</h2>

{% if msg %}
    <p style="color: green;">{{ msg }}</p>
{% endif %}

<form method="post" action="{{ url_for('submit_contact') }}">
    <label>姓名:<input type="text" name="name" required></label><br><br>
    <label>Email:<input type="email" name="email" required></label><br><br>
    <label>訊息:<textarea name="message" rows="5" required></textarea></label><br><br>
    <button type="submit">送出</button>
</form>
{% endblock %}

關鍵點

  • Form(...) 讓 FastAPI 從 application/x-www-form-urlencodedmultipart/form-data 讀取表單欄位。
  • url_for('submit_contact') 會自動產生對應的路由 URL,避免硬編碼。

常見陷阱與最佳實踐

陷阱 可能的結果 解決方式
未將 request 放入 context Jinja2 內部的 url_for 失效,產生 UndefinedError TemplateResponsecontext 中必須包含 {"request": request}
靜態路徑寫錯 404 錯誤、樣式或圖片不顯示 使用 url_for('static', path='...') 產生路徑,避免手動拼字
模板檔案路徑不一致 TemplateNotFound 錯誤 確認 Jinja2Templates(directory="templates") 指向正確資料夾,且檔名區分大小寫(Linux 上特別注意)
在非同步路由中使用同步 I/O 事件迴圈被阻塞,效能下降 若需讀寫檔案或 DB,使用 async 版本(如 aiofilesasyncpg
過度在模板內寫 Python 邏輯 可讀性下降、難以維護 把複雜計算搬到路由或服務層,模板只負責呈現

最佳實踐

  1. 分層設計

    • router 處理請求與回應;
    • service(或 business)執行資料運算;
    • templates 僅負責渲染。
  2. 使用基礎版型(base.html):所有頁面都 extends 基礎版型,讓全站樣式與腳本集中管理。

  3. 靜態檔案快取:在 StaticFiles 時加入 cache_control 參數,或在 CDN 上設定長效快取。

    app.mount(
        "/static",
        StaticFiles(directory="static", html=True, check_dir=False),
        name="static"
    )
    
  4. 安全性

    • 防止 XSS:Jinja2 內建 HTML 轉義,除非使用 | safe,否則不會直接輸出 HTML。
    • 只允許可信任的使用者上傳檔案,並在 static 目錄之外儲存原始檔案。

實際應用場景

場景 為何使用 Jinja2Templates
後台管理介面 快速產生表格、分頁與搜尋結果,且可與同一個 FastAPI API 共用資料模型。
簡易部落格或文件站 文章內容以 Markdown 存在資料庫或檔案,渲染時以 Jinja2 組合版型,提供 SEO 友好的 HTML。
表單驅動的業務流程(如報名、聯絡) 直接在 FastAPI 中處理表單驗證,回傳成功訊息或錯誤提示,減少前端框架依賴。
混合式微服務 某些微服務只提供 API,另一個服務負責 UI,兩者皆可使用同一套 FastAPI + Jinja2 架構,降低部署與維護成本。
即時報表 使用 async 讀取資料庫後,將結果傳給模板產生圖表(配合 Chart.js),在瀏覽器即時呈現。

總結

  • Jinja2Templates 為 FastAPI 提供了完整且易用的模板渲染能力,讓開發者能在同一個框架內同時提供 API 與傳統的 HTML 網頁。
  • 透過 StaticFilesurl_for,靜態資源的管理變得安全且可維護。
  • 掌握 基礎版型、區塊(block)、以及 context 的傳遞技巧,可大幅提升開發效率與程式碼可讀性。
  • 注意常見的路由、檔案路徑與安全性陷阱,並遵循 分層、快取、XSS 防護 等最佳實踐,才能在生產環境中發揮 FastAPI 的高效能優勢。

結合 FastAPI 的非同步特性與 Jinja2 的靈活模板語法,你現在已具備構建 高效、易維護且具備完整前後端功能 的 Web 應用所需的全部基礎。快去動手實作,將理論轉化為實際的產品吧! 🚀