全栈项目实战:待办应用
从零搭建前后端分离的待办应用
- 理解全栈项目结构
- 掌握前后端联调流程
- 实现完整 CRUD
- 理解部署流程
项目概览
就做个经典的待办事项(Todo)应用。技术栈:FastAPI 后端 + SQLite 数据库 + React 前端。
# 项目结构
print("""
todo-app/
├── backend/
│ ├── main.py # FastAPI 应用
│ ├── models.py # 数据库模型
│ ├── database.py # 数据库连接
│ └── requirements.txt
└── frontend/
├── src/
│ ├── App.tsx # 主组件
│ ├── api.ts # API 调用封装
│ └── components/
└── package.json
""")后端实现
# database.py
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker, declarative_base
SQLALCHEMY_DATABASE_URL = "sqlite:///./todos.db"
engine = create_engine(SQLALCHEMY_DATABASE_URL, connect_args={"check_same_thread": False})
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
Base = declarative_base()
# models.py
from sqlalchemy import Column, Integer, String, Boolean
class Todo(Base):
__tablename__ = "todos"
id = Column(Integer, primary_key=True, index=True)
title = Column(String(200), nullable=False)
done = Column(Boolean, default=False)
# main.py
from fastapi import FastAPI, Depends
from pydantic import BaseModel
app = FastAPI()
Base.metadata.create_all(bind=engine)
class TodoCreate(BaseModel):
title: str
class TodoUpdate(BaseModel):
title: str | None = None
done: bool | None = None
@app.get("/api/todos")
def list_todos():
db = SessionLocal()
todos = db.query(Todo).all()
db.close()
return todos
@app.post("/api/todos", status_code=201)
def create_todo(todo: TodoCreate):
db = SessionLocal()
db_todo = Todo(title=todo.title)
db.add(db_todo)
db.commit()
db.refresh(db_todo)
db.close()
return db_todo
@app.put("/api/todos/{todo_id}")
def update_todo(todo_id: int, todo: TodoUpdate):
db = SessionLocal()
db_todo = db.query(Todo).filter(Todo.id == todo_id).first()
if not db_todo:
db.close()
raise HTTPException(404, "不存在")
if todo.title is not None:
db_todo.title = todo.title
if todo.done is not None:
db_todo.done = todo.done
db.commit()
db.close()
return {"ok": True}
@app.delete("/api/todos/{todo_id}")
def delete_todo(todo_id: int):
db = SessionLocal()
db.query(Todo).filter(Todo.id == todo_id).delete()
db.commit()
db.close()
return {"ok": True}前端实现
// api.ts - 封装 API 调用
const API = "http://localhost:8000/api";
export async function fetchTodos() {
const res = await fetch(`${API}/todos`);
return res.json();
}
export async function createTodo(title: string) {
const res = await fetch(`${API}/todos`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ title }),
});
return res.json();
}
export async function toggleTodo(id: number, done: boolean) {
await fetch(`${API}/todos/${id}`, {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ done }),
});
}
export async function deleteTodo(id: number) {
await fetch(`${API}/todos/${id}`, { method: "DELETE" });
}// App.tsx
import { useState, useEffect } from "react";
import { fetchTodos, createTodo, toggleTodo, deleteTodo } from "./api";
function App() {
const [todos, setTodos] = useState([]);
const [input, setInput] = useState("");
useEffect(() => { fetchTodos().then(setTodos); }, []);
const add = async () => {
if (!input.trim()) return;
const todo = await createTodo(input);
setTodos([...todos, todo]);
setInput("");
};
const toggle = async (id, done) => {
await toggleTodo(id, !done);
setTodos(todos.map(t => t.id === id ? {...t, done: !t.done} : t));
};
const remove = async (id) => {
await deleteTodo(id);
setTodos(todos.filter(t => t.id !== id));
};
return (
<div style={{maxWidth:500, margin:"40px auto"}}>
<h1>待办事项</h1>
<div>
<input value={input} onChange={e=>setInput(e.target.value)}
onKeyDown={e=>e.key==="Enter"&&add()} placeholder="添加待办..." />
<button onClick={add}>添加</button>
</div>
<ul>
{todos.map(t => (
<li key={t.id}>
<input type="checkbox" checked={t.done}
onChange={()=>toggle(t.id, t.done)} />
<span style={{textDecoration:t.done?"line-through":"none"}}>{t.title}</span>
<button onClick={()=>remove(t.id)}>删除</button>
</li>
))}
</ul>
</div>
);
}全栈不是什么都懂一点,而是能端到端地交付产品。核心能力:数据建模(表结构设计)API 设计(接口契约)前后端联调(数据格式对齐)错误处理(网络错误、边界情况)部署上线。这个 Todo 项目虽小,但涵盖了全栈开发的全部核心流程。
后端:pip install fastapi uvicorn sqlalchemy && uvicorn main:app --reload;前端:npx create-next-app && npm run dev。前端访问 http://localhost:3000,后端在 http://localhost:8000,记得配置 CORS。
前后端分离架构里,接口一般用什么格式传数据?
资深工程师加餐
底层原理 · 大厂视角 · 工程经验,点卡片展开
穿透是反复查「根本不存在」的数据,用布隆过滤器或缓存空值挡;击穿是某个热点 key 过期瞬间大量请求压向数据库,用互斥重建或逻辑过期;雪崩是大批 key 同时过期,给 TTL 加随机抖动。缓存与库的一致性常用「先更新数据库、再删除缓存」,并靠重试/订阅补偿。
挑战任务
用 Python 完成一次 JSON 收发
后端返回给前端的是 JSON 字符串,前端提交的也是 JSON 字符串。 to_json(todo) 把待办字典序列化为紧凑 JSON,保留中文、键排序。 用 json.loads 解析回来,打印解析后 title 和 done 字段。
课后作业
扩展 Todo 应用
给 Todo 应用加三个东西:① 截止日期字段 ② 优先级(高/中/低)③ 按完成状态筛选。要改后端模型和前端界面。