30 分钟
全栈开发

全栈项目实战:待办应用

从零搭建前后端分离的待办应用

  • 理解全栈项目结构
  • 掌握前后端联调流程
  • 实现完整 CRUD
  • 理解部署流程

项目概览

就做个经典的待办事项(Todo)应用。技术栈:FastAPI 后端 + SQLite 数据库 + React 前端。

Python
# 项目结构
print("""
todo-app/
├── backend/
│   ├── main.py          # FastAPI 应用
│   ├── models.py        # 数据库模型
│   ├── database.py      # 数据库连接
│   └── requirements.txt
└── frontend/
    ├── src/
    │   ├── App.tsx      # 主组件
    │   ├── api.ts       # API 调用封装
    │   └── components/
    └── package.json
""")

后端实现

Python
# 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}

前端实现

TypeScript
// 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" });
}
JavaScript
// 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 收发

简单+50 XP

后端返回给前端的是 JSON 字符串,前端提交的也是 JSON 字符串。 to_json(todo) 把待办字典序列化为紧凑 JSON,保留中文、键排序。 用 json.loads 解析回来,打印解析后 title 和 done 字段。

用 Python 完成一次 JSON 收发
1 个测试用例

课后作业

扩展 Todo 应用

中等+40 XP

给 Todo 应用加三个东西:① 截止日期字段 ② 优先级(高/中/低)③ 按完成状态筛选。要改后端模型和前端界面。

扩展 Todo 应用
1 个测试用例