React低代码项目:网络请求与问卷基础实现
- 游戏开发
- 2025-09-02 10:18:02

🍞吐司问卷:网络请求与问卷基础实现
Date: February 10, 2025
Log
技术要点:
HTTP协议XMLHttpRequest、fetch、axiosmock.js、postmanWebpack devServer 代理、craco.js 扩展 webpackRestful API开发要点:
搭建 mock 服务注:前端项目并不推荐直接使用 mock.js。因为它不支持 fetch,且上线时需要剔除模拟接口。
因此,建议构建一个简易服务端,通过 Koa 搭建一个接口路由用于测试接口。
Ajax 封装、useRequest 使用分页、LoadMoreMock 数据 前端 Mock 模拟 Ajax
要点:
前端引入 mock.js 测试 api安装:
npm i mockjs npm i --save-dev @types/mockjs // 使用ts需要额外安装注意点:
mock.js 只能劫持 XMLHttpRequest,不能劫持 fetch要在生产环境(上线时)注释掉,否则线上请求也被劫持Case:
**效果:**会执行两次 mock(原因见下)
定义的mock
import Mock from 'mockjs' Mock.mock('/api/test', 'get', () => { return { error: 0, data: { name: 'test', age: 18, }, } })页面中引用
import React, { FC, useEffect } from 'react' import styles from './Home.module.scss' import { useNavigate } from 'react-router-dom' import { Typography, Button } from 'antd' import { MANAGE_INDEX_PATHNAME } from '../router' import axios from 'axios' import '../_mock/index' const { Title, Paragraph } = Typography const Home: FC = () => { const nav = useNavigate() useEffect(() => { axios.get('/api/test').then(res => console.log(res)) }, []) return ( <div className={styles.container}> <div className={styles.info}> <Title>问卷调查 | 在线投票</Title> <Paragraph> 已累计创建问卷 100 份,发布问卷 90 份,收到答卷 980 份 </Paragraph> <div> <Button type="primary" onClick={() => nav(MANAGE_INDEX_PATHNAME)}> 创建问卷 </Button> </div> </div> </div> ) } export default Home思考:
为什么 useEffect 会执行两次?
React 18 中,useEffect 默认会在开发模式下执行两次,这是为了帮助开发者发现副作用的潜在问题。
参考:
github /nuysoft/Mock/wiki/Getting-Started
服务端 nodejs 实现 mock.js 服务端 mock 实现
目标:
服务端实现mock要点:
mock.js 用于劫持网络请求,并实现丰富的 Random 能力mock.js 部署于 nodejs 服务端,并实现 Random 功能安装:
npm init -y npm i mockjs npm i koa koa-router npm i nodemon # 用于监听node修改, 不用重启项目功能实现:
思路:服务端采用 Koa 构建路由处理需要 Mock 的 api
mock文件夹用于存放 Mock 的api,其中 index 做所有 Mock api 的整合。
目录:
. ├── index.js ├── mock │ ├── index.js │ ├── question.js │ └── test.js ├── package-lock.json ├── package.json └── projectTree.md 2 directories, 7 filesindex.js
注意:这里设计 getRes() 可以刻意延迟 1s,模拟 loading 效果
const Koa = require('koa') const Router = require('koa-router') const mockList = require('./mock/index') const app = new Koa() const router = new Router() // 模拟网络延迟函数 async function getRes(fn) { return new Promise(resolve => { setTimeout(() => { const res = fn() resolve(res) }, 1000) }) } mockList.forEach(item => { const {url, method, response} = item router[method](url, async (ctx, next) => { const res = await getRes(response) ctx.body = res } ) }) app.use(router.routes()) app.listen(3002)mock/index.js
const test = require('./test') const question = require('./question') const mockList = [ ...test, ...question ] module.exports = mockListquestion.js
const Mock = require('mockjs') const Random = Mock.Random module.exports = [ { url: '/api/question/:id', method: 'get', response() { return { error: 0, data: { id: Random.id(), title: Random.ctitle(), content: Random.cparagraph() } } } }, { url: '/api/question', method: 'post', response() { return { error: 0, data: { id: Random.id() } } } } ]**测试:**采用 postman 进行测试 post 请求
跨域问题处理
**目标:**处理跨域问题
刚刚我们搞定了服务端的 Mock,并处理了前端页面。现在遇到跨域问题:
问题:
问题原因:
http://localhost:3001/home 访问 http://localhost:3002/api/test 会产生 CORS 即跨域问题
Home.tsx
const nav = useNavigate() useEffect(() => { try { const fetchData = async () => { try { const response = await axios.get('http://localhost:3001/api/test') console.log(response.data) // 输出返回的响应数据 } catch (error) { console.error('请求失败', error) } } fetchData() } catch (error) { console.error('请求失败', error) } }, [])解决方案:
采用 Craco 来处理 React 中的跨域问题,本质上讲是通过拓展 React 的 CRA 工具配置来处理跨域
具体步骤:
前端配置Craco: 配置见参考文档通过 Craco 构建 api 代理**结果:**成功处理
参考文档:
github /dilanx/cracoAPI 设计 Restful API
概念:
RESTful API 是一种基于 REST(Representational State Transfer)架构风格的 Web 服务设计方法。
特点:
资源导向:
将系统中的所有内容视为资源,每个资源有唯一的 URI(统一资源标识符)。例如,/users 表示用户资源。无状态性(Statelessness):
每个请求都是独立的,不依赖于之前的请求。服务器不保留客户端状态信息。表现层状态转移(Representation of Resources):
通过 JSON、XML 等格式在客户端和服务器之间传递资源的表现形式,而不是资源本身。统一接口:
定义一致的方式进行操作,使得不同的客户端可以以统一的接口与服务器交互。自描述消息:
请求和响应中包含所有必要的信息,例如 HTTP 状态码、头信息等,以帮助客户端理解操作结果。可缓存性:
设计 API 使得响应可以被缓存,从而提高性能。使用标准 HTTP 方法:
使用 HTTP 动词来操作资源: GET:获取资源。POST:创建资源。PUT:更新资源。DELETE:删除资源。总结:
RESTful API 简洁灵活,适用于构建现代Web服务,因其遵循标准化的设计原则,使得开发和集成变得简单直观。
用户和问卷API设计
以下是设计的 API 表格,涵盖了用户功能和问卷功能:
功能方法路径请求体响应获取用户信息GET/api/user/info无{ errno: 0, data: {...} } 或 { errno: 10001, msg: 'xxx' }注册POST/api/user/register{ username, password, nickname }{ errno: 0 }登录POST/api/user/login{ username, password }{ errno: 0, data: { token } } — JWT 使用 token创建问卷POST/api/question无{ errno: 0, data: { id } }获取单个问卷GET/api/question/:id无{ errno: 0, data: { id, title ... } }获取问卷列表GET/api/question无{ errno: 0, data: { list: [ ... ], total } }更新问卷信息PATCH/api/question/:id{ title, isStar ... }{ errno: 0 }批量彻底删除问卷DELETE/api/question{ ids: [ ... ] }{ errno: 0 }复制问卷POST/api/question/duplicate/:id无{ errno: 0, data: { id } }说明:
GET 请求 通常用于获取资源,不需要请求体。POST 请求 用于创建资源或进行某些操作,可能需要请求体包含必要的数据。PATCH 请求 用于部分更新资源,需要请求体提供更新的字段和值。DELETE 请求 用于删除资源,这里是“假删除”,通过更新 isDeleted 属性实现。问卷功能实现
目标:
配置 axios 基础功能开发问卷功能,期间使用 useRequest分页和 LoadMore接口案例测试
**目标:**构建接口文件并测试
要点:
设计 axios instance 实例设计 getQuestionList 接口测试 getQuestionList 接口文件树:
├── src │ ├── services │ │ ├── ajax.ts │ │ └── question.tsajax.ts
import axios from 'axios' import { message } from 'antd' const instance = axios.create({ timeout: 10000, }) instance.interceptors.response.use(res => { const resData = (res.data || {}) as ResType console.log('resData', resData) const { errno, data, msg } = resData if (errno !== 0) { if (msg) { message.error(msg) } throw new Error(msg || '未知错误') } return data as any }) export default instance export type ResType = { errno: number data?: ResDataType msg?: string } // key表示字段名,any表示字段值的类型 export type ResDataType = { [key: string]: any }question.tsx
import axios, { ResDataType } from './ajax' export const getQuestionList = async (id: string): Promise<ResDataType> => { const url = `/api/question/${id}` const data = (await axios.get(url)) as ResDataType return data }接口测试:
import React, { FC, useEffect } from 'react' import { useParams } from 'react-router-dom' import { getQuestionList } from '../../../services/question' const Edit: FC = () => { const { id = '' } = useParams() useEffect(() => { async function fetchData() { const res = await getQuestionList(id) console.log('res', res) } fetchData() }, []) return ( <div> <h1>Edit {id}</h1> </div> ) } export default Edit设置 loading 状态优化体验
前言:之前我们在设计接口的时候,故意设计延迟函数用于模拟
// 模拟网络延迟函数 async function getRes(fn) { return new Promise(resolve => { setTimeout(() => { const res = fn() resolve(res) }, 1000) }) }**问题:**现在我们设计完成新增问卷函数 createQuestionService() 。实际测试时,点击创建页面到新页面时,会发生1s的延迟。在这期间,我们仍然可以频繁点击创建问卷,如下所示:
**解决方案:**设置 disable 属性,当点击时禁用问卷创建即可
Case:
import { createQuestionService } from '../services/question' const ManageLayout: FC = () => { const nav = useNavigate() const { pathname } = useLocation() const [loading, setLoading] = useState(false) async function handleCreateClick() { setLoading(true) const data = await createQuestionService() const { id } = data if (id) { nav(`/question/edit/${id}`) message.success('创建成功') } setLoading(false) } return ( <> <div className={styles.container}> <div className={styles.left}> <Flex gap="small" wrap> <Button type="primary" size="large" icon={<PlusOutlined />} disabled={loading} onClick={handleCreateClick} > 新建问卷 </Button> </Flex> </div> </> ) } export default ManageLayout自定义Hook抽离公共逻辑
**思路:**抽离原有的获取编辑页面的数据为 hook,方便编辑页面进行复用。
不用 Hook 之前:/edit/index
import React, { FC, useState, useEffect } from 'react' import { useParams } from 'react-router-dom' import { getQuestionListService } from '../../../services/question' const Edit: FC = () => { const { id = '' } = useParams() const [loading, setLoading] = useState(true) const [questionData, setQuestionData] = useState({}) useEffect(() => { async function fetchData() { const res = await getQuestionListService(id) setQuestionData(res) setLoading(false) } fetchData() }, []) return ( <div> <h1>Edit {id}</h1> {loading ? ( <div>Loading...</div> ) : ( <div> <p>{JSON.stringify(questionData)}</p> </div> )} </div> ) } export default EditHook设计:
hooks/useLoadQuestionData
import { useEffect, useState } from 'react' import { useParams } from 'react-router-dom' import { getQuestionListService } from '../services/question' function useLoadQuestionData() { const { id = '' } = useParams() const [loading, setLoading] = useState(true) const [questionData, setQuestionData] = useState({}) useEffect(() => { async function fetchData() { const res = await getQuestionListService(id) setQuestionData(res) setLoading(false) } fetchData() }, []) return { loading, questionData } } export default useLoadQuestionData优化之后:/edit/index
import React, { FC } from 'react' import { useParams } from 'react-router-dom' import useLoadQuestionData from '../../../hooks/useLoadQuestionData' const Edit: FC = () => { const { id = '' } = useParams() const { loading, questionData } = useLoadQuestionData() return ( <div> <h1>Edit {id}</h1> {loading ? ( <div>Loading...</div> ) : ( <div> <p>{JSON.stringify(questionData)}</p> </div> )} </div> ) } export default EdituseRequest重构Ajax请求
**思路:**采用 ahooks 中的 useRequest 钩子重构之前的 Ajax 请求
useRequest:
默认请求:默认情况下,useRequest 第一个参数是一个异步函数,在组件初始化时,会自动执行该异步函数。同时自动管理该异步函数的 loading , data , error 等状态。
const { data, error, loading } = useRequest(service);**Case:**重构 useLoadQuestionData
原本:
import { useEffect, useState } from 'react' import { useParams } from 'react-router-dom' import { getQuestionListService } from '../services/question' function useLoadQuestionData() { const { id = '' } = useParams() const [loading, setLoading] = useState(true) const [questionData, setQuestionData] = useState({}) useEffect(() => { async function fetchData() { const res = await getQuestionListService(id) setQuestionData(res) setLoading(false) } fetchData() }, []) return { loading, questionData } } export default useLoadQuestionData重构之后:
import { useParams } from 'react-router-dom' import { getQuestionListService } from '../services/question' import { useRequest } from 'ahooks' function useLoadQuestionData() { const { id = '' } = useParams() async function load() { const data = await getQuestionListService(id) return data } const { data, loading } = useRequest(load) return { data, loading } } export default useLoadQuestionData参考:
ahooks.js.org/zh-CN/hooks/use-request/index#index-default
分页功能实现
要点:
从 URL 参数中获取 page 和 pageSize, 并同步到 Pagination 组件中当 page 或 pageSize 变化时, 更新 URL 参数AntD 中 Pagination 的 current、pageSize、total、onChange 等属性和方法ListPage.tsx
import React, { FC } from 'react' import { Pagination, PaginationProps } from 'antd' import { useSearchParams, useNavigate, useLocation } from 'react-router-dom' import { LIST_PAGE_SIZE, LIST_PAGE_PARAM_KEY, LIST_PAGE_SIZE_PARAM_KEY, } from '../constant' type ListPageProps = { total: number } const ListPage: FC<ListPageProps> = (props: ListPageProps) => { const { total } = props const [current, setCurrent] = React.useState(1) const [pageSize, setPageSize] = React.useState(LIST_PAGE_SIZE) // 从 URL 参数中获取 page 和 pageSize, 并同步到 Pagination 组件中 const [searchParams] = useSearchParams() const nav = useNavigate() const { pathname } = useLocation() const handleChange: PaginationProps['onChange'] = pageNumber => { searchParams.set(LIST_PAGE_PARAM_KEY, pageNumber.toString()) searchParams.set(LIST_PAGE_SIZE_PARAM_KEY, pageSize.toString()) nav({ pathname, search: searchParams.toString(), }) } React.useEffect(() => { const page = parseInt(searchParams.get(LIST_PAGE_PARAM_KEY) || '') || 1 const pageSize = parseInt(searchParams.get(LIST_PAGE_SIZE_PARAM_KEY) || '') || LIST_PAGE_SIZE setCurrent(page) setPageSize(pageSize) }, [searchParams]) return ( <Pagination current={current} pageSize={pageSize} total={total} onChange={handleChange} /> ) } export default ListPage问卷中进行使用:
Star.tsx
... const { Title } = Typography const Star: FC = () => { useTitle('星标问卷') const { data = {}, loading } = useLoadQuestionListData({ isStar: true }) const { list = [], total = 0 } = data return ( <> ... {!loading && list.length > 0 && ( <div className={styles.footer}> <ListPage total={total} /> </div> )} </> ) } export default StarLoadMore 功能实现
要点:
防抖功能实现思路:
当页面的 ele 的 bottom 距离顶部一段距离时,自动加载页面
问卷标星、复制、删除功能
**目标:**实现问卷标星功能
**效果:**点击星标更新
思路:
标星接口更新实现:采用 useRequest 实现页面标星状态更新:采用 useRequest 的回调函数实现Code:
const [isStarState, setIsStarState] = useState(isStar) // 标星接口更新实现 const { loading: changeStarLoading, run: changeStar } = useRequest( async () => { await updateQuestionService(_id, { isStar: !isStarState }) }, { // 页面标星状态更新 manual: true, onSuccess: () => { setIsStarState(!isStarState) message.success('已更新') }, } )**目标:**实现问卷复制功能
思路:
实现复制功能的接口请求实现复制功能的回调实现,实现导航到编辑页面细节:
防止重复点击:loading: duplicateLoading 绑定到 Button 上,当我们点击复制后,在接口数据返回前,按钮无法再次点击。Code:
const { loading: duplicateLoading, run: duplicate } = useRequest( async () => { const data = await duplicateQuestionService(_id) return data }, { manual: true, onSuccess: (res: any) => { message.success('复制成功') nav(`/question/edit/${res.id}`) }, } ) ---- <Popconfirm title="确认复制吗?" okText="确认" cancelText="取消" onConfirm={duplicate} > <Button type="text" size="small" icon={<CopyOutlined />} disabled={duplicateLoading} > 复制 </Button> </Popconfirm>**目标:**实现问卷删除功能
**需求:**实现删除功能,问卷点击删除是假删除,删除后,问卷会进入回收站
思路:
实现删除功能的接口请求与回调实现当删除后,页面会不再渲染此卡片 const [isDeleted, setIsDeleted] = useState(false) const { loading: deleteLoading, run: deleteQuestion } = useRequest( async () => await updateQuestionService(_id, { isDeleted: true }), { manual: true, onSuccess: () => { message.success('删除成功') }, } ) function del() { confirm({ title: '删除问卷', icon: <ExclamationCircleOutlined />, onOk() { deleteQuestion() setIsDeleted(true) }, }) } // 实现当删除后,页面会不再渲染此卡片 if (isDeleted) return null // 已经删除的问卷,不要再渲染卡片了 return ( <div className={styles.container}> <div className={styles.title}> <div className={styles.left}> ... --- <Button type="text" size="small" icon={<DeleteOutlined />} onClick={del} disabled={deleteLoading} > 删除 </Button>问卷恢复与删除
要点:
for await (const id of selectionIds) 可以遍历请求useRequeset 中的debounceWait 可以实现恢复防抖 useRequeset 中的refresh() 可以实现:使用上一次的参数,重新发起请求。理解:refresh() 触发数据的重新加载,它确保在执行恢复和删除操作后,页面上的数据能够及时更新,避免了显示过时的信息。
Code:
const { data = {}, loading, refresh, } = useLoadQuestionListData({ isDeleted: true }) ... // 恢复 const { run: recover } = useRequest( async () => { for await (const id of selectionIds) { await updateQuestionService(id, { isDeleted: false }) } }, { manual: true, debounceWait: 500, onSuccess: () => { message.success('恢复成功') refresh() setSelectionIds([]) }, } ) // 删除 const { run: deleteQuestion } = useRequest( async () => await deleteQuestionService(selectionIds), { manual: true, onSuccess: () => { message.success('删除成功') refresh() setSelectionIds([]) }, } )React低代码项目:网络请求与问卷基础实现由讯客互联游戏开发栏目发布,感谢您对讯客互联的认可,以及对我们原创作品以及文章的青睐,非常欢迎各位朋友分享到个人网站或者朋友圈,但转载请说明文章出处“React低代码项目:网络请求与问卷基础实现”