继续浏览精彩内容
慕课网APP
程序员的梦工厂
打开
继续
感谢您的支持,我会继续努力的
赞赏金额会直接到老师账户
将二维码发送给自己后长按识别
微信支付
支付宝支付

用React和FastAPI搭建一个CRUD应用详解

四季花海
关注TA
已关注
手记 316
粉丝 42
获赞 161

在这篇文章中,我们将带你一步步开发一个完整的 CRUD 应用程序,重点介绍 React 如何在前端处理数据的增删改查,而 FastAPI 则在后端处理这些操作。通过把这些框架结合在一起,你将看到它们如何一起工作以构建一个可以流畅执行所有基本 CRUD 操作的应用程序。

前提条件:
  • Node.js
  • Python 3.10
  • MySQL
搭建 React 项目
npm create vite@4.4.0 view -- --template react  # 使用 npm 创建一个使用 Vite 4.4.0 和 React 模板的新项目
cd view  # 切换到项目目录
npm install react-router-dom@5 axios  # 安装 react-router-dom@5 和 axios 依赖包
React项目的结构是什么?
    ├─ index.html  
    ├─ public  
    │  └─ css  
    │     └─ style.css  # 样式表文件
    └─ src  
       ├─ components  
       │  └─ product  
       │     ├─ Create.jsx  # 创建产品组件
       │     ├─ Delete.jsx  # 删除产品组件
       │     ├─ Detail.jsx  # 产品详情组件
       │     ├─ Edit.jsx  # 编辑产品组件
       │     ├─ Index.jsx  # 产品列表组件
       │     └─ Service.js  # 产品服务文件
       ├─ history.js  # 历史记录文件
       ├─ http.js  # 网络请求文件
       ├─ main.jsx  # 主应用文件
       ├─ router.jsx  # 路由配置文件
       └─ App.jsx  # 应用入口文件
React项目的文件
main.jsx
    import React from 'react'  
    import ReactDOM from 'react-dom/client'  
    import App from './App'  

    ReactDOM.createRoot(document.getElementById('root')).render(  
      <App />  
    )

main.jsx 文件是 React 应用的入口点。它引入了 React 和 ReactDOM,同时引入了 App 组件。使用 ReactDOM.createRoot 方法将 App 组件渲染到 ID 为 root 的 HTML 元素中。

App.jsx
import React, { useState, useEffect } from 'react'  
import { Router, Link } from 'react-router-dom'  
import history from './history'  
import Route from './router'  

export default function App() {  
  return (  
    <Router history={ history }>  
      <Route />  
    </Router>  
  )  
}

App.jsx 文件设置了 React 应用的路由配置。App 组件使用自定义的 history 将应用包裹在 Router 组件中,并通过 Route 组件来处理路由。

历史.js (记录了历史操作的文件)
import { createBrowserHistory } from 'history'  

export default createBrowserHistory()

history.js 文件导出一个通过 createBrowserHistory 创建的自定义浏览器历史记录,以管理 React 应用导航。

router.jsx
    import React, { Suspense, lazy } from 'react'  
    import { Switch, Route, Redirect } from 'react-router-dom'  

    export default function AppRoute(props) {  
      return (  
        <Suspense fallback={''}>  
          <Switch>  
            <Route path="/" component={(p) => <Redirect to="/product" />} exact />  
            <Route path="/product" component={lazy(() => import('./components/product/Index'))} exact />  
            <Route path="/product/create" component={lazy(() => import('./components/product/Create'))} exact />  
            <Route path="/product/:id/" component={lazy(() => import('./components/product/Detail'))} exact />  
            <Route path="/product/edit/:id/" component={lazy(() => import('./components/product/Edit'))} exact />  
            <Route path="/product/delete/:id/" component={lazy(() => import('./components/product/Delete'))} exact />  
          </Switch>  
        </Suspense>  
      )  
    }

router.jsx 文件为一个 React 应用设置了路由,并使用懒加载的组件。它使用 Suspense 处理加载状态,并使用 Switch 来定义路由。根路径指向 /product,特定路由分别处理与产品相关的页面,如 createdetaileditdelete

一个处理HTTP请求的模块
http.js
    import axios from 'axios'  

    let http = axios.create({  
      baseURL: 'http://localhost:8000/api',  
      headers: {  
        'Content-type': 'application/json'  
      }  
    })  

    export default http

http.js 文件中,配置并导出一个 Axios 实例,该实例具有统一管理的基础 URL,用于标准的 API 端点,并设置了默认的 application/json 标头。

创建.jsx
import React, { useState, useEffect } from 'react'
import { Link } from 'react-router-dom'
import Service from './Service'

export default function ProductCreate(props) {

  const [product, setProduct] = useState({})

  function create(e) {
    e.preventDefault()
    Service.create(product).then(() => {
      props.history.push('/product')
    }).catch((e) => {
      alert(e.response.data)
    })
  }

  function onChange(e) {
    let data = { ...product }
    data[e.target.name] = e.target.value
    setProduct(data)
  }

  return (
    <div className="container">
      <div className="row">
        <div className="col">
          <form method="post" onSubmit={create}>
            <div className="row">
              <div className="mb-3 col-md-6 col-lg-4">
                <label className="form-label" htmlFor="product_name">名称</label>
                <input id="product_name" name="name" className="form-control" onChange={onChange} value={product.name || ''} maxLength="50" />
              </div>
              <div className="mb-3 col-md-6 col-lg-4">
                <label className="form-label" htmlFor="product_price">价格</label>
                <input id="product_price" name="price" className="form-control" onChange={onChange} value={product.price || ''} type="number" />
              </div>
              <div className="col-12">
                <Link className="btn btn-secondary" to="/product">取消</Link>
                <button className="btn btn-primary">提交</button>
              </div>
            </div>
          </form>
        </div>
      </div>
    </div>
  )
}

create.jsx 文件定义了一个名为 ProductCreate 的组件,用于添加新商品。它使用 useState 来管理表单中的数据,并使用 create(e) 函数处理表单提交,该函数通过调用 Service.create 发送数据,并在提交成功后进行重定向。表单包含商品名称和价格字段,还设有取消链接和提交按钮。

删除.jsx
    import React, { useState, useEffect } from 'react'  
    import { Link } from 'react-router-dom'  
    import Service from './Service'  

    export default function ProductDelete(props) {  

      const [ product, setProduct ] = useState({})  

      useEffect(() => {  
        get()  
      }, [ props.match.params.id ])  

      function get() {  
        return Service.delete(props.match.params.id).then(response => {  
          setProduct(response.data)  
        }).catch(e => {  
          alert(e.response.data)  
        })  
      }  

      function remove(e) {  
        e.preventDefault()  
        Service.delete(props.match.params.id, product).then(() => {  
          props.history.push('/product')  
        }).catch((e) => {  
          alert(e.response.data)  
        })  
      }  

      return (  
        <div className="container">  
          <div className="row">  
            <div className="col">  
              <form method="post" onSubmit={remove}>  
                <div className="row">  
                  <div className="mb-3 col-md-6 col-lg-4">  
                    <label className="form-label" htmlFor="product_id">ID号</label>  
                    <input readOnly id="product_id" name="id" className="form-control" value={product.id ?? '' } type="number" required />  
                  </div>  
                  <div className="mb-3 col-md-6 col-lg-4">  
                    <label className="form-label" htmlFor="product_name">商品名称</label>  
                    <input readOnly id="product_name" name="name" className="form-control" value={product.name ?? '' } maxLength="50" />  
                  </div>  
                  <div className="mb-3 col-md-6 col-lg-4">  
                    <label className="form-label" htmlFor="product_price">商品价格</label>  
                    <input readOnly id="product_price" name="price" className="form-control" value={product.price ?? '' } type="number" />  
                  </div>  
                  <div className="col-12">  
                    <Link className="btn btn-secondary" to="/product">取消操作</Link>  
                    <button className="btn btn-danger">确定删除</button>  
                  </div>  
                </div>  
              </form>  
            </div>  
          </div>  
        </div>  
      )  
    }

Delete.jsx 文件定义了一个名为 ProductDelete 的组件,该组件通过 Service.delete 获取产品的详细信息,并将这些信息以只读表单的形式展示。组件利用 useEffect 根据路由参数中的产品 ID 来加载产品数据。remove(e) 函数负责处理删除操作,并在成功删除后重定向到 /product

Detail.jsx
    import React, { useState, useEffect } from 'react'  
    import { Link } from 'react-router-dom'  
    import Service from './Service'  

    export default function ProductDetail(props) {  

      const [ product, setProduct ] = useState({})  

      useEffect(() => {  
        get()  
      }, [ props.match.params.id ])  

      function get() {  
        return Service.get(props.match.params.id).then(response => {  
          setProduct(response.data)  
        }).catch(e => {  
          alert(e.response.data)  
        })  
      }  

      return (  
        <div className="container">  
          <div className="row">  
            <div className="col">  
              <form method="post">  
                <div className="row">  
                  <div className="mb-3 col-md-6 col-lg-4">  
                    <label className="form-label" htmlFor="product_id">Id</label>  
                    <input readOnly id="product_id" name="id" className="form-control" value={product.id ?? '' } type="number" required />  
                  </div>  
                  <div className="mb-3 col-md-6 col-lg-4">  
                    <label className="form-label" htmlFor="product_name">名称</label>  
                    <input readOnly id="product_name" name="name" className="form-control" value={product.name ?? '' } maxLength="50" />  
                  </div>  
                  <div className="mb-3 col-md-6 col-lg-4">  
                    <label className="form-label" htmlFor="product_price">价格</label>  
                    <input readOnly id="product_price" name="price" className="form-control" value={product.price ?? '' } type="number" />  
                  </div>  
                  <div className="col-12">  
                    <Link className="btn btn-secondary" to="/product">返回</Link>  
                    <Link className="btn btn-primary" to={`/product/edit/${product.id}`}>编辑商品</Link>  
                  </div>  
                </div>  
              </form>  
            </div>  
          </div>  
        </div>  
      )  
    }

Detail.jsx 文件定义了一个名为 ProductDetail 的组件,该组件用于显示产品的详细信息。它根据路由参数中的产品 ID 通过 Service.get 获取产品数据,并以只读形式展示,同时提供了返回产品列表和编辑产品的链接。

/ 编辑.jsx /

    import React, { useState, useEffect } from 'react'  
    import { Link } from 'react-router-dom'  
    import Service from './Service'  

    export default function ProductEdit(props) {  

      const [ product, setProduct ] = useState({})  

      useEffect(() => {  
        get()  
      }, [ props.match.params.id ])  

      function get() {  
        return Service.edit(props.match.params.id).then(response => {  
          setProduct(response.data)  
        }).catch(e => {  
          alert(e.response.data)  
        })  
      }  

      function edit(e) {  
        e.preventDefault()  
        Service.edit(props.match.params.id, product).then(() => {  
          props.history.push('/product')  
        }).catch((e) => {  
          alert(e.response.data)  
        })  
      }  

      function onChange(e) {  
        let data = { ...product }  
        data[e.target.name] = e.target.value  
        setProduct(data)  
      }  

      return (  
        <div className="container">  
          <div className="row">  
            <div className="col">  
              <form method="post" onSubmit={edit}>  
                <div className="row">  
                  <div className="mb-3 col-md-6 col-lg-4">  
                    <label className="form-label" htmlFor="product_id">编号</label>  
                    <input readOnly id="product_id" name="id" className="form-control" onChange={onChange} value={product.id ?? '' } type="number" required />  
                  </div>  
                  <div className="mb-3 col-md-6 col-lg-4">  
                    <label className="form-label" htmlFor="product_name">名字</label>  
                    <input id="product_name" name="name" className="form-control" onChange={onChange} value={product.name ?? '' } maxLength="50" />  
                  </div>  
                  <div className="mb-3 col-md-6 col-lg-4">  
                    <label className="form-label" htmlFor="product_price">价格</label>  
                    <input id="product_price" name="price" className="form-control" onChange={onChange} value={product.price ?? '' } type="number" />  
                  </div>  
                  <div className="col-12">  
                    <Link className="btn btn-secondary" to="/product">取消</Link>  
                    <button className="btn btn-primary">提交</button>  
                  </div>  
                </div>  
              </form>  
            </div>  
          </div>  
        </div>  
      )  
    }

Edit.jsx 文件定义了一个用于更新产品详情的 ProductEdit 组件。它使用 Service.edit 获取当前产品数据,并用这些数据填充表单的内容。表单允许用户修改产品的名称和价格等信息。当用户提交表单时,edit(e) 函数通过 Service.edit 更新产品信息,并在更新成功后重定向到产品列表。

Index.jsx
import React, { useState, useEffect } from 'react'  
import { Link } from 'react-router-dom'  
import Service from './Service'  

export default function ProductIndex(props) {  

  const [products, setProducts] = useState([])  

  useEffect(() => {  
    get()  
  }, [props.location])  

  function get() {  
    Service.get().then(response => {  
      setProducts(response.data)  
    }).catch(e => {  
      alert(e.response.data)  
    })  
  }  

  return (  
    <div className="container">  
      <div className="row">  
        <div className="col">  
          <table className="table table-striped table-hover">  
            <thead>  
              <tr>  
                <th>ID</th>  
                <th>名称</th>  
                <th>价格</th>  
                <th>操作项</th>  
              </tr>  
            </thead>  
            <tbody>  
              {products.map((product, index) =>  
              <tr key={index}>  
                <td className="text-center">{product.id}</td>  
                <td>{product.name}</td>  
                <td className="text-center">{product.price}</td>  
                <td className="text-center">  
                  <Link className="btn btn-secondary" to={`/product/${product.id}`} title="查"><i className="fa fa-eye"></i></Link>  
                  <Link className="btn btn-primary" to={`/product/edit/${product.id}`} title="编"><i className="fa fa-pencil"></i></Link>  
                  <Link className="btn btn-danger" to={`/product/delete/${product.id}`} title="删"><i className="fa fa-times"></i></Link>  
                </td>  
              </tr>  
              )}  
            </tbody>  
          </table>  
          <Link className="btn btn-primary" to="/product/create">新建</Link>  
        </div>  
      </div>  
    </div>  
  )  
}

Index.jsx 文件定义了一个 ProductIndex 组件,该组件显示产品列表,并以表格形式排列,包括产品的ID、名称和价格。它获取产品数据并通过 Service.get 更新显示的产品列表,在组件挂载后执行此操作。表格为每个产品提供了查看、编辑和删除的按钮,并包含一个创建新产品的链接。

Service.js
    import http from '../../http'  

    export default {  
      get(id) {  
        // 根据 id 获取商品信息
        if (id) {  
          return http.get(`/products/${id}`)  
        } else {  
          // 获取 URL 查询参数
          return http.get('/products' + location.search)  
        }  
      },  
      create(data) {  
        // 通过 data 创建商品
        if (data) {  
          return http.post('/products', data)  
        } else if (!data) {  
          return http.get('/products/create')  
        }  
      },  
      edit(id, data) {  
        // 通过 data 编辑商品信息
        if (data) {  
          return http.put(`/products/${id}`, data)  
        } else {  
          return http.get(`/products/${id}`)  
        }  
      },  
      delete(id, data) {  
        // 根据 id 删除商品
        if (data) {  
          return http.delete(`/products/${id}`)  
        } else {  
          // 如果没有数据,获取商品详情
          return http.get(`/products/${id}`)  
        }  
      }  
    }

Service.js文件中定义了处理产品相关的操作的 API 方法。它使用一个 http 实例来发起请求:

  • get(id) 根据 ID 获取单个产品,若未指定 ID,则获取所有产品。
  • create(data) 使用提供的数据创建新产品,若未提供数据,则显示创建表单。
  • edit(id, data) 使用提供的数据更新指定 ID 的产品,若未提供数据,则显示该产品的详细信息。
  • delete(id, data) 删除指定 ID 的产品,若未提供数据,则显示该产品详情。
样式表.css
.container {  
  margin-top: 2em;  
}  

.btn {  
  margin-right: 0.25em;  
}

下面的代码设置了一个容器元素的顶部边距为2em,以及按钮元素之间的右边距为0.25em。

CSS通过在容器上方添加间距以及水平间隔按钮之间的距离来调整布局。

首页.html
    <!DOCTYPE html>  
    <html lang="en">  
    <head>  
      <meta charset="utf-8">  
      <meta name="viewport" content="width=device-width,initial-scale=1">  
      <link href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.3/css/bootstrap.min.css" rel="stylesheet">  
      <link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css" rel="stylesheet">  
      <link href="/css/style.css" rel="stylesheet">  
    </head>  
    <body>  
      <div id="root"></div>  
      <script type="module" src="/src/main.jsx"></script>  
    </body>  
    </html>

HTML 是一个 React 应用的主要入口文件,包含了 Bootstrap 用于样式和 Font Awesome 用于图标。其中包含一个 ID 为 rootdiv,React 应用将在该 div 中渲染。

搭建 FastAPI 项目

在命令行中运行以下命令来安装所需的库:

pip install fastapi sqlalchemy pymysql uvicorn python-dotenv

创建一个测试数据库,并命名为“example”。运行[database.sql]脚本以导入表结构和数据。

这里我们讨论快速API(FastAPI)的项目结构
    ├─ .env  # 环境配置文件
    └─ app  # 应用程序主目录
       ├─ db.py  # 数据库操作文件
       ├─ main.py  # 应用程序主入口文件
       ├─ models  # 模型定义目录
       │  └─ product.py  # 产品模型定义文件
       ├─ routers  # 路由定义目录
       │  └─ product.py  # 产品相关路由定义文件
       ├─ schemas  # 数据模式定义目录
       │  └─ product.py  # 产品数据模式定义文件
       └─ __init__.py  # 初始化文件

__init__.py 作为初始化文件,使得目录可以被识别为一个包含子模块并便于导入的包。

FastAPI项目文件
.env 环境变量配置文件注释
    DB_HOST=localhost  
    DB_PORT=3306  
    DB_DATABASE=example  
    DB_USER=root  
    DB_PASSWORD=  

此文件包含了连接数据库所需的配置信息。

数据库文件:db.py
    import os  
    from dotenv import load_dotenv  
    from sqlalchemy import create_engine  
    from sqlalchemy.ext.declarative import declarative_base  
    from sqlalchemy.orm import sessionmaker  

    load_dotenv()  
    url = f"mysql+pymysql://{os.getenv('DB_USER')}:{os.getenv('DB_PASSWORD')}@{os.getenv('DB_HOST')}:{os.getenv('DB_PORT')}/{os.getenv('DB_DATABASE')}"  
    engine = create_engine(url)  
    SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)  
    Base = declarative_base()  

    def get_db():  
        db = SessionLocal()  
        try:  
            yield db  
        finally:  
            db.close()

db.py 模块为一个 FastAPI 应用程序设置了数据库连接,使用了 SQLAlchemy。该模块从环境变量中加载数据库凭据,创建一个 MySQL 数据库的 SQLAlchemy 引擎实例,并创建一个用于数据库交互的会话工厂。get_db 函数提供了一个数据库会话,可以用于依赖注入功能,确保在使用后会话能够正确关闭。

models\产品模型.py
    from sqlalchemy import *  
    from app.db import Base  

    class Product(Base):  
        __tablename__ = "Product"  
        id = Column(INTEGER, primary_key=True)  
        name = Column(VARCHAR)  
        price = Column(DECIMAL)

models\product.py 模块定义了一个 SQLAlchemy 模型,用于 Product 表,映射了该产品的 ID、名称和价格这些字段。它继承了来自 db 模块的 Base 类,从而可以方便地操作数据库中的产品数据。

schemas\模式.py
    from pydantic import BaseModel  
    from decimal import Decimal  

    class 产品创建(BaseModel):  
        name: str  
        price: Decimal  

    class 产品更新(BaseModel):  
        name: str  
        price: Decimal

schemas/product.py 文件定义了用于在 FastAPI 应用程序中验证和序列化产品数据的 Pydantic 模型。它包括两个类:ProductCreate,用于通过指定名称和价格来创建新产品的;以及 ProductUpdate 类,用于更新现有产品的信息。这两个类确保提供的数据符合预期的类型。

路由\产品.py
    从 fastapi 导入 APIRouter, Depends  
    从 sqlalchemy.orm 导入 Session  
    从 app.db 导入 get_db  
    从 app.models.product 导入 Product  
    从 app.schemas.product 导入 ProductCreate, ProductUpdate  

    router = APIRouter()  

    @router.get("/products")  
    def index(db: Session = Depends(get_db)):  
        返回 db.query(Product).all()  

    @router.get("/products/{id}")  
    def get(id: int, db: Session = Depends(get_db)):  
        返回 db.query(Product).filter(Product.id == id).first()  

    @router.post("/products")  
    def create(payload: ProductCreate, db: Session = Depends(get_db)):  
        product = Product(**payload.model_dump())  
        db.add(product)  
        db.commit()  
        db.refresh(product)  
        返回 product  

    @router.put("/products/{id}")  
    def update(id: int, payload: ProductUpdate, db: Session = Depends(get_db)):  
        product = db.query(Product).filter(Product.id == id).first()  
        product.name = payload.name  
        product.price = payload.price  
        db.commit()  
        db.refresh(product)  
        返回 product  

    @router.delete("/products/{id}")  
    def delete(id: int, db: Session = Depends(get_db)):  
        db.query(Product).filter(Product.id == id).delete()  
        db.commit()

routers/product.py模块定义了一个FastAPI路由,用于管理和操作产品相关的API端点。它提供了以下功能:

  • index 函数返回数据库中的所有产品列表。
  • get 函数通过产品 ID 获取特定产品信息。
  • create 函数使用提供的 ProductCreate 数据架构将新产品添加到数据库中。
  • update 函数根据产品 ID 和提供的 ProductUpdate 数据更新现有产品信息。
  • delete 函数通过产品 ID 从数据库中移除该产品。

所有操作都依赖于通过get_db获取的数据库会话,以确保每个请求都有适当的会话处理。

main.py
    from fastapi import FastAPI  
    from fastapi.responses import FileResponse  
    from fastapi.middleware.cors import CORSMiddleware  
    from app.routers.product import router  

    # 创建 FastAPI 应用实例
    app = FastAPI()  
    app.include_router(router, prefix="/api")  

    # 定义根路径的 GET 请求处理函数
    @app.get("/")  
    async def read_index():  
        return FileResponse("app/static/index.html")  

    # 配置跨域请求中间件
    app.add_middleware(  
        CORSMiddleware,  
        allow_origins=["*"],  
        allow_methods=["*"],  
        allow_headers=["*"],  
    )  

    # 当脚本作为主程序运行时
    if __name__ == "__main__":  
        uvicorn.run(app, host="127.0.0.1")

main.py 模块是 FastAPI 应用的入口点。它初始化 FastAPI 应用,并包含一个针对 /api 前缀下与商品相关的端点的路由。模块还定义了一个根端点,用于从静态目录提供 index.html 页面。添加了 CORS 中间件以允许来自任何来源的请求,从而启用跨源资源共享(CORS)。最后,当直接运行时,应用配置为使用 Uvicorn 运行,监听本地主机 127.0.0.1

运行项目(即运行任务)

启动 React 项目吧

npm run dev # 运行开发版本的命令

快速运行 FastAPI 项目

使用Uvicorn运行Python应用程序的命令为 uvicorn app.main:app。在Python项目中,这是启动ASGI应用的标准命令,其中app.main:app指定了要启动的应用程序的模块和变量。

打开你的网络浏览器,访问http://localhost:5173
你就能看到这个产品列表页。

试一试,

点击“浏览”按钮查看产品详情。

点击一下“编辑产品”按钮以修改产品信息并更新产品详情。

点击“提交”按钮保存更新后的商品信息。

点击“新建”按钮来添加新产品的信息并输入其详细信息后。

点击“提交”按钮来保存新商品的信息。

点击一下“删除”按钮来删除先前创建的产品。

点击该“删除”按钮来确认删除这个产品。

结论

总之,我们学会了如何使用JSX语法创建一个基本的React项目来构建视图和管理路由,同时搭建了一个FastAPI服务器作为后端。利用SQLAlchemy进行数据库操作,我们开发了一个动态的前端,可以无缝地与强大的后端进行交互,为现代全栈Web应用程序打下了坚实的基础。

你可以在这里找到源代码: https://github.com/stackpuz/Example-CRUD-React-18-FastAPI

几分钟内就能创建一个 React CRUD 应用程序:https://stackpuz.com

最初发布于https://blog.stackpuz.com,日期为2024年10月28日

打开App,阅读手记
0人推荐
发表评论
随时随地看视频慕课网APP