在这篇文章中,我们将带你一步步开发一个完整的 CRUD 应用程序,重点介绍 React 如何在前端处理数据的增删改查,而 FastAPI 则在后端处理这些操作。通过把这些框架结合在一起,你将看到它们如何一起工作以构建一个可以流畅执行所有基本 CRUD 操作的应用程序。
前提条件:- Node.js
- Python 3.10
- MySQL
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 元素中。
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
组件来处理路由。
import { createBrowserHistory } from 'history'
export default createBrowserHistory()
history.js
文件导出一个通过 createBrowserHistory
创建的自定义浏览器历史记录,以管理 React 应用导航。
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
,特定路由分别处理与产品相关的页面,如 create
、detail
、edit
和 delete
。
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
标头。
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
发送数据,并在提交成功后进行重定向。表单包含商品名称和价格字段,还设有取消链接和提交按钮。
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
。
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
更新产品信息,并在更新成功后重定向到产品列表。
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
更新显示的产品列表,在组件挂载后执行此操作。表格为每个产品提供了查看、编辑和删除的按钮,并包含一个创建新产品的链接。
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 的产品,若未提供数据,则显示该产品详情。
.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" class="lazyload" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsQAAA7EAZUrDhsAAAANSURBVBhXYzh8+PB/AAffA0nNPuCLAAAAAElFTkSuQmCC" data-original="/src/main.jsx"></script>
</body>
</html>
HTML 是一个 React 应用的主要入口文件,包含了 Bootstrap 用于样式和 Font Awesome 用于图标。其中包含一个 ID 为 root
的 div
,React 应用将在该 div
中渲染。
在命令行中运行以下命令来安装所需的库:
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
作为初始化文件,使得目录可以被识别为一个包含子模块并便于导入的包。
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
函数提供了一个数据库会话,可以用于依赖注入功能,确保在使用后会话能够正确关闭。
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
类,从而可以方便地操作数据库中的产品数据。
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
类,用于更新现有产品的信息。这两个类确保提供的数据符合预期的类型。
从 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
获取的数据库会话,以确保每个请求都有适当的会话处理。
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日
共同学习,写下你的评论
评论加载中...
作者其他优质文章