在現代 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 }}- 轉換篩選器:如
int和float篩選器,用於將一種資料型別轉換為另一種。{{ 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檔案。
- 安裝Jinja套件並建立
templates資料夾:(venv)$ pip install jinja2 (venv)$ mkdir templates - 在新建立的資料夾中,建立兩個新的檔案
home.html和todo.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})
內容解密:
- 從
fastapi.templating匯入Jinja2Templates,以便在 FastAPI 中使用 Jinja 範本。 - 建立
templates物件,並指定範本目錄為templates/。 - 在
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")
內容解密:
- 在
retrieve_todo和get_single_todo函式中,使用templates.TemplateResponse傳回渲染後的todo.html範本。 - 根據路由的不同,傳遞不同的資料給範本。
建立範本
現在,我們來建立 home.html 和 todo.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>
內容解密:
- 使用 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 %}
內容解密:
- 使用
{% extends "home.html" %}繼承home.html範本。 - 在
{% block todo_container %}中定義子範本的內容。 - 使用 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 應用程式」 中深入討論。
實作模型
讓我們看看實作模型的步驟:
建立應用程式的第一步是定義事件和使用者的模型。模型描述了資料將如何在我們的應用程式中儲存、輸入和表示。下圖顯示了使用者和事件的建模以及它們之間的關係: 此圖示呈現了使用者與事件之間的關聯,每個使用者都擁有一個事件列表。
讓我們在 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用於定義範例資料,以便在檔案中使用。
- 讓我們在 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用於定義範例資料,以便在檔案中使用。
- 讓我們建立一個新的模型 NewUser,它繼承自 User 模型;這個新模型將用作註冊新使用者時的資料型別。
- 最後,讓我們實作一個用於登入使用者的模型:
class UserSignIn(BaseModel):
email: EmailStr
password: str
class Config:
schema_extra = {
"example": {
"email": "fastapi@packt.com",
"password": "strong!!!",
}
}
內容解密:
- UserSignIn 模型包含兩個欄位:email 和 password。
schema_extra用於定義範例資料,以便在檔案中使用。
現在我們已經成功地實作了我們的模型,讓我們在下一節中實作路由。