建立一个简单的todo应用程序(前端React;后端FastAPI;数据库MongoDB)

成果展示

todo app demo

文件目录

在这里插入图片描述
backend
在这里插入图片描述
src
在这里插入图片描述

1. 创建python环境

mkdir farm-todo
cd farm-todo
mkdir frontend backend
cd backend
python -m venv venv
source venv/bin/activate
pip install "fastapi[all]" "motor[srv]" beanie aiostream
  • 安装了FastAPI的所有可选依赖(包括一些web框架常用的依赖库,如uvicornpydantic等),使它能够运行完整的API功能。FastAPI是一个用于构建高性能API的Python框架,以异步编程和Python类型注解为基础,易于构建快速而灵活的API。

  • 安装Motor库并附带srv选项,确保能够使用MongoDB的SRV协议连接字符串。Motor是MongoDB官方的异步驱动程序,与FastAPI等异步框架兼容,可以异步地操作MongoDB数据库,提升处理大量并发请求的效率。

  • 安装了Beanie库,它是一个MongoDB的ORM(对象关系映射库),支持Pydantic模型并与Motor集成。Pydantic模型是一种在Python中使用的数据验证和数据结构定义工具。Beanie可以让开发者使用Python对象的形式来定义和操作MongoDB中的数据模型,减少了操作数据库的复杂性。

  • 安装了Aiostream库,它提供了异步流处理的工具。这个库可以帮助管理和处理数据流,适用于处理多个并发任务的数据操作,使数据流管理更加方便。

pip freeze > requirements.txt

2. 创建2个文档:Dockerfile和pyproject.toml

Dockerfile: 定义一个用于运行Python应用程序的Docker镜像。

FROM python:3

WORKDIR /usr/src/app
COPY requirements.txt ./

RUN pip install --no-cache-dir --upgrade -r ./requirements.txt

EXPOSE 3001

CMD [ "python", "./src/server.py" ]
  • 使用Python 3的官方镜像作为基础镜像,提供了Python 3环境以及相关依赖,这样就不需要手动安装Python。
  • 指定容器内的工作目录为/usr/src/app。之后的所有命令都会在这个目录中执行。如果目录不存在,Docker会自动创建它。
  • 将本地目录中的requirements.txt文件复制到工作目录/usr/src/app中。requirements.txt包含了应用程序所需的Python依赖库列表。
  • 运行pip install命令,安装requirements.txt中列出的所有依赖项。--no-cache-dir选项可以减少镜像体积,因为不会缓存安装包。--upgrade确保安装的是最新版本的依赖。
  • 声明容器会监听3001端口,通常表示应用会在这个端口上提供服务。虽然EXPOSE不会直接启用端口访问,但它是一个声明性指示,用于告知运行时应该暴露的端口。
  • 指定容器启动时的默认命令,这里运行Python脚本./src/server.py。当容器启动时,这个命令会被执行,用于启动应用服务器。
这个Dockerfile的作用是创建一个Docker镜像,用于运行一个Python应用程序,依赖于requirements.txt中列出的库,并在端口3001上监听请求。

pyproject.toml: 指定了pytest工具的一个选项,定义了测试时的Python路径。

[tool.pytest.ini_options]
pythonpath = "src"
  • pytest是Python的一个常用测试框架,用于运行单元测试、集成测试等。
  • src目录添加到PYTHONPATH中。这样在运行测试时,pytest可以直接导入src目录中的模块,而不需要额外的路径配置。

3. 在backend的文件夹之下建立src文件夹,并创建 dal.py和 server.py

dal.py

from bson import ObjectId
# ObjectId是MongoDB的默认主键类型,通常用于在查询和处理数据库中的唯一标识符。

from motor.motor_asyncio import AsyncIOMotorCollection
# motor是MongoDB的异步驱动程序,用于与MongoDB进行异步交互。
# AsyncIOMotorCollection是一个集合(collection)对象,提供了对MongoDB集合的异步操作。

from pymongo import ReturnDocument
# ReturnDocument 是pymongo中的常量,用于在更新操作时指定返回更新前或更新后的文档。

from pydantic import BaseModel
# Pydantic的BaseModel类是一个数据验证和数据结构定义工具。

from uuid import uuid4

class ListSummary(BaseModel):
  id: str # 文档的唯一标识符,将MongoDB中的ObjectId转换为字符串。
  name: str # 文档的名称字段。
  item_count: int # 文档中项目的数量字段。

  @staticmethod
  def from_doc(doc) -> "ListSummary":
      return ListSummary(
          id=str(doc["_id"]),
          name=doc["name"],
          item_count=doc["item_count"],
      )
# 这是一个静态方法,接受一个MongoDB文档(字典类型)作为参数,
# 并将该文档转换为ListSummary对象。doc通常是从MongoDB集合中查询返回的文档数据。


class ToDoListItem(BaseModel):
  id: str
  label: str
  checked: bool

  @staticmethod
  def from_doc(item) -> "ToDoListItem":
      return ToDoListItem(
          id=item["id"],
          label=item["label"],
          checked=item["checked"],
      )
# 将MongoDB中表示单个待办事项的文档(item)转换为ToDoListItem对象。
# 它提取id、label和checked字段,并使用这些字段来创建一个ToDoListItem实例。


class ToDoList(BaseModel):
  id: str
  name: str
  items: list[ToDoListItem]

  @staticmethod
  def from_doc(doc) -> "ToDoList":
      return ToDoList(
          id=str(doc["_id"]),
          name=doc["name"],
          items=[ToDoListItem.from_doc(item) for item in doc["items"]],
      )
# 接收一个待办事项列表的MongoDB文档(doc),将其转换为ToDoList对象。
# 方法从doc中提取_id和name字段,并将items字段中的每个待办事项文档
# 通过ToDoListItem.from_doc方法转换为ToDoListItem对象,最终返回一个ToDoList实例。


# 处理对MongoDB中待办事项数据的增删改查操作。
class ToDoDAL:
  def __init__(self, todo_collection: AsyncIOMotorCollection):
      self._todo_collection = todo_collection

  # 返回所有待办事项列表的简要信息(名称和项目数)
  async def list_todo_lists(self, session=None):
      async for doc in self._todo_collection.find(
          {},
          projection={
              "name": 1,
              "item_count": {"$size": "$items"},
          },
          sort={"name": 1},
          session=session,
      ):
          yield ListSummary.from_doc(doc)

  # 创建一个新的待办事项列表。
  # 插入文档的_id字符串形式。
  async def create_todo_list(self, name: str, session=None) -> str:
      response = await self._todo_collection.insert_one(
          {"name": name, "items": []},
          session=session,
      )
      return str(response.inserted_id)

  # 根据ID获取特定待办事项列表的完整信息。
  async def get_todo_list(self, id: str | ObjectId, session=None) -> ToDoList:
      doc = await self._todo_collection.find_one(
          {"_id": ObjectId(id)},
          session=session,
      )
      return ToDoList.from_doc(doc)

  # 根据ID删除一个待办事项列表。
  async def delete_todo_list(self, id: str | ObjectId, session=None) -> bool:
      response = await self._todo_collection.delete_one(
          {"_id": ObjectId(id)},
          session=session,
      )
      return response.deleted_count == 1

  # 向指定待办事项列表中添加新项目
  async def create_item(
      self,
      id: str | ObjectId,
      label: str,
      session=None,
  ) -> ToDoList | None:
      result = await self._todo_collection.find_one_and_update(
          {"_id": ObjectId(id)},
          {
              "$push": {
                  "items": {
                      "id": uuid4().hex,
                      "label": label,
                      "checked": False,
                  }
              }
          },
          session=session,
          return_document=ReturnDocument.AFTER,
      )
      if result:
          return ToDoList.from_doc(result)

  # 设置待办事项item的checked状态。
  async def set_checked_state(
      self,
      doc_id: str | ObjectId,
      item_id: str,
      checked_state: bool,
      session=None,
  ) -> ToDoList | None:
      result = await self._todo_collection.find_one_and_update(
          {"_id": ObjectId(doc_id), "items.id": item_id},
          {"$set": {"items.$.checked": checked_state}},
          session=session,
          return_document=ReturnDocument.AFTER,
      )
      if result:
          return ToDoList.from_doc(result)

  # 从指定待办事项列表中删除一个项目。
  async def delete_item(
      self,
      doc_id: str | ObjectId,
      item_id: str,
      session=None,
  ) -> ToDoList | None:
      result = await self._todo_collection.find_one_and_update(
          {"_id": ObjectId(doc_id)},
          {"$pull": {"items": {"id": item_id}}},
          session=session,
          return_document=ReturnDocument.AFTER,
      )
      if result:
          return ToDoList.from_doc(result)

server.py

from contextlib import asynccontextmanager
from datetime import datetime
import os
import sys

from bson import ObjectId
from fastapi import FastAPI, status

from motor.motor_asyncio import AsyncIOMotorClient 
# 是MongoDB异步客户端(motor库)来连接数据库。

from pydantic import BaseModel
import uvicorn # 是ASGI服务器,用于运行FastAPI应用。

from dal import ToDoDAL, ListSummary, ToDoList

COLLECTION_NAME = "todo_lists"
MONGODB_URI = os.environ["MONGODB_URI"]
# 从环境变量中获取MongoDB的URI连接字符串。

DEBUG = os.environ.get("DEBUG", "").strip().lower() in {"1", "true", "on", "yes"}
# 从环境变量读取调试模式,接受多种True的写法。


# 这是一个异步上下文管理器,管理FastAPI应用的启动和关闭。
@asynccontextmanager
async def lifespan(app: FastAPI):
    # Startup:
    client = AsyncIOMotorClient(MONGODB_URI)
    # 创建MongoDB异步客户端实例,连接到数据库。
    
    database = client.get_default_database()
    # 获取MongoDB数据库实例,默认使用连接字符串中的数据库名称。

	# ping命令用于测试MongoDB连接是否正常。pong["ok"] == 1表示连接成功。
    pong = await database.command("ping")
    if int(pong["ok"]) != 1:
        raise Exception("Cluster connection is not okay!")

    todo_lists = database.get_collection(COLLECTION_NAME)
    # 获取待办事项集合todo_lists。
    
    app.todo_dal = ToDoDAL(todo_lists)
    # 将ToDoDAL实例附加到FastAPI应用实例上,便于其他模块使用。

    # Yield back to FastAPI Application:
    yield
    # 暂停上下文管理器,将控制权交给应用,以继续运行其他操作。

    # Shutdown:
    client.close()
    # 应用关闭时,自动关闭MongoDB连接以释放资源。


app = FastAPI(lifespan=lifespan, debug=DEBUG)
# 使用lifespan管理器(在应用启动和关闭时管理数据库连接)。
# debug=DEBUG设定调试模式,使得在调试时更容易看到错误和详细信息。

# 异步获取所有待办事项列表的摘要信息。
@app.get("/api/lists")
async def get_all_lists() -> list[ListSummary]:
    return [i async for i in app.todo_dal.list_todo_lists()]

# 用于创建新的待办事项列表,包含列表名称。
class NewList(BaseModel):
    name: str

# 用于创建操作成功后的响应,包含列表的id和name。
class NewListResponse(BaseModel):
    id: str
    name: str


# 接受NewList格式的数据,创建新的待办事项列表。
@app.post("/api/lists", status_code=status.HTTP_201_CREATED)
async def create_todo_list(new_list: NewList) -> NewListResponse:
    return NewListResponse(
        id=await app.todo_dal.create_todo_list(new_list.name),
        name=new_list.name,
    )

# 使用列表的唯一标识符list_id获取单个待办事项列表。
# 调用get_todo_list方法,返回一个包含列表详细信息的ToDoList对象。
@app.get("/api/lists/{list_id}")
async def get_list(list_id: str) -> ToDoList:
    """Get a single to-do list"""
    return await app.todo_dal.get_todo_list(list_id)

# 根据list_id删除指定的待办事项列表。
# 调用delete_todo_list方法,返回布尔值表示删除是否成功。
@app.delete("/api/lists/{list_id}")
async def delete_list(list_id: str) -> bool:
    return await app.todo_dal.delete_todo_list(list_id)


# 用于添加新待办事项的数据模型,包含label字段,表示待办事项的标签(描述)。
class NewItem(BaseModel):
    label: str

# 用于返回新添加项的响应模型,包含id和label字段。
class NewItemResponse(BaseModel):
    id: str
    label: str


# 用于向指定的待办事项列表(list_id)中添加新待办事项items。
@app.post(
    "/api/lists/{list_id}/items/",
    status_code=status.HTTP_201_CREATED,
)
# 调用create_item方法,将list_id和new_item.label传入。
# 返回更新后的完整待办事项列表ToDoList,包含新的待办事项item。
async def create_item(list_id: str, new_item: NewItem) -> ToDoList:
    return await app.todo_dal.create_item(list_id, new_item.label)


# 根据list_id和item_id删除指定待办事项列表中的特定项。
# 返回更新后的ToDoList对象,删除项后更新的列表。
@app.delete("/api/lists/{list_id}/items/{item_id}")
async def delete_item(list_id: str, item_id: str) -> ToDoList:
    return await app.todo_dal.delete_item(list_id, item_id)


# 定义更新待办事项item的完成状态所需的数据模型。
# 包含item_id(待办事项item的唯一标识符)和checked_state(布尔值,表示是否已完成)字段。
class ToDoItemUpdate(BaseModel):
    item_id: str
    checked_state: bool


# 用于更新指定待办事项列表中某个待办事项item的完成状态。
# 返回更新后的ToDoList对象,反映状态变更后的完整列表。
@app.patch("/api/lists/{list_id}/checked_state")
async def set_checked_state(list_id: str, update: ToDoItemUpdate) -> ToDoList:
    return await app.todo_dal.set_checked_state(
        list_id, update.item_id, update.checked_state
    )


class DummyResponse(BaseModel):
    id: str
    when: datetime


@app.get("/api/dummy")
async def get_dummy() -> DummyResponse:
    return DummyResponse(
        id=str(ObjectId()),
        when=datetime.now(),
    )
# 使用ObjectId()生成一个新的唯一标识符并转换为字符串,赋值给id。
# 每个文档在MongoDB数据库中都有一个_id字段,默认情况下,它的值是一个ObjectId。
# 使用datetime.now()获取当前时间并赋值给when。


def main(argv=sys.argv[1:]):
    try:
    	# 使用uvicorn来启动FastAPI应用
        uvicorn.run("server:app", host="0.0.0.0", port=3001, reload=DEBUG)
        # host="0.0.0.0":让应用在所有可用的网络接口上监听。
        # reload=DEBUG:如果DEBUG为True,在代码更改时自动重载应用(适合开发阶段)。
    except KeyboardInterrupt:
        pass
        # 使用try...except来捕获KeyboardInterrupt异常,以便在按下Ctrl+C时优雅地停止服务。


if __name__ == "__main__":
    main()

4. 在MONGODB上建立一个cluster,建立.env文件(与backend文件夹同一级),在.env文件中贴上MONGODB_URI,并在问号前加上todo

5. 创建compose.yaml(与backend文件夹同一级)

name: todo-app
services:
  nginx:
    image: nginx:1.17
    volumes:
      - ./nginx/nginx.conf:/etc/nginx/conf.d/default.conf
    ports:
      - 8000:80
    depends_on:
      - backend
      - frontend
  frontend:
    image: "node:22"
    user: "node"
    working_dir: /home/node/app
    environment:
      - NODE_ENV=development
      - WDS_SOCKET_PORT=0
    volumes:
      - ./frontend/:/home/node/app
    expose:
      - "3000"
    ports:
      - "3000:3000"
    command: "npm start"
  backend:
    image: todo-app/backend
    build: ./backend
    volumes:
      - ./backend/:/usr/src/app
    expose:
      - "3001"
    ports:
      - "8001:3001"
    command: "python src/server.py"
    environment:
      - DEBUG=true
    env_file:
      - path: ./.env
        required: true

nginx 服务

  • 使用 Nginx 1.17 版本的官方镜像。
  • 挂载本地 nginx.conf 配置文件到容器中,以覆盖默认配置。使 Nginx 可以按照自定义配置处理请求。
  • 将容器的 80 端口映射到主机的 8000 端口。这样外部可以通过 localhost:8000 访问 Nginx。
  • 指定 backendfrontend 是 Nginx 服务的依赖项。Nginx 启动时,这两个服务应该先启动。

frontend 服务

  • 使用 node 版本为 22 的镜像。
  • node 用户身份运行,避免使用 root 权限以增强安全性。
  • volumes:将本地的 ./frontend/ 目录挂载到容器的工作目录中,以便同步代码改动。
  • 暴露内部 3000 端口给其他 Docker 容器访问。
  • 将本地端口 3000 映射到容器的 3000 端口,使开发环境可以访问前端应用。
  • command: "npm start":启动前端应用的命令,通常运行开发服务器。

backend 服务

  • build: ./backend:使用 ./backend 目录中的 Dockerfile 来构建该服务的镜像。
  • volumes:将本地的 ./backend/ 目录挂载到容器中的 /usr/src/app,以便实时更新代码。
  • ports:将主机的 8001 端口映射到容器的 3001 端口,使外部可以访问后端 API。
  • environment:设置环境变量,如 DEBUG=true 开启调试模式。

6. 创建nginx文件夹(与backend文件夹同一级),建立nginx.conf文件。

  • 静态资源或前端请求通过 / 路由到 frontend 服务。
  • 后端 API 请求通过 /api 路由到 backend 服务的 API。
server {
    listen 80;
    server_name farm_intro;

    location / {
        proxy_pass http://frontend:3000;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
    }

    location /api {
        proxy_pass http://backend:3001/api;
    }
}
  • listen 80:指定服务器监听在端口 80,这是 HTTP 的默认端口。
  • proxy_pass http://frontend:3000:将请求转发给名为 frontend 的服务,其内部端口为 3000。这里假设 frontend 是一个运行在 Docker Compose 中的 Node.js 服务。

7. 切换到frontend文件夹

npx create-react-app .
npm install axios react-icons
  • create-react-app 是一个官方的 React 脚手架工具,用于快速创建标准的 React 项目结构。
  • axios 是一个用于发送 HTTP 请求的库,常用于从后端 API 获取数据。
  • react-icons 提供了大量常用的图标,可以很方便地在 React 组件中使用。

8. 更新App.js文件

import { useEffect, useState } from "react";
import axios from "axios";
import "./App.css";
import ListToDoLists from "./ListTodoLists";
import ToDoList from "./ToDoList";

function App() {
  const [listSummaries, setListSummaries] = useState(null); 
  // 存储从 /api/lists 获取的所有待办事项列表的摘要。
  
  const [selectedItem, setSelectedItem] = useState(null);
  
上一篇:ubuntu24.04环境源码编译安装nginx 1.20.2及常见问题解决


下一篇:网络安全(黑客)——自学2024