Developing a Single Page App with FastAPI and React

在本教程中,您将使用 FastAPIReact 构建一个 CRUD 应用程序。

在使用 FastAPI 构建后端 RESTful API 之前,我们将首先使用 Vite 搭建一个新的 React 应用程序。最后,我们将开发后端 CRUD 路由以及前端 React 组件。对于样式,我们将使用 Chakra UI,这是一个模块化组件库。

最终应用

最终的 Todo 应用程序

依赖:

  • 反应 v19.0.0
  • Vite v6.1.1 版本
  • 节点 v22.13.1
  • npm v10.9.2
  • FastAPI 版本 0.115.7
  • Python 版 3.13.1

在开始本教程之前,您应该熟悉 React 的工作原理。要快速复习 React,请查看快速入门指南中的主要概念,如果您是 React 的新手,请查看井字棋教程进行一些动手练习。

Contents

目标

在本教程结束时,您将能够:

  1. 使用 Python 和 FastAPI 开发 RESTful API
  2. 使用 Vite 搭建 React 项目的基架
  3. 使用 React Context API 和 Hook 管理 state作
  4. 在浏览器中创建和渲染 React 功能组件
  5. 将 React 应用程序连接到 FastAPI 后端

什么是 FastAPI?

FastAPI 是一个 Python Web 框架,旨在构建快速高效的后端 API。它处理同步和异步作,并内置了对数据验证、身份验证和由 OpenAPI 提供支持的交互式 API 文档的支持。

有关 FastAPI 的更多信息,请查看以下资源:

  1. 官方文档

什么是 React?

React 是一个开源的、基于组件的 JavaScript UI 库,用于构建前端应用程序。

有关更多信息,请查看 React 思考教程,以了解 React 的工作原理。

设置 FastAPI

首先创建一个名为 “fastapi-react” 的新文件夹来保存你的项目:

$ mkdir fastapi-react
$ cd fastapi-react

在 “fastapi-react” 文件夹中,创建一个新文件夹来存放后端:

$ mkdir backend
$ cd backend

接下来,创建并激活虚拟环境:

$ python3.13 -m venv venv
$ source venv/bin/activate
$ export PYTHONPATH=$PWD

随意将 venv 和 Pip 换成 PoetryPipenv。有关更多信息,请查看现代 Python 环境

安装 FastAPI:

(venv)$ pip install fastapi==0.115.7 uvicorn==0.34.0

Uvicorn 是一个与 ASGI(异步服务器网关接口)兼容的服务器,将用于建立后端 API。

接下来,在 “backend” 文件夹中创建以下文件和文件夹:

└── backend
    ├── main.py
    └── app
        ├── __init__.py
        └── api.py

main.py 文件中,定义用于运行应用程序的入口点:

import uvicorn

if __name__ == "__main__":
    uvicorn.run("app.api:app", host="0.0.0.0", port=8000, reload=True)

在这里,我们指示文件在端口 8000 上运行 Uvicorn 服务器,并在每次文件更改时重新加载。

在通过入口点文件启动服务器之前,请在 backend/app/api.py 中创建一个 base 路由:

from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware

app = FastAPI()

origins = [
    "http://localhost:5173",
    "localhost:5173"
]

app.add_middleware(
    CORSMiddleware,
    allow_origins=origins,
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"]
)

@app.get("/", tags=["root"])
async def read_root() -> dict:
    return {"message": "Welcome to your todo list."}

为什么我们需要 CORSMiddleware?为了发出跨域请求——即来自不同协议、IP 地址、域名或端口的请求——你需要启用跨域资源共享 (CORS)。FastAPI 的内置功能为我们处理了这个问题。CORSMiddleware

上述配置将允许来自我们的前端域和端口的跨域请求,这些请求将在 .localhost:5173

有关在 FastAPI 中处理 CORS 的更多信息,请查看官方文档

从控制台运行入口点文件:

(venv)$ python main.py

在浏览器中导航到 http://localhost:8000。您应该会看到:

{
  "message": "Welcome to your todo list."
}

设置 React

让我们用 Vite 搭建一个新的 React 应用程序。

打开一个新的终端窗口,导航到项目目录,然后生成一个新的 React 应用程序:

$ npm create [email protected]

回答以继续,并提供以下信息:y

Project name: frontend
Select a framework: › React
Select a variant: › TypeScript

安装依赖项:

$ cd frontend
$ npm install

接下来,安装一个名为 Chakra UI 的 UI 组件库:

$ npm install @chakra-ui/[email protected]
$ npm install @emotion/[email protected] @emotion/[email protected] [email protected]

安装后,在 “src” 文件夹中创建一个名为 “components” 的新文件夹,该文件夹将用于保存应用程序的组件,以及两个组件 Header.tsxTodos.tsx

$ cd src
$ mkdir components
$ cd components
$ touch {Header,Todos}.tsx

打开 frontend/src/index.css 并将其替换为以下代码:

:root{
  font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
  line-height: 1.5;
  font-weight: 400;

  color-scheme: light dark;
  color: rgba(255, 255, 255, 0.87);
  background-color: #242424;

  font-synthesis: none;
  text-rendering: optimizeLegibility;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

[data-scope="dialog"] {
  background: #1a1a1a !important;
  --bg-currentcolor: #1a1a1a !important;
}

button {
  background-color: #1a1a1a;
  color: #ffffff;
}

input::placeholder {
  color: #888888;
  opacity: 1;
}

/* For Firefox */
input::-moz-placeholder {
  color: #888888;
  opacity: 1;
}

/* For Chrome/Safari/Opera */
input::-webkit-input-placeholder {
  color: #888888;
}

/* For Internet Explorer */
input:-ms-input-placeholder {
  color: #888888;
}

a {
  font-weight: 500;
  color: #646cff;
  text-decoration: inherit;
}
a:hover {
  color: #535bf2;
}

h1 {
  font-size: 3.2em;
  line-height: 1.1;
}

button {
  border-radius: 8px;
  border: 1px solid transparent;
  padding: 0.6em 1.2em;
  font-size: 1em;
  font-weight: 500;
  font-family: inherit;
  background-color: #1a1a1a;
  cursor: pointer;
  transition: border-color 0.25s;
}
button:hover {
  border-color: #646cff;
}
button:focus,
button:focus-visible {
  outline: 4px auto -webkit-focus-ring-color;
}

现在让我们开始在 Header.tsx 文件中构建组件:Header

import React from "react";
import { Heading, Flex, Separator } from "@chakra-ui/react";

const Header = () => {
  return (

        Todos

  );
};

export default Header;

从 Chakra UI 导入 React 和 HeadingFlexSeparator 组件后,我们定义了一个功能组件来渲染基本标头。然后导出该组件以在基本组件中使用。

接下来,让我们将 frontend/src/App.tsx 文件替换为以下代码:

import { ChakraProvider } from '@chakra-ui/react'
import { defaultSystem } from "@chakra-ui/react"
import Header from "./components/Header";

function App() {

  return (

      
) } export default App;

从 Chakra UI 库导入的 ChakraProvider 作为其他使用 Chakra UI 的组件的父组件。它通过 React 的 Context API 为所有子组件(在本例中)提供主题。Header

请注意,我们从 Chakra UI 库导入了主题。您可以创建自己的自定义主题并将其传递给组件,要了解有关主题的更多信息,请查看官方 自定义文档defaultSystemChakraProvider

从终端启动您的 React 应用程序:

$ npm run dev

这将在 http://localhost:5173/ 的默认浏览器中打开 React 应用程序。您应该会看到:

Todo 应用程序

我们在构建什么?

在本教程的其余部分,您将构建一个 todo CRUD 应用程序,用于创建、读取、更新和删除 todo。最后,您的应用程序将如下所示:

最终的 Todo 应用程序

GET 路由

后端

首先将 todos 列表添加到 backend/app/api.py

todos = [
    {
        "id": "1",
        "item": "Read a book."
    },
    {
        "id": "2",
        "item": "Cycle around town."
    }
]

上面的列表只是本教程中使用的虚拟数据。数据只是代表单个 todos 的结构。你可以随意连接一个数据库并将 todos 存储在那里。

然后,添加路由处理程序:

@app.get("/todo", tags=["todos"])
async def get_todos() -> dict:
    return { "data": todos }

http://localhost:8000/todo 手动测试新路由。另请查看 http://localhost:8000/docs 上的交互式文档:

应用文档

前端

Todos.tsx 组件中,首先导入 React、and 钩子和一些 Chakra UI 组件:useState()useEffect()

import React, { useEffect, useState, createContext, useContext } from "react";
import {
  Box,
  Button,
  Container,
  Flex,
  Input,
  DialogBody,
  DialogContent,
  DialogFooter,
  DialogHeader,
  DialogRoot,
  DialogTitle,
  DialogTrigger,
  Stack,
  Text,
  DialogActionTrigger,
} from "@chakra-ui/react";

钩子负责管理我们的客户端组件状态,而钩子允许我们在组件挂载到 DOM 时执行数据获取等作。useStateuseEffect

有关 React Hook 的更多信息,请查看官方文档中的 React Hook 入门教程和内置 React Hooks

现在,让我们定义我们的接口:Todo

interface Todo {
  id: string;
  item: string;
}

该接口用于定义将传递给组件的数据的形状。TodoTodos

接下来,创建一个上下文来管理所有组件中的全局状态活动:

const TodosContext = createContext({
  todos: [], fetchTodos: () => {}
})

在上面的代码块中,我们通过 createContext 定义了一个上下文对象,该对象接受两个提供程序值:和 .该函数将在下一个代码块中定义。todosfetchTodosfetchTodos

想了解更多关于使用 React Context API 管理状态的信息吗?查看 React Context API:轻松管理状态 一文。

接下来,添加组件:Todos

export default function Todos() {
  const [todos, setTodos] = useState([])
  const fetchTodos = async () => {
    const response = await fetch("http://localhost:8000/todo")
    const todos = await response.json()
    setTodos(todos.data)
  }
}

在这里,我们创建了一个空的状态变量数组 ,和一个状态方法 ,以便我们可以更新 state 变量。接下来,我们定义了一个函数,该函数用于从后端异步检索 todos,并在函数末尾更新 state 变量。todossetTodosfetchTodostodo

接下来,在组件中,使用函数检索 todos 并通过迭代 todos 状态变量来渲染数据:TodosfetchTodos

useEffect(() => {
  fetchTodos()
}, [])

return (

        {todos.map((todo: Todo) => (
          {todo.item}
        ))}

)

Todos.tsx 现在应该如下所示:

import React, { useEffect, useState, createContext, useContext } from "react";
import {
  Box,
  Button,
  Container,
  Flex,
  Input,
  DialogBody,
  DialogContent,
  DialogFooter,
  DialogHeader,
  DialogRoot,
  DialogTitle,
  DialogTrigger,
  Stack,
  Text,
  DialogActionTrigger,
} from "@chakra-ui/react";

interface Todo {
  id: string;
  item: string;
}

const TodosContext = createContext({
  todos: [], fetchTodos: () => {}
})

export default function Todos() {
  const [todos, setTodos] = useState([])
  const fetchTodos = async () => {
    const response = await fetch("http://localhost:8000/todo")
    const todos = await response.json()
    setTodos(todos.data)
  }
  useEffect(() => {
    fetchTodos()
  }, [])

  return (

          {todos.map((todo: Todo) => (
            {todo.item}
          ))}

  )
}

App.tsx 文件中导入组件并渲染它:Todos

import { ChakraProvider } from '@chakra-ui/react'
import { defaultSystem } from "@chakra-ui/react"
import Header from "./components/Header";
import Todos from "./components/Todos";  // new

function App() {

  return (

      
{/* new */} ) } export default App;

http://localhost:5173 上的应用程序现在应如下所示:

Todo 应用程序

尝试将一个新的 todo 添加到 backend/app/api.py 中的列表中。刷新浏览器。你应该会看到新的 todo。这样,我们就完成了检索所有 todo 的 GET 请求。todos

POST 路由

后端

首先添加新的路由处理程序来处理将新 todo 添加到 backend/app/api.py 的 POST 请求:

@app.post("/todo", tags=["todos"])
async def add_todo(todo: dict) -> dict:
    todos.append(todo)
    return {
        "data": { "Todo added." }
    }

在后端运行的情况下,您可以使用以下方法在新的终端选项卡中测试 POST 路由:curl

$ curl -X POST http://localhost:8000/todo -d 
    '{"id": "3", "item": "Buy some testdriven courses."}' 
    -H 'Content-Type: application/json'

您应该会看到:

{
    "data: [
        "Todo added."
    ]"
}

您还应该在来自 http://localhost:8000/todo 终端节点的响应以及 http://localhost:5173 中看到新的 todo。

作为练习,实施检查以防止添加重复的 todo 项。

前端

首先添加 shell,用于将新 todo 添加到 frontend/src/components/Todos.tsx

function AddTodo() {
  const [item, setItem] = React.useState("")
  const {todos, fetchTodos} = React.useContext(TodosContext)
}

在这里,我们创建了一个新的 state 变量,它将保存表单中的值。我们还检索了 context 值和 .todosfetchTodos

接下来,将用于从表单获取输入并处理表单提交的函数添加到 :AddTodo

const handleInput = (event: React.ChangeEvent) => {
  setItem(event.target.value)
}

const handleSubmit = (event: React.FormEvent) => {
  event.preventDefault()
  const newTodo = {
    "id": todos.length + 1,
    "item": item
  }

  fetch("http://localhost:8000/todo", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify(newTodo)
  }).then(fetchTodos)
}

在该函数中,我们添加了一个 POST 请求,并将带有 todo 信息的数据发送到服务器。然后我们调用 update 。handleSubmitfetchTodostodos

React.ChangeEvent是表示 input 元素的事件对象的类型。

紧跟在函数之后,返回要渲染的表单:handleSubmit

return (

)

在上面的代码块中,我们将表单事件侦听器设置为我们之前创建的函数。待办事项值也会随着输入值的变化而通过侦听器更新。onSubmithandleSubmitonChange

完整的组件现在应如下所示:AddTodo

function AddTodo() {
  const [item, setItem] = React.useState("")
  const {todos, fetchTodos} = React.useContext(TodosContext)

  const handleInput = (event: React.ChangeEvent) => {
    setItem(event.target.value)
  }

  const handleSubmit = (event: React.FormEvent) => {
    event.preventDefault()
    const newTodo = {
      "id": todos.length + 1,
      "item": item
    }

    fetch("http://localhost:8000/todo", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify(newTodo)
    }).then(fetchTodos)
  }

  return (

  )
}

接下来,将组件添加到组件中,如下所示:AddTodoTodos

export default function Todos() {
  const [todos, setTodos] = useState([])
  const fetchTodos = async () => {
    const response = await fetch("http://localhost:8000/todo")
    const todos = await response.json()
    setTodos(todos.data)
  }
  useEffect(() => {
    fetchTodos()
  }, [])

  return (

        {/* new */}

          {todos.map((todo: Todo) => (
            {todo.item}
          ))}

  )
}

前端应用程序应如下所示:

Todo 应用程序

通过添加 todo 来测试表单:

添加新的待办事项

PUT 路由

后端

添加更新路由:

@app.put("/todo/{id}", tags=["todos"])
async def update_todo(id: int, body: dict) -> dict:
    for todo in todos:
        if int(todo["id"]) == id:
            todo["item"] = body["item"]
            return {
                "data": f"Todo with id {id} has been updated."
            }

    return {
        "data": f"Todo with id {id} not found."
    }

因此,我们检查了 ID 与提供的 ID 匹配的 todo,然后,如果找到,则使用请求正文中的值更新了 todo 的 item。

前端

首先在 frontend/src/components/Todos.tsx 中定义组件并传递两个 prop 值,然后传递给它:UpdateTodoitemid

const UpdateTodo = ({ item, id, fetchTodos }: UpdateTodoProps) => {
  const [todo, setTodo] = useState(item);
}

在文件顶部的 import 语句后,添加接口:UpdateTodoProps

interface UpdateTodoProps {
  item: string;
  id: string;
  fetchTodos: () => void;
}

void是一种表示缺少值的类型。我们用它来表示函数不返回值。

上面的 state 变量用于 modal,我们很快就会创建它,并保存要更新的 todo 值。上下文值也会被导入,以便在进行更改后进行更新。fetchTodostodos

现在,让我们编写负责发送 PUT 请求的函数。在组件主体中,在 state 和 context 变量之后,添加以下内容:UpdateTodo

const updateTodo = async () => {
  await fetch(http://localhost:8000/todo/${id}, {
    method: "PUT",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ item: todo }),
  });
  await fetchTodos();
};

在上面的异步函数中,将 PUT 请求发送到后端,然后调用 。fetchTodos()

接下来,渲染模态框:

return (

      

        Update Todo

         setTodo(event.target.value)}
        />

          

        

)

在上面的代码中,我们使用 Chakra UI 的 Dialog 组件创建了一个模态。在模态体中,我们监听了文本框的更改并更新了状态对象 .最后,当单击“Update Todo”按钮时,将调用该函数并更新我们的 Todo。todoupdateTodo()

完整的组件现在应如下所示:

const UpdateTodo = ({ item, id, fetchTodos }: UpdateTodoProps) => {
  const [todo, setTodo] = useState(item);
  const updateTodo = async () => {
    await fetch(http://localhost:8000/todo/${id}, {
      method: "PUT",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ item: todo }),
    });
    await fetchTodos();
  };

  return (

        

          Update Todo

           setTodo(event.target.value)}
          />

            

          

  )
}

在将组件添加到组件之前,让我们添加一个用于渲染 todos 的辅助组件来清理一下:Todos

function TodoHelper({item, id, fetchTodos}: TodoHelperProps) {
  return (

          {item}

  )
}

将接口添加到文件顶部:TodoHelperProps

interface TodoHelperProps {
  item: string;
  id: string;
  fetchTodos: () => void;
}

在上面的组件中,我们渲染了传递给组件的 todo,并为其附加了一个 update 按钮。

替换组件内块中的代码:returnTodos

return (

          {
          todos.map((todo) => (

          ))
          }

)

浏览器应具有焕然一新的外观:

Todo 应用程序

验证它是否有效:

更新 todo

DELETE 路由

后端

最后,添加 delete 路由:

@app.delete("/todo/{id}", tags=["todos"])
async def delete_todo(id: int) -> dict:
    for todo in todos:
        if int(todo["id"]) == id:
            todos.remove(todo)
            return {
                "data": f"Todo with id {id} has been removed."
            }

    return {
        "data": f"Todo with id {id} not found."
    }

前端

让我们编写一个用于删除 todo 的组件,它将在组件中使用:TodoHelper

const DeleteTodo = ({ id, fetchTodos }: DeleteTodoProps) => {
  const deleteTodo = async () => {
    await fetch(http://localhost:8000/todo/${id}, {
      method: "DELETE",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ id: id })
    })
    await fetchTodos()
  }

  return (
    
  )
}

添加接口:DeleteTodoProps

interface DeleteTodoProps {
  id: string;
  fetchTodos: () => void;
}

在这里,我们首先从全局 state 对象调用函数。接下来,我们创建了一个异步函数,该函数向服务器发送 DELETE 请求,然后通过再次调用 .最后,我们呈现了一个按钮,单击该按钮时,会触发 .fetchTodosfetchTodosdeleteTodo()

接下来,将组件添加到 :DeleteTodoTodoHelper

function TodoHelper({item, id, fetchTodos}: TodoHelperProps) {
  return (

          {item}

              {/* new */}

  )
}

客户端应用程序应自动更新:

Todo 应用程序

现在,测试删除按钮:

删除 todo

结论

本教程介绍了使用 FastAPI 和 React 设置 CRUD 应用程序的基础知识。

通过查看本教程开头的目标来检查您的理解情况。你可以在 fastapi-react 仓库中找到源码。感谢阅读。

寻找一些挑战?

  1. 使用本指南将 React 应用程序部署到 Netlify,并在后端更新 CORS 对象,以便使用环境变量动态配置它。
  2. 将后端 API 服务器部署到 Heroku 并替换前端上的连接 URL。同样,为此使用环境变量。您可以从使用 FastAPI 和 Heroku 部署和托管机器学习模型教程中学习将 FastAPI 部署到 Heroku 的基础知识。有关基础知识之外的内容,请查看使用 FastAPI 和 Docker 进行测试驱动开发课程。
  3. 使用后端的 pytest 和前端的 React 测试库设置单元测试和集成测试。使用 FastAPI 和 Docker 进行测试驱动开发课程介绍了如何使用 pytest 测试 FastAPI,而使用 Flask、React 和 Docker 进行身份验证则详细介绍了如何使用 Jest 和 React 测试库测试 React 应用程序。

//testdriven.io/courses/tdd-fastapi/)

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇