在現代 Web 應用程式開發中,前後端分離已成趨勢,但仍有許多場景需要伺服器端渲染。FastAPI 作為一個高效能的 Python Web 框架,結合 Jinja2 範本引擎,可以輕鬆實作伺服器端渲染。Jinja2 提供了豐富的範本語法,包含變數、控制結構、篩選器等,讓開發者能靈活地控制 HTML 內容的生成。透過本文,你將學會如何在 FastAPI 中使用 Jinja2 建立動態網頁,並瞭解如何結構化你的 FastAPI 應用程式,使其更易於維護和擴充套件。從設定 Jinja2 環境到建立範本檔案,再到將其與 FastAPI 路由整合,本文將提供完整的實作步驟和程式碼範例,幫助你快速上手。此外,我們也會探討 FastAPI 應用程式的結構化設計,以事件規劃器為例,說明如何組織程式碼結構,提升程式碼可讀性和可維護性,為構建更複雜的應用程式打下堅實基礎。

在FastAPI中使用Jinja範本

瞭解Jinja範本引擎

Jinja是一種用Python編寫的範本引擎,旨在幫助API回應的渲染過程。在每種範本語言中,都有變數會被替換成實際傳遞給它們的值,還有控制範本邏輯的標籤。

Jinja範本引擎使用大括號 {} 來區分其表示式和語法與常規HTML、文字和範本檔案中的任何其他變數。{{ }} 語法稱為變數區塊,而 {% %} 語法則包含控制結構,如if/else、迴圈和巨集。

在Jinja範本語言中使用的三種常見語法區塊包括:

  • {% ... %}:此語法用於控制結構等陳述式。
  • {{ todo.item }}:此語法用於列印傳遞給它的表示式的值。
  • {# 這是一本很棒的API書!#}:此語法用於撰寫註解,不會在網頁上顯示。

Jinja範本變數可以是任何Python型別或物件,只要它們可以轉換為字串。模型、清單或字典型別可以傳遞給範本,並透過將這些屬性放在前面列出的第二個區塊中來顯示其屬性。

篩選器(Filters)

儘管Python和Jinja的語法相似,但像連線字串、將字串的第一個字元設為大寫等修改不能使用Python的語法在Jinja中完成。因此,為了執行這些修改,Jinja中有篩選器。

篩選器透過管道符號(|)與變數分開,並可能在括號中包含可選引數。篩選器的定義格式如下:

{{ variable | filter_name(*args) }}

如果沒有引數,則定義變為:

{{ variable | filter_name }}
常見篩選器
  • default篩選器:用於替換傳遞的值的輸出,如果結果為None。
    {{ todo.item | default('這是一個預設的待辦事項') }}
    
  • escape篩選器:用於呈現原始HTML輸出。
    {{ "<title>待辦事項應用程式</title>" | escape }}
    
  • 轉換篩選器:如intfloat篩選器,用於將一種資料型別轉換為另一種。
    {{ 3.142 | int }}
    {{ 31 | float }}
    
  • join篩選器:用於將清單中的元素連線成一個字串。
    {{ ['Packt', 'produces', 'great', 'books!'] | join(' ') }}
    
  • length篩選器:用於傳回傳遞的物件的長度。
    Todo count: {{ todos | length }}
    

在Jinja中使用if陳述式和迴圈

在Jinja中使用if陳述式與在Python中使用它們類別似。if陳述式用於 {% %} 控制區塊中。

{% if todo | length < 5 %}
你沒有很多待辦事項!
{% else %}
你似乎有忙碌的一天!
{% endif %}

也可以在Jinja中迭代變數。這可以是一個清單或一個通用函式,例如:

{% for todo in todos %}
<b> {{ todo.item }} </b>
{% endfor %}

巨集(Macros)

巨集是傳回HTML字串的函式。巨集的主要使用案例是避免重複程式碼,而是使用單一函式呼叫。例如,定義一個輸入巨集以減少HTML表單中輸入標籤的連續定義:

{% macro input(name, value='', type='text', size=20) %}
<div class="form">
<input type="{{ type }}" name="{{ name }}" value="{{ value|escape }}" size="{{ size }}">
</div>
{% endmacro %}

現在,要快速在表單中建立輸入,可以呼叫巨集:

{{ input('item') }}

範本繼承(Template Inheritance)

Jinja最強大的功能是範本的繼承。這一功能推進了「不要重複自己」(DRY)原則,在大型Web應用程式中非常方便。範本繼承是一種情況,其中定義了一個基礎範本,子範本可以互動、繼承和替換基礎範本的定義部分。

在FastAPI中使用Jinja範本

首先,需要安裝Jinja套件並在專案目錄中建立一個新的資料夾templates。這個資料夾將儲存所有Jinja檔案,這些檔案是混合了Jinja語法的HTML檔案。

  1. 安裝Jinja套件並建立templates資料夾:
    (venv)$ pip install jinja2
    (venv)$ mkdir templates
    
  2. 在新建立的資料夾中,建立兩個新的檔案home.htmltodo.html
    (venv)$ cd templates
    (venv)$ touch {home,todo}.html
    

這樣就完成了在FastAPI專案中設定Jinja範本的基本步驟。接下來,可以根據專案需求進一步開發和自訂範本。

在 FastAPI 中使用 Jinja 範本

在開發 Web 應用程式時,範本引擎是不可或缺的工具之一。FastAPI 支援多種範本引擎,其中 Jinja 是最受歡迎的選擇之一。本章將介紹如何在 FastAPI 中使用 Jinja 範本。

設定 Jinja

首先,我們需要在 FastAPI 應用程式中設定 Jinja。我們將修改 todo.py 檔案中的 POST 路由:

from fastapi import APIRouter, Path, HTTPException, status, Request, Depends
from fastapi.templating import Jinja2Templates
from model import Todo, TodoItem, TodoItems

todo_router = APIRouter()
todo_list = []
templates = Jinja2Templates(directory="templates/")

@todo_router.post("/todo")
async def add_todo(request: Request, todo: Todo = Depends(Todo.as_form)):
    todo.id = len(todo_list) + 1
    todo_list.append(todo)
    return templates.TemplateResponse("todo.html", {"request": request, "todos": todo_list})

內容解密:

  1. fastapi.templating 匯入 Jinja2Templates,以便在 FastAPI 中使用 Jinja 範本。
  2. 建立 templates 物件,並指定範本目錄為 templates/
  3. add_todo 函式中,使用 templates.TemplateResponse 傳回渲染後的 todo.html 範本。

更新 GET 路由

接下來,我們需要更新 GET 路由以使用 Jinja 範本:

@todo_router.get("/todo", response_model=TodoItems)
async def retrieve_todo(request: Request):
    return templates.TemplateResponse("todo.html", {"request": request, "todos": todo_list})

@todo_router.get("/todo/{todo_id}")
async def get_single_todo(request: Request, todo_id: int = Path(..., title="The ID of the todo to retrieve.")):
    for todo in todo_list:
        if todo.id == todo_id:
            return templates.TemplateResponse("todo.html", {"request": request, "todo": todo})
    raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Todo with supplied ID doesn't exist")

內容解密:

  1. retrieve_todoget_single_todo 函式中,使用 templates.TemplateResponse 傳回渲染後的 todo.html 範本。
  2. 根據路由的不同,傳遞不同的資料給範本。

建立範本

現在,我們來建立 home.htmltodo.html 範本。

home.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Packt Todo Application</title>
    <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.0/css/bootstrap.min.css">
</head>
<body>
    <header>
        <nav class="navar">
            <div class="container-fluid">
                <center><h1>Packt Todo Application</h1></center>
            </div>
        </nav>
    </header>
    <div class="container-fluid">
        {% block todo_container %}{% endblock %}
    </div>
</body>
</html>

內容解密:

  1. 使用 Jinja 的區塊語法 {% block todo_container %}{% endblock %} 定義一個區塊,供子範本使用。

todo.html

{% extends "home.html" %}

{% block todo_container %}
<main class="container">
    <hr>
    <section class="container-fluid">
        <form method="post">
            <div class="col-auto">
                <div class="input-group mb-3">
                    <input type="text" name="item" value="{{ item }}" class="form-control" placeholder="Purchase Packt's Python workshop course">
                    <button class="btn btn-outline-primary" type="submit">Add Todo</button>
                </div>
            </div>
        </form>
    </section>
    {% if todo %}
    <article class="card container-fluid">
        <br/>
        <h4>Todo ID: {{ todo.id }} </h4>
        <p><strong>Item: {{ todo.item }}</strong></p>
    </article>
    {% else %}
    <section class="container-fluid">
        <h2 align="center">Todos</h2>
        <br>
        <div class="card">
            <ul class="list-group list-group-flush">
                {% for todo in todos %}
                <li class="list-group-item">{{ loop.index }}. <a href="/todo/{{loop.index}}">{{ todo.item }}</a></li>
                {% endfor %}
            </ul>
        </div>
    {% endif %}
    </section>
</main>
{% endblock %}

內容解密:

  1. 使用 {% extends "home.html" %} 繼承 home.html 範本。
  2. {% block todo_container %} 中定義子範本的內容。
  3. 使用 Jinja 的條件語法 {% if todo %} 和迴圈語法 {% for todo in todos %} 來渲染不同的內容。

結構化 FastAPI 應用程式

在前四章中,我們已經瞭解了使用 FastAPI 的基礎步驟並建立了一個簡單的 Todo 應用程式。到目前為止,我們建立的應用程式是一個單一檔案的 Todo 應用程式,展示了 FastAPI 的靈活性和強大功能。前面章節的重點在於使用 FastAPI 建立應用程式的簡易性。然而,隨著應用程式的複雜性和功能的增加,需要對應用程式進行適當的結構化。

結構化是指將應用程式元件以有組織的格式排列,這種格式可以是模組化的,以提高應用程式碼的可讀性和內容的可維護性。結構合理的應用程式能夠實作更快速的開發、更快速的除錯和整體生產力的提高。

在本章結束時,您將具備有關結構化的知識以及如何結構化您的 API。在本章中,您將涵蓋以下主題:

  • 結構化應用程式路由和模型
  • 為規劃 API 實作模型

技術需求

FastAPI 應用程式中的結構化

在本章中,我們將建立一個事件規劃器。讓我們設計應用程式結構如下:

planner/
main.py
database/
__init__.py
connection.py
routes/
__init__.py
events.py
users.py
models/
__init__.py
events.py
users.py

第一步是為應用程式建立一個新的資料夾,並將其命名為 planner:

$ mkdir planner && cd planner

在新建立的 planner 資料夾中,建立一個入口檔案 main.py 和三個子資料夾 – database、routes 和 models:

$ touch main.py
$ mkdir database routes models

接下來,在每個資料夾中建立 init.py:

$ touch {database,routes,models}/__init__.py

在 database 資料夾中,讓我們建立一個空白檔案 connection.py,它將處理我們將在下一章中使用的資料函式庫抽象和組態:

$ touch database/connection.py

在 routes 和 models 資料夾中,我們將建立兩個檔案,events.py 和 users.py:

$ touch {routes,models}/{events,users}.py

每個檔案都有其功能,如下所述:

  • routes 資料夾中的檔案:
    • events.py:此檔案將處理路由操作,例如建立、更新和刪除事件。
    • users.py:此檔案將處理路由操作,例如註冊和登入使用者。
  • models 資料夾中的檔案:
    • events.py:此檔案將包含事件操作的模型定義。
    • users.py:此檔案將包含使用者操作的模型定義。

建立事件規劃器應用程式

在本文中,我們將建立一個事件規劃器應用程式。在此應用程式中,註冊的使用者將能夠建立、更新和刪除事件。建立的事件可以透過導航到應用程式自動建立的事件頁面來檢視。

每個註冊的使用者和事件都將具有唯一的 ID,以防止在管理具有相同 ID 的使用者和事件時發生衝突。在本文中,我們不會優先考慮身份驗證或資料函式倉管理,因為這將在第 6 章「連線到資料函式庫」 和第 7 章「保護 FastAPI 應用程式」 中深入討論。

實作模型

讓我們看看實作模型的步驟:

  1. 建立應用程式的第一步是定義事件和使用者的模型。模型描述了資料將如何在我們的應用程式中儲存、輸入和表示。下圖顯示了使用者和事件的建模以及它們之間的關係: 此圖示呈現了使用者與事件之間的關聯,每個使用者都擁有一個事件列表。

  2. 讓我們在 models/events.py 中定義 Event 模型:

from pydantic import BaseModel
from typing import List

class Event(BaseModel):
    id: int
    title: str
    image: str
    description: str
    tags: List[str]
    location: str

    class Config:
        schema_extra = {
            "example": {
                "title": "FastAPI Book Launch",
                "image": "https://linktomyimage.com/image.png",
                "description": "We will be discussing the contents of the FastAPI book in this event. Ensure to come with your own copy to win gifts!",
                "tags": ["python", "fastapi", "book", "launch"],
                "location": "Google Meet"
            }
        }

內容解密:

  • Event 模型包含五個欄位:id、title、image、description、tags 和 location。
  • schema_extra 用於定義範例資料,以便在檔案中使用。
  1. 讓我們在 models/users.py 中定義 User 模型:
from pydantic import BaseModel, EmailStr
from typing import Optional, List
from models.events import Event

class User(BaseModel):
    email: EmailStr
    password: str
    events: Optional[List[Event]]

    class Config:
        schema_extra = {
            "example": {
                "email": "fastapi@packt.com",
                "username": "strong!!!",
                "events": [],
            }
        }

內容解密:

  • User 模型包含三個欄位:email、password 和 events。
  • events 欄位是可選的,且預設為空列表。
  • schema_extra 用於定義範例資料,以便在檔案中使用。
  1. 讓我們建立一個新的模型 NewUser,它繼承自 User 模型;這個新模型將用作註冊新使用者時的資料型別。
  2. 最後,讓我們實作一個用於登入使用者的模型:
class UserSignIn(BaseModel):
    email: EmailStr
    password: str

    class Config:
        schema_extra = {
            "example": {
                "email": "fastapi@packt.com",
                "password": "strong!!!",
            }
        }

內容解密:

  • UserSignIn 模型包含兩個欄位:email 和 password。
  • schema_extra 用於定義範例資料,以便在檔案中使用。

現在我們已經成功地實作了我們的模型,讓我們在下一節中實作路由。