注意:这是关于 FastApi 和 React 的多部分教程的第 4 部分。如果你想从头开始(我推荐!😉)这里是第 1 部分!

欢迎来到本教程的第 4 部分!今天我们将看到如何将 React 应用程序连接到我们很棒的 FastAPI 后端!与往常一样,是存储库,其中包含我们将在本文中编写的代码。

上次我们在 API 中添加了以下路由:

  • /polls/:列出所有现有的问题

  • /polls/{id}/:显示投票详情,包括相关结果

现在我们的目标是使用它们来显示与原始Django 教程中相同的信息,使用 React:

  • 列出投票的索引页面

  • 每次投票的表格

  • 每次投票的结果页面

事实上,由于我们将使用 React,我们可以更进一步,将最后两个视图合并到一个具有以下规范的多用途详细视图中:

1.首先到达/polss/{id}/时,用户应该看到投票的标题和可用的选项

  1. 然后用户通过点击其中一个选项来提交自己的投票

  2. 最后,一旦投票被 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)

现在剩下要做的就是构建一个PollIndexPollResult组件来替换占位符!这些组件将负责使用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

Logo

学AI,认准AI Studio!GPU算力,限时免费领,邀请好友解锁更多惊喜福利 >>>

更多推荐