文章目錄
- 1 API設計
- 1.1 用戶功能
- 1.1.1 獲取用戶信息
- 1.1.2 注冊
- 1.1.3 登錄
- 1.2 問卷功能
- 1.2.1 獲取單個問卷
- 1.2.2 獲取問卷列表
- 1.2.3 創建問卷
- 1.2.4 更新問卷
- 1.2.5 批量徹底刪除問卷
- 1.2.6 復制問卷
- 1.3 小結
- 2 實戰
- 2.1配置axios
- 2.2 封裝API和測試
- 2.3 新建問卷
- 2.4 自定義hooks封裝獲取問卷信息
- 2.5 使用useRequest重構Ajax請求-統一處理
- 2.6 開發問卷列表
- 2.7 第三方hook抽離搜索功能
- 2.8 開發星標和回收站
- 結語
1 API設計
1.1 用戶功能
1.1.1 獲取用戶信息
- method:
get
- path:
/api/user/info
- response:
- 成功:
{errno: 0, data{...}}
- 失敗:
{errno: 10001, mes: 'xxx'}
- 成功:
1.1.2 注冊
-
method:
post
-
path:
/api/user/register
-
request body:
{username, password, nickname}
-
response:
{errno: 0}
1.1.3 登錄
- method:
post
- path:
/api/user/login
- request body:
{username, password}
- response:
{errno: 0, token}
- Token令牌:使用jwt
1.2 問卷功能
1.2.1 獲取單個問卷
- method:
get
- path:
/api/question/:id
- response:
{errno: 0, data: {id, title,...}}
1.2.2 獲取問卷列表
- method:
get
- path:
/api/question/
- response:
{errno: 0, data:{list:[{...}}
1.2.3 創建問卷
- method:
post
- path:
/api/question
- request body: 暫無
- response:
{errno: 0, data: {id}}
1.2.4 更新問卷
- method:
patch
- path:
/api/question/:id
- response:
{errno: 0}
tips:刪除是“假刪除”,實際 是更新isDeleted
屬性
1.2.5 批量徹底刪除問卷
- method:
delete
- path:
/api/question
- request body:
{ids: [...]}
- response:
{errno: 0}
1.2.6 復制問卷
- method:
post
- path: `/api/question/duplicate/:id``
- response:
{errno: 0, data: {id}}
1.3 小結
- 使用Restful API
- 用戶驗證使用JWT
- 統一返回格式:
{errno, data, msg}
2 實戰
2.1配置axios
src/services/request.ts基礎代碼如下:
import axios from "axios";const request = axios.create({timeout: 5000,
});export default request;
添加返回類型和統一錯誤出來,request.ts代碼如下:
import axios from "axios";
import { message } from "antd";const request = axios.create({timeout: 5000,
});// response 攔截:統一處理errno和msg
request.interceptors.response.use(res => {const resData = (res.data || {}) as ResTypeconst {errno, data, msg} = resDataif (errno !== 0) {// 錯誤提示if (msg) {message.error(msg)}throw new Error(msg);}return data as any}
)
export default request;export type ResDataType = {[key: string]: any;
};export type ResType = {errno: number;data?: ResDataType;msg?: string;
};
2.2 封裝API和測試
獲取問卷信息API,src/api/question.ts代碼如下:
import request, { ResDataType } from "../services/request";export async function getQuestionApi(id: string): Promise<ResDataType> {const url = `/api/question/${id}`;const data = (await request.get(url)) as ResDataType;return data;
}
src/pages/quesiton/Edit/index.tsx代碼如下:
import { FC, useEffect} from "react";
import { useParams } from "react-router-dom";
import { getQuestionApi } from "@/api/question";const Edit: FC = () => {const { id = "" } = useParams();// 獲取問卷信息useEffect(()=> {async function fn() {const resData = await getQuestionApi(id)console.log(resData);}fn()}, [])return <div>Edit {id}</div>;
};export default Edit;
測試結果如下圖所示:
2.3 新建問卷
封裝新建問卷API,question.ts代碼如下:
import request, { ResDataType } from "../services/request";.../*** 新建問卷* @returns 問卷id*/
export async function createQuestionApi() {const url = `/api/question`;const data = (await request.post(url)) as ResDataType;return data;
}
ManageLayout.tsx中調用新建問卷,src/layouts/MangeLayout.tsx代碼如下:
import { FC, useState } from "react";
import { Outlet, useNavigate, useLocation } from "react-router-dom";
import {PlusOutlined,BarsOutlined,StarOutlined,DeleteOutlined,
} from "@ant-design/icons";
import { Button, Space, Divider, message } from "antd";import { createQuestionApi } from "@/api/question";
import styles from "./ManageLayout.module.scss";const ManageLayout: FC = () => {const nav = useNavigate();const { pathname } = useLocation();const [loading, setLoading] = useState(false)// 點擊創建問卷async function handleCreateClick() {setLoading(true)const data = await createQuestionApi()const {id} = data || {}if (id) {nav(`/question/edit/${id}`)message.success("創建成功")}setLoading(false)}return (<div className={styles.container}><div className={styles.left}><Space direction="vertical"><Buttontype="primary"size="large"icon={<PlusOutlined />}onClick={handleCreateClick}disabled={loading}>新建問卷</Button>...);
};export default ManageLayout;
tips:接口請求有延遲,防止用戶連續點擊按鈕,觸發重復操作,這里添加loading控制按鈕,后續可以用封裝好的“防抖”操作替換。
2.4 自定義hooks封裝獲取問卷信息
在編輯頁和統計頁都需要獲取帶加載狀態的問卷信息,這里我們通過自定義hooks抽取公共部分,src/hooks/useLoadQuestionData.ts代碼如下:
import { useEffect, useState } from "react";
import { useParams } from "react-router-dom";import { getQuestionApi } from "@/api/question";/*** 獲取帶加載狀態的問卷信息* @returns loading狀態,問卷信息*/
function useLoadQuestionData() {const { id = "" } = useParams();const [loading, setLoading] = useState(true);const [questionData, setQuestionData] = useState({});useEffect(() => {async function fn() {const data = await getQuestionApi(id);setQuestionData(data);setLoading(false);}fn();}, []);return { loading, questionData };
}export default useLoadQuestionData;
編輯頁src/pages/question/Edit/index.tsx代碼改造如下:
import { FC } from "react";
import useLoadQuestionData from "@/hooks/useLoadQuestionData";const Edit: FC = () => {// 獲取問卷信息const { loading, questionData } = useLoadQuestionData();return (<div><p>Edit page</p><div>{loading ? <p>loading</p> : <p>{JSON.stringify(questionData)}</p>}</div></div>);
};export default Edit;
2.5 使用useRequest重構Ajax請求-統一處理
第三方ahooks-useRequst 當前功能有:
- 自動/手動請求
- 輪詢
- 防抖
- 節流
- 窗口聚焦刷新
- 錯誤重試
- 加載延遲
- SWR(stale-while-revalidate)
- 緩存
由于接口請求,每次都需要定義loading,data,使用useEffect執行函數,很繁瑣,這里我們使用第三方ahooks提供的useRequest簡化。
改造自定義hook-useLoadQuestionData,useLoadQuestionData.ts代碼如下:
import { useParams } from "react-router-dom";
import { useRequest } from "ahooks";
import { getQuestionApi } from "@/api/question";/*** 獲取帶加載狀態的問卷信息* @returns loading狀態,問卷信息*/
function useLoadQuestionData() {const { id = "" } = useParams();async function load() {const data = await getQuestionApi(id);return data;}const { loading, data, error } = useRequest(load);return { loading, data, error };
}export default useLoadQuestionData;
改造創建問卷,ManageLayout.tsx代碼如下:
import { FC, useState } from "react";
import { Outlet, useNavigate, useLocation } from "react-router-dom";
import {PlusOutlined,BarsOutlined,StarOutlined,DeleteOutlined,
} from "@ant-design/icons";
import { Button, Space, Divider, message } from "antd";import { createQuestionApi } from "@/api/question";
import styles from "./ManageLayout.module.scss";
import { useRequest } from "ahooks";const ManageLayout: FC = () => {const nav = useNavigate();const { pathname } = useLocation();// 點擊創建問卷const {loading,// error,run: handleCreateClick,} = useRequest(createQuestionApi, {manual: true,onSuccess: (res) => {nav(`/question/edit/${res.id}`);message.success("創建成功");},});return (<div className={styles.container}><div className={styles.left}><Space direction="vertical"><Buttontype="primary"size="large"icon={<PlusOutlined />}onClick={handleCreateClick}disabled={loading}>新建問卷</Button>
...
};export default ManageLayout;
2.6 開發問卷列表
獲取問卷列表mock接口 getQuestionList.js代碼如下:
const Mock = require('mockjs');
const Random = Mock.Random;function getQuestionList(len = 10, isDeleted = false) {const list = [];for (let i = 0; i < len; i++) {list.push({_id: Random.id(),title: Random.ctitle(),isPublished: Random.boolean(),isStar: Random.boolean(),answerCount: Random.natural(50, 100),createdAt: Random.datetime(),isDeleted})}return list;
}module.exports = getQuestionList;
question.js代碼如下:
const Mock = require('mockjs')const getQuestionList = require("./data/getQuestionList")const Random = Mock.Randommodule.exports = [
...{// 獲取問卷列表url: '/api/question',method: 'get',response() {return {errno: 0,data: {list: getQuestionList(),total: 100}}}},
]
question.ts api代碼如下所示:
/*** 獲取問卷列表* @returns 問卷列表數據*/
export async function getQuestionListApi() {const url = `/api/question`;const data = (await request.get(url)) as ResDataType;return data;
}
List.tsx調用接口代碼如下所示:
import { FC } from "react";
// import { useSearchParams } from "react-router-dom";
import { useRequest, useTitle } from "ahooks";
import { Typography, Spin, Divider } from "antd";import QuestionCard from "@/components/QuestionCard";
import ListSearch from "@/components/ListSearch";
import { getQuestionListApi } from "@/api/question";
import styles from "./common.module.scss";const { Title } = Typography;const List: FC = () => {useTitle("調查問卷-我的問卷");// const [searchParams] = useSearchParams();// console.log("keyword", searchParams.get("keyword"));//問卷列表數據const { data = {}, loading } = useRequest(getQuestionListApi);const { list = [], total = 0 } = data;return (<><div className={styles.header}><div className={styles.left}><Title level={3}>我的問卷</Title></div><div className={styles.right}><ListSearch /></div></div><div className={styles.content}>{loading && (<div style={{ textAlign: "center" }}><Spin /></div>)}{!loading &&list.length > 0 &&list.map((q: any) => {const { _id } = q;return <QuestionCard key={_id} {...q} />;})}</div><div className={styles.footer}>loadingMore 上劃加載更多</div></>);
};export default List;
2.7 第三方hook抽離搜索功能
在我的問卷、星標問卷和回收站都有搜索功能,有相同的邏輯,這里我們通過自定義hook抽離公共邏輯。自定義搜索hook-useLoadQuestionListData.ts代碼如下所示:
import { useSearchParams } from "react-router-dom";
import { useRequest } from "ahooks";
import { getQuestionListApi } from "@/api/question";import { LIST_SEARCH_PARAM_KEY } from "@/constant";/*** 獲取問卷列表* @returns 問卷列表*/
function useLoadQuestionListData() {const [searchParams] = useSearchParams();const keyword = searchParams.get(LIST_SEARCH_PARAM_KEY) || "";async function load() {const data = await getQuestionListApi({ keyword });return data;}const { loading, data, error } = useRequest(load, {refreshDeps: [searchParams],});return { loading, data, error };
}export default useLoadQuestionListData;
getQuestionListApi.ts如下所示:
import request, { ResDataType } from "../services/request";type SearchOption = {keyword: string;
};.../*** 獲取問卷列表* @param opt 請求參數* @returns 問卷列表數據*/
export async function getQuestionListApi(opt: Partial<SearchOption>
): Promise<ResDataType> {const url = `/api/question`;const data = (await request.get(url, {params: opt})) as ResDataType;return data;
}
說明:
- refreshDeps: useRequest 刷新依賴項,根據數組里面的變量重新執行useRequest
- request.get(url, {params: opt})),params axios get請求傳遞參數,形式:url?a=b&b=1…
2.8 開發星標和回收站
星標問卷查詢的數據 isStar=true
;回收站查詢的問卷列表isDeleted=true
,我們需要擴展api接口參數。
useLoadQuestionListData.ts代碼改造如下:
import { useSearchParams } from "react-router-dom";
import { useRequest } from "ahooks";
import { getQuestionListApi } from "@/api/question";import { LIST_SEARCH_PARAM_KEY } from "@/constant";type OptionType = {isStar: boolean;isDeleted: boolean;
};/*** 獲取問卷列表* @returns 問卷列表*/
function useLoadQuestionListData(opt: Partial<OptionType>) {const { isStar, isDeleted } = opt;const [searchParams] = useSearchParams();const keyword = searchParams.get(LIST_SEARCH_PARAM_KEY) || "";async function load() {const data = await getQuestionListApi({ keyword, isStar, isDeleted });return data;}const { loading, data, error } = useRequest(load, {refreshDeps: [searchParams],});return { loading, data, error };
}export default useLoadQuestionListData;
星標問卷-Star.tsx代碼如下所示:
import { FC } from "react";
import { useTitle } from "ahooks";
import { Typography, Empty, Spin } from "antd";import QuestionCard from "@/components/QuestionCard";
import ListSearch from "@/components/ListSearch";
import styles from "./common.module.scss";
import useLoadQuestionListData from "@/hooks/useLoadQuestionListData";const { Title } = Typography;const List: FC = () => {useTitle("調查問卷-星標問卷");//問卷列表數據const { data = {}, loading } = useLoadQuestionListData({ isStar: true });const { list = [], total = 0 } = data;return (<><div className={styles.header}><div className={styles.left}><Title level={3}>星標問卷</Title></div><div className={styles.right}><ListSearch /></div></div><div className={styles.content}>{loading && (<div style={{ textAlign: "center" }}><Spin /></div>)}{!loading && list.length === 0 && <Empty description="暫無數據" />}{!loading &&list.length > 0 &&list.map((q: any) => {const { _id } = q;return <QuestionCard key={_id} {...q} />;})}</div><div className={styles.footer}>分頁</div></>);
};export default List;
回收站-Trash.tsx代碼如下所示;
import { FC, useState } from "react";
import { useTitle } from "ahooks";
import {Typography,Empty,Table,Tag,Space,Button,Modal,message,Spin,
} from "antd";import { ExclamationCircleOutlined } from "@ant-design/icons";
import ListSearch from "../../components/ListSearch";
import styles from "./common.module.scss";
import useLoadQuestionListData from "@/hooks/useLoadQuestionListData";const { Title } = Typography;
const { confirm } = Modal;const List: FC = () => {useTitle("調查問卷-回收站");//問卷列表數據const { data = {}, loading } = useLoadQuestionListData({ isDeleted: true });const { list = [], total = 0 } = data;const columns = [{title: "標題",dataIndex: "title",},{title: "是否發布",dataIndex: "isPublished",render: (isPublished: boolean) =>isPublished ? <Tag color="processing">已發布</Tag> : <Tag>未發布</Tag>,},{title: "答卷",dataIndex: "answerCount",},{title: "創建時間",dataIndex: "createdAt",},];// 選擇ids集合const [selectedIds, setSelectedIds] = useState<string[]>([]);function del() {confirm({title: "您確定要刪除嗎?",okText: "確定",cancelText: "取消",content: "刪除以后不可找回!",icon: <ExclamationCircleOutlined />,onOk: () => message.success("刪除成功"),});}const TableEle = (<><div style={{ marginBottom: "10px" }}><Space><Button type="primary" disabled={selectedIds.length === 0}>恢復</Button><Button danger disabled={selectedIds.length === 0} onClick={del}>徹底刪除</Button></Space></div><TabledataSource={list}columns={columns}pagination={false}rowKey={(q: any) => q._id}rowSelection={{type: "checkbox",onChange: (selectRowKeys) => {setSelectedIds(selectRowKeys as string[]);},}}></Table></>);return (<><div className={styles.header}><div className={styles.left}><Title level={3}>回收站</Title></div><div className={styles.right}><ListSearch /></div></div><div className={styles.content}>{loading && (<div style={{ textAlign: "center" }}><Spin /></div>)}{!loading && list.length === 0 && <Empty description="暫無數據" />}{TableEle}</div><div className={styles.footer}>分頁</div></>);
};export default List;
服務端星標問卷查詢返回的isStar=true
;回收站返回的isDeleted=true
;
index.js代碼如下所示:
...async function getRes(fn, ctx) {return new Promise(resolve => {setTimeout(() => {const res = fn(ctx)resolve(res)}, 500);})
}// 注冊mock路由
mockList.forEach(item => {const {url, method, response} = itemrouter[method](url, async ctx => {const res = await getRes(response, ctx)ctx.body = res})
})...
question.js代碼如下:
...{// 獲取問卷列表url: '/api/question',method: 'get',response(ctx) {const { url = '' } = ctxconst isStar = url.indexOf('isStar=true') >= 0const isDeleted = url.indexOf('isDeleted=true') >= 0return {errno: 0,data: {list: getQuestionList({isStar, isDeleted}),total: 100}}}},
]
getQuestionList.js如下所示:
const Mock = require('mockjs');
const Random = Mock.Random;function getQuestionList(opt = {}) {const { len = 10, isStar, isDeleted } = optconst list = [];for (let i = 0; i < len; i++) {list.push({_id: Random.id(),title: Random.ctitle(),isPublished: Random.boolean(),isStar: isStar || Random.boolean(),answerCount: Random.natural(50, 100),createdAt: Random.datetime(),isDeleted: isDeleted || Random.boolean()})}return list;
}module.exports = getQuestionList;
說明
- 數據展示有3種狀態:根據不同的狀態有不同展示
- 加載中:
- 加載完成,數據為空
- 加載完成,有數據
- rowKey={(q: any) => q._id}:ts不設置any類型,程序不報錯,但是ts檢查錯誤
結語
?QQ:806797785
??倉庫地址:https://gitee.com/gaogzhen
??倉庫地址:https://github.com/gaogzhen
[1]ahook官網[CP/OL].
[2]mock文檔[CP/OL].
[3]Ant Design官網[CP/OL].