跳至内容

使用 FastHTML 和 MongoDB 构建实时任务管理器

使用 Python 原生工具实现异步 CRUD 操作与 HTMX 交互的完整教程。
更新 2026年6月17日  · 5分钟

FastHTML 与 MongoDB 提供了一种高效、原生于 Python 的现代 Web 开发路径。在本教程中,我们将构建一个响应式、实时的任务管理应用程序,在一个可维护的 Python 文件中演示完整的 CRUD(创建、读取、更新、删除)生命周期。为了在 Python 中上手使用 MongoDB,我推荐课程 Introduction to MongoDB in Python

什么是 FastHTML?

FastHTML 是一个建立在 FastAPI 基础之上的极简高性能框架。它引入了 Pythonic 的 HTML 范式,使开发者可以用可复用的 Python 函数来构建完整的前端,而无需传统模板。

其核心优势在于与 HTMX 的原生集成。通过使用简单的 HTML 属性驱动服务端更新,HTMX 让您无需复杂的重型 JavaScript 构建栈,也能创建动态的单页应用体验。

什么是 MongoDB?

MongoDB 是领先的通用型、面向文档的 NoSQL 数据库。其灵活的架构与 JSON 风格的 BSON 文档,使其非常适合现代的迭代式开发。

在 Python 端,我们使用官方的异步驱动 Motor,它提供了非阻塞接口,非常契合 FastAPI 与 FastHTML 的性能导向架构。

为何这一技术栈表现出色

将这些技术结合起来,可以构建一个高效的开发环境:

  • Pydantic 与 MongoDB 的类型安全: FastHTML 借助 Pydantic 进行数据建模与校验。这些模型与 MongoDB 的文档结构天然契合,提供“代码优先”的体验,免去了沉重的对象关系映射(ORM)样板代码。
  • 端到端的异步性能: 将 Motor 与 FastHTML 的异步内核配合使用,数据库操作不会阻塞事件循环。这可确保高并发与低延迟,对实时、响应式应用至关重要。
  • 减少上下文切换: 开发者可以在统一的 Python 生态中管理数据库架构、后端逻辑与前端组件,从而显著提升交付速度。

设置与连接

在开始本教程之前,您需要准备以下先决条件:

  • Python 3.8+
  • MongoDB 实例: 本地安装或 MongoDB Atlas 集群(推荐用于实时特性)
  • 基础知识: 熟悉 Python 装饰器与基础 HTML 结构。

项目初始化

为展示该技术栈的高效性,我们将在单个 app.py 文件中实现整个应用,包括数据库配置、数据模型与响应式路由。

安装

我们需要 FastHTML 框架、Motor(MongoDB 官方异步驱动)以及用于 ASGI 服务器的 Uvicorn。

MongoDB 连接设置

我们使用 AsyncIOMotorClient 建立非阻塞连接。这样在应用等待数据库 I/O 时,仍可继续处理其他并发请求。

import os
from motor.motor_asyncio import AsyncIOMotorClient
from fasthtml.common import *
from bson import ObjectId
from pydantic import Field, BaseModel
from pymongo.errors import ServerSelectionTimeoutError, ConnectionFailure


# configure MongoDB
MONGO_URI = os.environ.get("MONGO_URI", "mongodb://localhost:27017/") # your Mongo URI
DB_NAME = "fasthtml_tasks_db"
COLLECTION_NAME = "tasks"

# Initialize the async client and reference our collections
client = AsyncIOMotorClient(MONGO_URI)
db = client[DB_NAME]
collection = db[COLLECTION_NAME]

# FastHTML Application Initialization
app = FastHTML()

定义数据模型

在面向文档的工作流中,架构定义在您的应用代码里。我们使用 Pydantic v2 来桥接 MongoDB 的 BSON ObjectId 与标准的 Python 字符串。这可确保进入或离开数据库的每个文档都按我们的要求进行校验。

我们定义了自定义的 PyObjectId 类。这是必要的,因为 Pydantic 并不原生支持 MongoDB 的 ObjectId 类型。通过 __get_pydantic_core_schema__,我们让 Pydantic 在 JSON 中将其视为字符串,但在数据库端按 BSON 对象进行校验。

python
from pydantic import BaseModel, Field
from pydantic_core import core_schema
from pydantic.json_schema import JsonSchemaValue

class PyObjectId(ObjectId):
    """Custom type to bridge MongoDB ObjectId and Pydantic v2 validation."""
    @classmethod
    def __get_pydantic_core_schema__(cls, source_type, handler):
        # Defines how Pydantic should validate this type
        return core_schema.no_info_plain_validator_function(cls.validate)

    @classmethod
    def validate(cls, v):
        if isinstance(v, ObjectId): return v
        if isinstance(v, str) and ObjectId.is_valid(v): return ObjectId(v)
        raise ValueError("Invalid ObjectId")

    @classmethod
    def __get_pydantic_json_schema__(cls, _core_schema, handler) -> JsonSchemaValue:
        # Ensures the OpenAPI/JSON schema reflects this as a string
        return {"type": "string"}

class Task(BaseModel):
    """Pydantic model representing the Task document schema."""
    # Maps MongoDB's internal '_id' to a developer-friendly 'id' field
    id: PyObjectId | None = Field(None, alias='_id')
    title: str
    description: str | None = None
    completed: bool = False

    model_config = ConfigDict(
        arbitrary_types_allowed=True
    )

布局与初始路由

布局组件是一个可复用的高阶函数,用于包裹视图。这确保了 Tailwind CSS(用于样式)和 HTMX(用于交互)等关键依赖在整个应用中保持一致。

借助 FastHTML 的标题式组件(如 MainDiv),我们保持了干净的 Pythonic 结构,映射出 HTML 树。

# A responsive layout using Tailwind CSS and the HTMX CDN
def layout(*comps):
    """Wraps the application content in a consistent container."""
    return Main(
        Div(
            H1('📝 Real-Time FastHTML Task Manager', 
               cls='text-3xl font-bold mb-8 text-center text-gray-800'),
            *comps,
            cls='container mx-auto p-6 max-w-2xl'
        )
    )

@app.get('/')
async def home():
    """Initial route rendering the core application layout."""
    return layout(
        await TaskList(), # Fetches and displays existing tasks (Read)
        TaskForm()        # Renders the entry form (Create)
    )

现在运行应用,您的界面将如下所示:

图片:完成初始设置后的截图

目前还没有任务。我们将在下一节添加和更新任务。

完整 CRUD 实现

读取:显示任务(GET /

要显示任务,我们会获取所有文档并在组件中渲染。不过在真实场景中,数据库可能离线。我们更新后的 TaskList 会优雅地处理这一情况,在界面中直接提供故障排除步骤。

健壮的数据获取

我们使用 collection.find().to_list(length=None) 异步获取文档。通过放入 try/except 块,我们可以检测 MongoDB 是否断开连接,并立即向用户反馈。

async def TaskList():
    """Fetches documents from MongoDB and hydrates the Task list view with error handling."""
    try:
        tasks_data = await collection.find().to_list(length=None)
        tasks = [Task(**doc) for doc in tasks_data]
    except (ServerSelectionTimeoutError, ConnectionFailure, Exception) as e:
        # Catch MongoDB connection errors and provide troubleshooting tips
        return Div(
            H2('Current Tasks', cls='text-xl font-semibold mb-4'),
            Div(
                P('⚠️ MongoDB is not running.', cls='text-red-600 font-semibold mb-2'),
                P('To start MongoDB:', cls='text-gray-600 mb-1'),
                P('1. brew install mongodb-community', cls='text-gray-500 text-sm ml-4'),
                P('2. brew services start mongodb-community', cls='text-gray-500 text-sm ml-4'),
                cls='p-4 bg-yellow-50 border border-yellow-200 rounded'
            ),
            id='task-list'
        )

    return Div(
        H2('Current Tasks', cls='text-xl font-semibold mb-4'),
        *[TaskItem(task) for task in tasks] if tasks else P('No tasks yet. Add one below!', cls='text-gray-500 italic p-4'),
        id='task-list',
        cls='bg-white rounded-lg shadow-sm border border-gray-200'
    )

创建:添加新任务

创建流程使用带有 HTMX 属性增强的 HTML 表单。在更新版本中,我们从 Request 对象中显式提取表单数据,并使用 model_dump 准备要插入 MongoDB 的文档。

@app.post('/add-task')
async def add_task(req: Request):
    """Create: Inserts a task and returns the refreshed list."""
    try:
        form_data = await req.form()
        task = Task(
            title=form_data.get('title', ''),
            description=form_data.get('description') or None
        )
        
        # model_dump(by_alias=True) ensures 'id' is converted back to '_id' for Mongo
        task_dict = task.model_dump(exclude_none=True, by_alias=True)
        
        # Remove _id if it's None so MongoDB can generate its own unique ID
        if '_id' in task_dict and task_dict['_id'] is None:
            del task_dict['_id']
            
        await collection.insert_one(task_dict)
        return await TaskList()
    except Exception as e:
        return Div(P(f'Error: {str(e)}', cls='text-red-500 p-4'), id='task-list')

如果我们在终端运行以下代码:

curl -X POST http://localhost:8000/add-task -d "title=Complete FastHTML MongoDB integration" -d "description=Verify that tasks can be created, updated, and deleted successfully" -H "Content-Type: application/x-www-form-urlencoded"

您就能看到这条新任务已被添加:

图片:添加任务后的截图

更新:切换完成状态

为处理更新,我们使用 PATCH 请求。这展示了“微刷新”的方式:仅更新数据库中的单条任务记录,并基于其特定 ID 在界面中重新渲染。

@app.patch('/toggle-task/{task_id}')
async def toggle_task(task_id: str):
    """Update: Toggles completion status and returns the single row fragment."""
    task_doc = await collection.find_one({"_id": ObjectId(task_id)})
    if not task_doc: raise HTTPException(404, "Task not found")

    await collection.update_one(
        {"_id": ObjectId(task_id)},
        {"$set": {"completed": not task_doc['completed']}}
    )

    # Return only the updated TaskItem for a surgical DOM update
    updated_doc = await collection.find_one({"_id": ObjectId(task_id)})
    return TaskItem(Task(**updated_doc))

删除:移除任务

删除由 HTMX 触发。一旦 MongoDB 确认删除,服务器返回 Empty() 响应。HTMX 会将这个空响应视为移除目标元素(最近的 div)的信号,并从 DOM 中移除它。

@app.delete('/delete-task/{task_id}')
async def delete_task(task_id: str):
    """Delete: Removes a task from MongoDB."""
    await collection.delete_one({"_id": ObjectId(task_id)})
    # Signal HTMX to remove the element
    return Empty()

如果我们执行……

curl -X DELETE http://localhost:8000/delete-task/695968244236010c04f313fa

……任务就会被删除。

图片:删除后的截图

您可以在 GitHub 查看完整脚本。

结论

借助现代、以 Python 为中心的技术栈,您构建了一个无需传统客户端 JavaScript 框架复杂性的响应式应用。此架构为现代 Web 开发带来多项关键优势。FastHTML 的基于组件的 UI 与 MongoDB 灵活的文档模型相互协同,使您能够在单一、连贯的生态中维护业务逻辑、数据完整性与呈现层。这种“全栈皆 Python”的方式显著降低了开发开销与部署复杂度。

读者的后续步骤

  • 用户认证: 使用 FastAPI 依赖将任务列表限制在特定用户范围内,并在任务文档中存储 user_id
  • 高级查询: 使用 MongoDB 的聚合框架 或简单过滤器,为您的界面添加“进行中”和“已完成”视图。
  • 部署: 使用 Uvicorn 并在 NGINX 反向代理后部署 app.py,以获得生产级性能。

FAQs

能否将 MongoDB Change Streams 与 FastHTML 结合用于“推送”更新?

可以!由于 Motor 和 FastHTML 都是异步的,您可以使用 Python 的 async for 循环来监听 MongoDB 变更流。然后将其与 FastHTML 的 EventStream(服务端推送事件)配合使用,一旦数据库中的文档发生变化,即可向每位已连接用户推送实时更新。

为何不直接用原生 Python 字典,而是使用 Pydantic 模型搭配 MongoDB?

虽然 MongoDB 接受原生字典,Pydantic 则充当您的“应用架构”。它提供数据校验、类型提示以及默认值(例如自动将 completed 设为 False)。这可防止“脏数据”进入集合,并让代码在规模扩大时更易调试。

在该技术栈下如何处理数据库迁移?

MongoDB 的一大优势就是其灵活的架构。您不需要传统 SQL 意义上的“迁移”。如果为 Task 模型新增字段,只需在 Pydantic 中提供一个默认值。对 MongoDB 中缺少该字段的既有文档,在加载到应用时会用默认值进行“补水”。

我能为该任务管理器加入复杂的搜索功能吗?

当然可以。MongoDB 提供强大的 $text 索引,以及更高级的 Atlas Search(基于 Lucene)。您可以在 FastHTML 中轻松创建一个搜索框,使用 hx-get 触发 MongoDB 的聚合管道,在用户输入时按关键字过滤任务。

与 Django 或 Flask 相比,这一技术栈如何应对高并发?

FastHTML 是受 FastAPI 启发而独立的框架。它采用 ASGI 标准,单进程即可处理成千上万的并发连接。与 Motor 的非阻塞连接池搭配时,您的应用不会因等待数据库响应而“卡住”,这让其在高并发、实时场景下更高效。

主题

Top DataCamp Courses

Tracks

数据工程师 在 Python 中

40小时
掌握高需求技能,高效摄取、清洗、管理数据,并调度和监控管道,让你在数据工程领域脱颖而出。
查看详情Right Arrow
开始课程
查看更多Right Arrow