ChatGPT 代码示例:从 Python 到前端实战演示

下面给你一套“从 Python 到前端”的完整可跑实战示例

  • Python(FastAPI):提供 API + 简单校验 + 错误处理
  • 前端(纯 HTML/JS):调用 API、渲染结果
  • 额外再给一个 React 版组件(同样调用同一 API)

实战目标

做一个“Todo 列表”的小应用:

  • GET /todos:获取列表
  • POST /todos:新增 todo
  • DELETE /todos/{id}:删除 todo

1)Python 后端(FastAPI)代码

1.1 安装

python -m venv .venv
# Windows:
# .venv\Scripts\activate
# macOS/Linux:
# source .venv/bin/activate

pip install fastapi uvicorn pydantic

1.2 保存为 main.py

from typing import List, Optional
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel, Field

app = FastAPI(title="Todo API", version="1.0.0")

class TodoCreate(BaseModel):
    title: str = Field(..., min_length=1, max_length=120)

class Todo(BaseModel):
    id: int
    title: str
    done: bool = False

# 简化:用内存存储(真实项目换数据库)
TODOS: List[Todo] = []
NEXT_ID = 1

@app.get("/health")
def health():
    return {"status": "ok"}

@app.get("/todos", response_model=list[Todo])
def list_todos(q: Optional[str] = None):
    if q:
        q_lower = q.lower()
        return [t for t in TODOS if q_lower in t.title.lower()]
    return TODOS

@app.post("/todos", response_model=Todo, status_code=201)
def create_todo(payload: TodoCreate):
    global NEXT_ID
    title = payload.title.strip()
    if not title:
        raise HTTPException(status_code=400, detail="title cannot be empty")

    todo = Todo(id=NEXT_ID, title=title, done=False)
    NEXT_ID += 1
    TODOS.append(todo)
    return todo

@app.delete("/todos/{todo_id}", status_code=204)
def delete_todo(todo_id: int):
    idx = next((i for i, t in enumerate(TODOS) if t.id == todo_id), None)
    if idx is None:
        raise HTTPException(status_code=404, detail="todo not found")
    TODOS.pop(idx)
    return None

1.3 运行

uvicorn main:app --reload --port 8000

打开:

  • http://127.0.0.1:8000/health
  • http://127.0.0.1:8000/docs(Swagger)

2)前端(纯 HTML + JS)实战:调用 API 并渲染

保存为 index.html,用浏览器打开即可(推荐用 VSCode Live Server 或任意静态服务器打开,避免某些浏览器跨域限制)。

<!doctype html>
<html lang="zh-CN">
<head>
  <meta charset="utf-8" />
  <meta name="viewport" content="width=device-width,initial-scale=1" />
  <title>Todo Demo</title>
  <style>
    body { font-family: system-ui, -apple-system, Segoe UI, Roboto, Arial; max-width: 720px; margin: 40px auto; padding: 0 16px; }
    h1 { margin-bottom: 10px; }
    form { display: flex; gap: 8px; margin: 12px 0 16px; }
    input { flex: 1; padding: 10px; font-size: 16px; }
    button { padding: 10px 14px; font-size: 16px; cursor: pointer; }
    ul { padding-left: 18px; }
    li { display: flex; align-items: center; justify-content: space-between; padding: 8px 0; border-bottom: 1px solid #eee; }
    .meta { color: #666; font-size: 13px; }
    .error { color: #b00020; margin-top: 10px; }
  </style>
</head>
<body>
  <h1>Todo Demo</h1>
  <div class="meta">后端 API:<code id="apiBase"></code></div>

  <form id="todoForm">
    <input id="titleInput" placeholder="输入一个 todo,比如:学习 FastAPI" />
    <button type="submit">添加</button>
  </form>

  <div>
    <button id="refreshBtn" type="button">刷新列表</button>
  </div>

  <ul id="todoList"></ul>
  <div id="error" class="error"></div>

  <script>
    const API_BASE = "http://127.0.0.1:8000";
    document.getElementById("apiBase").textContent = API_BASE;

    const todoList = document.getElementById("todoList");
    const errorBox = document.getElementById("error");
    const titleInput = document.getElementById("titleInput");

    function setError(msg) {
      errorBox.textContent = msg || "";
    }

    async function fetchJSON(url, options = {}) {
      const res = await fetch(url, {
        ...options,
        headers: {
          "Content-Type": "application/json",
          ...(options.headers || {}),
        },
      });

      // 204 No Content
      if (res.status === 204) return null;

      const text = await res.text();
      const data = text ? JSON.parse(text) : null;

      if (!res.ok) {
        const detail = data?.detail || `HTTP ${res.status}`;
        throw new Error(detail);
      }
      return data;
    }

    function renderTodos(todos) {
      todoList.innerHTML = "";
      for (const t of todos) {
        const li = document.createElement("li");

        const left = document.createElement("div");
        left.textContent = `#${t.id}  ${t.title}`;

        const delBtn = document.createElement("button");
        delBtn.textContent = "删除";
        delBtn.addEventListener("click", async () => {
          try {
            setError("");
            await fetchJSON(`${API_BASE}/todos/${t.id}`, { method: "DELETE" });
            await loadTodos();
          } catch (e) {
            setError(e.message);
          }
        });

        li.appendChild(left);
        li.appendChild(delBtn);
        todoList.appendChild(li);
      }
    }

    async function loadTodos() {
      try {
        setError("");
        const todos = await fetchJSON(`${API_BASE}/todos`);
        renderTodos(todos);
      } catch (e) {
        setError(e.message);
      }
    }

    document.getElementById("todoForm").addEventListener("submit", async (ev) => {
      ev.preventDefault();
      try {
        setError("");
        const title = titleInput.value;
        await fetchJSON(`${API_BASE}/todos`, {
          method: "POST",
          body: JSON.stringify({ title }),
        });
        titleInput.value = "";
        await loadTodos();
      } catch (e) {
        setError(e.message);
      }
    });

    document.getElementById("refreshBtn").addEventListener("click", loadTodos);

    // 初次加载
    loadTodos();
  </script>
</body>
</html>

3)React 版本(可选):同一个 API 的前端组件

假设你已经有 React 项目(Vite/Next 都行)。下面是一个最小可用组件。

import { useEffect, useState } from "react";

const API_BASE = "http://127.0.0.1:8000";

export default function TodoApp() {
  const [title, setTitle] = useState("");
  const [todos, setTodos] = useState([]);
  const [error, setError] = useState("");

  async function fetchJSON(url, options = {}) {
    const res = await fetch(url, {
      ...options,
      headers: { "Content-Type": "application/json", ...(options.headers || {}) },
    });

    if (res.status === 204) return null;

    const text = await res.text();
    const data = text ? JSON.parse(text) : null;

    if (!res.ok) throw new Error(data?.detail || `HTTP ${res.status}`);
    return data;
  }

  async function loadTodos() {
    try {
      setError("");
      const data = await fetchJSON(`${API_BASE}/todos`);
      setTodos(data);
    } catch (e) {
      setError(e.message);
    }
  }

  async function addTodo(e) {
    e.preventDefault();
    try {
      setError("");
      await fetchJSON(`${API_BASE}/todos`, {
        method: "POST",
        body: JSON.stringify({ title }),
      });
      setTitle("");
      await loadTodos();
    } catch (e) {
      setError(e.message);
    }
  }

  async function delTodo(id) {
    try {
      setError("");
      await fetchJSON(`${API_BASE}/todos/${id}`, { method: "DELETE" });
      await loadTodos();
    } catch (e) {
      setError(e.message);
    }
  }

  useEffect(() => {
    loadTodos();
  }, []);

  return (
    <div style={{ maxWidth: 720, margin: "40px auto", padding: 16, fontFamily: "system-ui" }}>
      <h1>Todo Demo (React)</h1>
      <div style={{ color: "#666", fontSize: 13 }}>
        API: <code>{API_BASE}</code>
      </div>

      <form onSubmit={addTodo} style={{ display: "flex", gap: 8, marginTop: 12 }}>
        <input
          value={title}
          onChange={(e) => setTitle(e.target.value)}
          placeholder="输入 todo"
          style={{ flex: 1, padding: 10, fontSize: 16 }}
        />
        <button type="submit" style={{ padding: "10px 14px", fontSize: 16 }}>
          添加
        </button>
      </form>

      <button onClick={loadTodos} style={{ marginTop: 12, padding: "8px 12px" }}>
        刷新
      </button>

      {error ? <div style={{ color: "#b00020", marginTop: 10 }}>{error}</div> : null}

      <ul style={{ marginTop: 12 }}>
        {todos.map((t) => (
          <li key={t.id} style={{ display: "flex", justifyContent: "space-between", padding: "8px 0", borderBottom: "1px solid #eee" }}>
            <span>#{t.id} {t.title}</span>
            <button onClick={() => delTodo(t.id)}>删除</button>
          </li>
        ))}
      </ul>
    </div>
  );
}

4)常见问题(你跑不起来通常是这些原因)

  1. 跨域(CORS)报错
    如果你前端不是同域访问后端,FastAPI 需要加 CORS。你可以在 main.py 加:
from fastapi.middleware.cors import CORSMiddleware

app.add_middleware(
    CORSMiddleware,
    allow_origins=["*"],  # 生产环境要收紧到你的域名
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)
  1. 端口不一致
    确认前端里的 API_BASE 和你 uvicorn 运行的端口一致。
  2. 打开 HTML 的方式
    建议用本地静态服务器打开(比如 VSCode Live Server),更稳定。

标签