目录
项目概述
我们将创建一个简单的待办事项(Todo)应用程序,包含以下组件:
- MySQL 数据库:存储待办事项
- FastAPI 后端:提供 RESTful API
- React 前端:用户界面
项目结构
mysql_fastapi_react/
├── backend/
│ ├── app/
│ │ ├── __init__.py
│ │ ├── main.py
│ │ ├── models.py
│ │ ├── database.py
│ │ └── routers/
│ │ └── todos.py
│ ├── requirements.txt
│ └── Dockerfile
├── frontend/
│ ├── src/
│ │ ├── App.jsx
│ │ ├── main.jsx
│ │ └── components/
│ │ └── TodoList.jsx
│ ├── package.json
│ └── index.html
├── docker-compose.yml
└── README.md
步骤 1:创建项目目录结构
首先,让我们创建项目的基本目录结构:
mkdir -p backend/app/routers
mkdir -p frontend/src/components
步骤 2:设置 MySQL 和 Docker
创建 docker-compose.yml
文件:
version: '3'
services:
db:
image: mysql:8.0
restart: always
environment:
MYSQL_DATABASE: tododb
MYSQL_USER: todouser
MYSQL_PASSWORD: todopassword
MYSQL_ROOT_PASSWORD: rootpassword
ports:
- "3306:3306"
volumes:
- mysql_data:/var/lib/mysql
networks:
- app-network
backend:
build: ./backend
restart: always
ports:
- "8000:8000"
depends_on:
- db
environment:
- DATABASE_URL=mysql+pymysql://todouser:todopassword@db:3306/tododb
networks:
- app-network
frontend:
build: ./frontend
restart: always
ports:
- "5173:5173"
depends_on:
- backend
networks:
- app-network
networks:
app-network:
driver: bridge
volumes:
mysql_data:
步骤 3:设置 FastAPI 后端
创建 requirements.txt
fastapi
uvicorn
sqlalchemy
pymysql
cryptography
pydantic
创建 Dockerfile
FROM python:3.12-slim
WORKDIR /app
# 假设 requirements.txt 在 backend 目录下
COPY requirements.txt .
RUN pip install -r requirements.txt
# 将整个 app 目录复制到容器中
COPY . .
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
创建数据库连接
from sqlalchemy import create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker
import os
DATABASE_URL = os.getenv("DATABASE_URL", "mysql+pymysql://todouser:todopassword@localhost:3306/tododb")
engine = create_engine(DATABASE_URL)
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
Base = declarative_base()
# 依赖项,用于获取数据库会话
def get_db():
db = SessionLocal()
try:
yield db
finally:
db.close()
创建数据模型
from sqlalchemy import Boolean, Column, Integer, String, DateTime
from sqlalchemy.sql import func
from .database import Base
class Todo(Base):
__tablename__ = "todos"
id = Column(Integer, primary_key=True, index=True)
title = Column(String(100), nullable=False)
description = Column(String(255))
completed = Column(Boolean, default=False)
created_at = Column(DateTime(timezone=True), server_default=func.now())
updated_at = Column(
DateTime(timezone=True),
server_default=func.now(), # ✅ 新增
onupdate=func.now(),
nullable=False # 确保不为空
)
创建路由
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.orm import Session
from typing import List
from pydantic import BaseModel
from datetime import datetime
from .. import models
from ..database import get_db
router = APIRouter(
prefix="/todos",
tags=["todos"],
)
# Pydantic 模型用于请求和响应
class TodoBase(BaseModel):
title: str
description: str = None
completed: bool = False
class TodoCreate(TodoBase):
pass
class TodoResponse(TodoBase):
id: int
created_at: datetime
updated_at: datetime
class Config:
from_attributes = True
@router.get("/", response_model=List[TodoResponse])
def get_todos(skip: int = 0, limit: int = 100, db: Session = Depends(get_db)):
todos = db.query(models.Todo).offset(skip).limit(limit).all()
return todos
@router.post("/", response_model=TodoResponse, status_code=status.HTTP_201_CREATED)
def create_todo(todo: TodoCreate, db: Session = Depends(get_db)):
db_todo = models.Todo(**todo.model_dump()) # 使用 model_dump() 替代 dict()
db.add(db_todo)
db.commit()
db.refresh(db_todo)
return db_todo
@router.get("/{todo_id}", response_model=TodoResponse)
def get_todo(todo_id: int, db: Session = Depends(get_db)):
db_todo = db.query(models.Todo).filter(models.Todo.id == todo_id).first()
if db_todo is None:
raise HTTPException(status_code=404, detail="Todo not found")
return db_todo
@router.put("/{todo_id}", response_model=TodoResponse)
def update_todo(todo_id: int, todo: TodoBase, db: Session = Depends(get_db)):
db_todo = db.query(models.Todo).filter(models.Todo.id == todo_id).first()
if db_todo is None:
raise HTTPException(status_code=404, detail="Todo not found")
for key, value in todo.model_dump().items(): # 使用 model_dump() 替代 dict()
setattr(db_todo, key, value)
db.commit()
db.refresh(db_todo)
return db_todo
@router.delete("/{todo_id}", status_code=status.HTTP_204_NO_CONTENT)
def delete_todo(todo_id: int, db: Session = Depends(get_db)):
db_todo = db.query(models.Todo).filter(models.Todo.id == todo_id).first()
if db_todo is None:
raise HTTPException(status_code=404, detail="Todo not found")
db.delete(db_todo)
db.commit()
return None
创建主应用
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from .routers import todos
from . import models
from .database import engine
# 创建数据库表
models.Base.metadata.create_all(bind=engine)
app = FastAPI(title="Todo API")
# 配置 CORS
app.add_middleware(
CORSMiddleware,
allow_origins=["*"], # 在生产环境中应该限制为前端域名
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# 包含路由
app.include_router(todos.router)
@app.get("/")
def read_root():
return {"message": "Welcome to Todo API"}
创建 init.py 文件
# 初始化包
步骤 4:设置 React 前端
创建 package.json
{
"name": "todo-frontend",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"axios": "^1.6.0",
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
"devDependencies": {
"@types/react": "^18.2.15",
"@types/react-dom": "^18.2.7",
"@vitejs/plugin-react": "^4.0.3",
"vite": "^4.4.5"
}
}
创建 index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Todo App</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.jsx"></script>
</body>
</html>
创建 main.jsx
import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App.jsx'
ReactDOM.createRoot(document.getElementById('root')).render(
<React.StrictMode>
<App />
</React.StrictMode>,
)
创建 App.jsx
import React from 'react';
import TodoList from './components/TodoList';
function App() {
return (
<div className="App" style={{ maxWidth: '800px', margin: '0 auto', padding: '20px' }}>
<h1>待办事项应用</h1>
<TodoList />
</div>
);
}
export default App;
创建 TodoList 组件
import React, { useState, useEffect } from 'react';
import axios from 'axios';
const API_URL = 'http://localhost:8000';
function TodoList() {
const [todos, setTodos] = useState([]);
const [title, setTitle] = useState('');
const [description, setDescription] = useState('');
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
fetchTodos();
}, []);
const fetchTodos = async () => {
try {
setLoading(true);
const response = await axios.get(`${API_URL}/todos/`);
setTodos(response.data);
setError(null);
} catch (err) {
setError('获取待办事项失败');
console.error('获取待办事项失败:', err);
} finally {
setLoading(false);
}
};
const addTodo = async (e) => {
e.preventDefault();
if (!title.trim()) return;
try {
const response = await axios.post(`${API_URL}/todos/`, {
title,
description,
completed: false
});
setTodos([...todos, response.data]);
setTitle('');
setDescription('');
} catch (err) {
setError('添加待办事项失败');
console.error('添加待办事项失败:', err);
}
};
const toggleTodo = async (todo) => {
try {
const updatedTodo = { ...todo, completed: !todo.completed };
await axios.put(`${API_URL}/todos/${todo.id}`, updatedTodo);
setTodos(todos.map(t => t.id === todo.id ? { ...t, completed: !t.completed } : t));
} catch (err) {
setError('更新待办事项失败');
console.error('更新待办事项失败:', err);
}
};
const deleteTodo = async (id) => {
try {
await axios.delete(`${API_URL}/todos/${id}`);
setTodos(todos.filter(todo => todo.id !== id));
} catch (err) {
setError('删除待办事项失败');
console.error('删除待办事项失败:', err);
}
};
if (loading) return <p>加载中...</p>;
return (
<div>
{error && <p style={{ color: 'red' }}>{error}</p>}
<form onSubmit={addTodo} style={{ marginBottom: '20px' }}>
<div style={{ marginBottom: '10px' }}>
<input
type="text"
value={title}
onChange={(e) => setTitle(e.target.value)}
placeholder="标题"
style={{ padding: '8px', width: '300px' }}
required
/>
</div>
<div style={{ marginBottom: '10px' }}>
<textarea
value={description}
onChange={(e) => setDescription(e.target.value)}
placeholder="描述"
style={{ padding: '8px', width: '300px', height: '80px' }}
/>
</div>
<button
type="submit"
style={{
padding: '8px 16px',
backgroundColor: '#4CAF50',
color: 'white',
border: 'none',
cursor: 'pointer'
}}
>
添加待办事项
</button>
</form>
<h2>待办事项列表</h2>
{todos.length === 0 ? (
<p>没有待办事项</p>
) : (
<ul style={{ listStyleType: 'none', padding: 0 }}>
{todos.map((todo) => (
<li
key={todo.id}
style={{
padding: '10px',
border: '1px solid #ddd',
marginBottom: '10px',
backgroundColor: todo.completed ? '#f8f8f8' : 'white',
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center'
}}
>
<div>
<h3 style={{
textDecoration: todo.completed ? 'line-through' : 'none',
margin: '0 0 5px 0'
}}>
{todo.title}
</h3>
<p style={{ margin: '0' }}>{todo.description}</p>
</div>
<div>
<button
onClick={() => toggleTodo(todo)}
style={{
marginRight: '5px',
padding: '5px 10px',
backgroundColor: todo.completed ? '#2196F3' : '#4CAF50',
color: 'white',
border: 'none',
cursor: 'pointer'
}}
>
{todo.completed ? '标记为未完成' : '标记为完成'}
</button>
<button
onClick={() => deleteTodo(todo.id)}
style={{
padding: '5px 10px',
backgroundColor: '#f44336',
color: 'white',
border: 'none',
cursor: 'pointer'
}}
>
删除
</button>
</div>
</li>
))}
</ul>
)}
</div>
);
}
export default TodoList;
创建 Dockerfile
FROM node:18-alpine
WORKDIR /app
COPY package.json .
RUN npm install
COPY . .
EXPOSE 5173
CMD ["npm", "run", "dev", "--", "--host", "0.0.0.0"]
步骤 5:创建 README.md
# MySQL + FastAPI + React 示例项目
这是一个简单的待办事项应用,展示了 MySQL、FastAPI 和 React 的交互过程。
## 技术栈
- **数据库**: MySQL 8.0 (Docker)
- **后端**: FastAPI + SQLAlchemy
- **前端**: React + Vite
- **包管理**: uv (Python), npm (JavaScript)
- **部署**: Docker Compose
## 项目结构
mysql_fastapi_react/
├── backend/ # FastAPI 后端
├── frontend/ # React 前端
└── docker-compose.yml # Docker 配置
## 启动项目
1. 确保已安装 Docker 和 Docker Compose
2. 启动所有服务:
```bash
docker-compose up -d```
3. 访问应用:
- 前端: http://localhost:5173
- 后端 API: http://localhost:8000
- API 文档: http://localhost:8000/docs
## 开发说明
### 后端开发
后端使用 FastAPI 框架,通过 SQLAlchemy ORM 连接 MySQL 数据库。
### 前端开发
前端使用 React 框架,通过 Axios 与后端 API 通信。
## 交互流程
1. React 前端通过 Axios 发送 HTTP 请求到 FastAPI 后端
2. FastAPI 处理请求,与 MySQL 数据库交互
3. FastAPI 返回 JSON 响应给前端
4. React 更新 UI 显示数据
如何运行项目
-
确保你已经安装了 Docker 和 Docker Compose
-
在项目根目录下运行以下命令启动所有服务:
docker-compose up -d
- 访问应用:
- 前端: http://localhost:5173
- 后端 API: http://localhost:8000
- API 文档: http://localhost:8000/docs
交互过程说明
这个项目展示了三个组件之间的交互过程:
-
前端 (React) 与后端 (FastAPI) 的交互:
- React 前端通过 Axios 发送 HTTP 请求到 FastAPI 后端
- FastAPI 处理请求并返回 JSON 响应
- React 更新 UI 显示数据
-
后端 (FastAPI) 与数据库 (MySQL) 的交互:
- FastAPI 使用 SQLAlchemy ORM 连接 MySQL 数据库
- 通过 SQLAlchemy 模型定义数据结构
- 使用 SQLAlchemy 会话执行 CRUD 操作
-
数据流程:
- 用户在 React 界面上执行操作 (如添加待办事项)
- React 发送 HTTP POST 请求到 FastAPI
- FastAPI 接收请求,验证数据
- FastAPI 使用 SQLAlchemy 将数据存储到 MySQL
- MySQL 保存数据并返回结果
- FastAPI 将结果转换为 JSON 并返回给 React
- React 更新 UI 显示新添加的待办事项
这个简单的项目展示了现代全栈应用的基本架构和交互方式。