使用 FastAPI 和 React 重做 Django 教程:将 React 应用程序连接到 FastAPI !
注意:这是关于 FastApi 和 React 的多部分教程的第 4 部分。如果你想从头开始(我推荐!😉)这里是第 1 部分!
欢迎来到本教程的第 4 部分!今天我们将看到如何将 React 应用程序连接到我们很棒的 FastAPI 后端!与往常一样,是存储库,其中包含我们将在本文中编写的代码。
上次我们在 API 中添加了以下路由:
-
/polls/
:列出所有现有的问题 -
/polls/{id}/
:显示投票详情,包括相关结果
现在我们的目标是使用它们来显示与原始Django 教程中相同的信息,使用 React:
-
列出投票的索引页面
-
每次投票的表格
-
每次投票的结果页面
事实上,由于我们将使用 React,我们可以更进一步,将最后两个视图合并到一个具有以下规范的多用途详细视图中:
1.首先到达/polss/{id}/
时,用户应该看到投票的标题和可用的选项
-
然后用户通过点击其中一个选项来提交自己的投票
-
最后,一旦投票被 API 处理,当前的投票数会在每个选项下显示给用户
就像在 Django 教程中一样,我们将在下一部分保留实际的投票提交!
我们将使用Create React App在 React 中构建我们的 UI。 CRA 是一个很棒的脚本集合,它负责捆绑、转译以及设置 React 项目可能需要的所有样板代码。这样我们就可以直接开始编码了!
设置项目
对于本教程,我们的 UI 将与我们的 API 位于同一个项目中。但在现实生活中,您可能希望拥有一个单独的存储库。从项目的根目录运行以下命令来创建 UI:
yarn create react-app ui --template typescript
或者,如果您更喜欢 npm
npx create-react-app ui --template typescript
注意:我们将在本教程中使用typescript。别担心,你不需要对类型有深入的了解,我们会保持非常基本的!这主要是为了防止我们在使用来自 API 的数据时出错。
我们还需要以下库来构建我们的 UI:
-
Axios:一个很棒的请求库。
-
React Router:用于客户端导航
-
react-query: 与服务器无痛同步数据
-
Material UI:没有必要,但如果你没有任何设计技能,可以快速制作原型。 (像我一样👌)
注意:这些都不是_严格_必要的,但是当我需要快速构建一个小型 SPA 时,这是我的设置。我必须说我对它非常满意,但是如果您有任何反馈在 Twitter 上联系🐦!
我们的项目已准备就绪。事不宜迟,让我们开始吧!
我会!
设置 react-query
我们将从设置 react-query 开始。 React 查询允许定义一个默认查询函数。由于我们将只使用useQuery
与我们的 API 进行通信,因此我们将其设置为使用 Axios 的 GET 函数。这样我们就可以使用我们的端点 URL,既作为查询键又作为 axios 的参数。
我喜欢将我的查询函数放在一个utils
文件夹中,如下所示:
// utils/queryFn.ts
import axios from "axios";
// We use the built-in QueryFunction type from `react-query` so we don't have to set it up oursevle
import { QueryFunction } from "react-query";
export const queryFn: QueryFunction = async ({ queryKey }) => {
// In a production setting the host would be remplaced by an environment variable
const { data } = await axios.get(`http://localhost:80/${queryKey[0]}`);
return data;
};
进入全屏模式 退出全屏模式
然后我们只需要配置 QueryClient 以使用我们的默认功能:
// index.tsx
import React from "react";
import ReactDOM from "react-dom";
import "./index.css";
import App from "./App";
import reportWebVitals from "./reportWebVitals";
import { queryFn } from "./utils/queryFn";
import { QueryClient, QueryClientProvider } from "react-query";
// Configuring the queryclient to use
// our query function
const queryClient = new QueryClient({
defaultOptions: {
queries: {
queryFn: queryFn,
},
},
});
ReactDOM.render(
<React.StrictMode>
<QueryClientProvider client={queryClient}>
<App />
</QueryClientProvider>
</React.StrictMode>,
document.getElementById("root")
);
进入全屏模式 退出全屏模式
设置反应路由器
我们还需要设置客户端路由。如简介中所述,我们将创建两个路由:轮询索引和轮询详细信息。现在我们将在其中放置一些占位符,直到我们在下一节中构建实际视图😄!
import React from "react";
import { BrowserRouter, Routes, Route } from "react-router-dom";
import PollIndex from "routes/Poll";
import Results from "routes/Poll/Results";
import CssBaseline from "@mui/material/CssBaseline";
import "./App.css";
function App() {
return (
<div className="App">
<CssBaseline />
<BrowserRouter>
<Routes>
<Route path="/" element={<div>Poll Index</div<}></Route>
<Route path=":questionId/" element={<div>Poll Form</div<} />
<Route path=":questionId/results/" element={<div>Poll Results</div<} />
</Routes>
</BrowserRouter>
</div>
);
}
export default App;
进入全屏模式 退出全屏模式
现在使用yarn start
启动应用程序,两条路线都应该可用!
[](https://res.cloudinary.com/practicaldev/image/fetch/s--fgx2mLD1--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cdn.hashnode. com/res/hashnode/image/upload/v1638712757443/Snlmu2A-x.png%3Fauto%3Dcompress)
现在剩下要做的就是构建一个PollIndex
和PollResult
组件来替换占位符!这些组件将负责使用react-query
查询 API 并显示结果!
构建投票索引
我们将开始构建民意调查索引。我们想列出所有现有的民意调查,并可能在我们处理时让它们链接到相应的表单!
... 为您的生活进行口型同步!使用useQuery
查询我们的端点!
类型定义
首先,由于我们使用的是 typescript,我们需要描述我们期望从 API 接收到的类型。这就是我认为 FastAPI 自动文档真正闪耀的地方。当您 - 或其他人 - 想要构建与我们的 API 接口的东西(在处理应用程序编程_Interface_时应该预期)时,您所要做的就是咨询/docs
端点。
让我们看看我们的两个端点:
这是/polls/
的记录响应形状
[](https://res.cloudinary.com/practicaldev/image/fetch/s--2WHiGQgg--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cdn.hashnode .com/res/hashnode/image/upload/v1638628547064/HyoxEp3qE.png%3Fauto%3Dcompress)
和/polls/{id}
的一个:
[](https://res.cloudinary.com/practicaldev/image/fetch/s--1K8NgW8B--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cdn.hashnode .com/res/hashnode/image/upload/v1638628618152/SSEyaZMU0.png%3Fauto%3Dcompress)
非常简单,我们将其翻译成打字稿,我们将保证与我们的 API 正确通信!以下是我们将使用的类型:
export interface Choice {
id: number;
choice_text: string;
votes: number;
}
export interface Question {
id: number;
pub_date: string;
question_text: string;
}
export interface QuestionResults extends Question {
choices: Choice[];
}
进入全屏模式 退出全屏模式
我们完成了打字稿!
现在,我喜欢将我所有的页面组件放在一个routes
文件夹中,然后模仿应用程序的实际路由结构。随着最新版本的react-router out,我需要检查当前的最佳实践是什么!
创建routes/Poll/index.ts
,实现如下:
//Poll/index.ts
import React from "react";
// The type we've just defined
import { Question } from "types";
import { useQuery } from "react-query";
// Routing
import { Link} from "react-router-dom";
// Material ui stuff
import { styled } from "@mui/material/styles";
import Card from "@mui/material/Card";
import Typography from "@mui/material/Typography";
import Container from "@mui/material/Container";
import Box from "@mui/material/Box";
import Page from "components/template/Page";
const StyledLink = styled(Link)`
text-decoration: none;
`;
const PollIndex: React.FunctionComponent = () => {
// Syncing our data
const { data: questions, isSuccess } = useQuery<Question[]>("polls/");
// In real life we should handle isError and isLoading
// displaying an error message or a loading animation as required.
// This will do for our tutorial
if (!isSuccess) {
return <div> no questions </div>;
}
return (
<Page title="Index">
<Container maxWidth="sm">
{questions?.map((question) => (
<Box marginY={2}>
<StyledLink to={`${question.id}/results/`}>
<Card key={question.id}>
<Typography color="primary" gutterBottom variant="h3">
{question.question_text}
</Typography>
</Card>
</StyledLink>
</Box>
))}
<Outlet />
</Container>
</Page>
);
};
export default PollIndex;
进入全屏模式 退出全屏模式
然后替换App.tsx
中的占位符:
// App.tsx
import PollIndex from "routes/Poll";
...
function App() {
return (
...
<Route>
...
<Route path="/" element={<PollIndex />}></Route>
</Routes>
)
}
进入全屏模式 退出全屏模式
这里最重要的一点是const { data: questions, isSuccess } = useQuery<Question[]>("polls/");
。如您所见,我正在通过useQuery
钩子传递我们响应的预期类型。否则data
将是unkown
类型,我们不希望这样!
对于其余部分,显示问题列表与查询结果的映射一样简单。让我们看看它的外观:
[](https://res.cloudinary.com/practicaldev/image/fetch/s--_drwAppl--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cdn.hashnode. com/res/hashnode/image/upload/v1638629389333/zJka6fykW.png%3Fauto%3Dcompress)
还不错吧?
现在,现在,不用哭了
我们将使用完全相同的方法构建详细信息视图!
构建详情页
这个会住在Polls/index.tsx
页面旁边,我们就叫它Polls/Details.tsx
吧。这一次,由于该页面将在polls/<poll_id>
访问,我们将使用reat-router-dom
中的useParam
挂钩来检索 id,并将其传递给我们的 API。像这样:
// Detail.tsx
import React, { useState } from "react";
// types
import { QuestionResults } from "types";
// routing
import { useParams } from "react-router-dom";
// querying
import { useQuery } from "react-query";
// Material ui stuff
import Card from "@mui/material/Card";
import Page from "components/template/Page";
import Chip from "@mui/material/Chip";
import CardContent from "@mui/material/CardContent";
import CardHeader from "@mui/material/CardHeader";
import CardActionArea from "@mui/material/CardActionArea";
import Typography from "@mui/material/Typography";
import Grid from "@mui/material/Grid";
const Details = () => {
const { questionId } = useParams();
// This state variable controls
// displaying the results
const [hasVoted, setHasVoted] = useState(false);
// We can use the id from use param
// directly with the useQuery hook
const questionQuery = useQuery<QuestionResults>(`polls/${questionId}/`);
if (!questionQuery.isSuccess) {
return <div> loading </div>;
}
return (
<Page title={questionQuery.data.question_text}>
<Grid spacing={2} container>
<Grid item xs={12}>
<Typography variant="h2">
{questionQuery.data.question_text}
</Typography>
</Grid>
{questionQuery.data.choices.map((choice) => (
<Grid item xs={12} md={6}>
<Card key={choice.id}>
<CardActionArea onClick={() => setHasVoted(true)}>
<CardHeader title={choice.choice_text}></CardHeader>
<CardContent>
{hasVoted && <Chip label={choice.votes} color="success" />}
</CardContent>
</CardActionArea>
</Card>
</Grid>
))}
</Grid>
</Page>
);
};
export default Details;
进入全屏模式 退出全屏模式
而已!看起来与索引几乎相同,我们只是在特定民意调查的选择上使用map
来显示它们。结果显示使用控制
一个简单的useState
钩子。但是,如果这些数据真的很敏感,我们也必须在服务器上限制对它的访问!
只需替换App.tsx
中的占位符并欣赏结果!
// App.tsx
import PollDetails from "routes/Poll/Details";
...
function App() {
return (
...
<Route>
...
<Route path="/" element={<PollIndex />}></Route>
<Route path="/" element={<PollDetails />}></Route>
</Routes>
)
}
进入全屏模式 退出全屏模式
[](https://res.cloudinary.com/practicaldev/image/fetch/s--3lbvYkMF--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_66%2Cw_880/https://cdn.hashnode. com/res/hashnode/image/upload/v1638632086531/L3YqjDcfh.gif%3Fauto%3Dcompress)
我做了一个非常科学的调查
看起来很棒 !
感谢阅读!
这是第4部分的包装!希望你喜欢它,下次我们将看到如何将投票实际提交到我们的 API 并将其保存到数据库中! 😃
与往常一样,如果您有任何问题,可以在Twitter🐦 上与我联系!
参考文献
1.反应查询
2.反应路由器
3.FastAPI
更多推荐
所有评论(0)