前言
灵感来源于 ahooks 的 useRequest
背景
我们先看个需求:做一个展示图片的页面。
你的代码可能是这样子的:
img.tsx
import react, { useEffect, useState } from "react";
import MyFetch from "../../lib/fetch";
export const PhotoExhibition = () => {
const [imgUrl, setImgUrl] = useState("");
useEffect(() => {
MyFetch({
url: "https://api.uomg.com/api/rand.img1?sort=汽车&format=json",
}).then((res) => {
setImgUrl(res.imgurl);
});
}, []);
return (
<>
<img src={imgUrl} alt="汽车" />
</>
);
};
手摸手第一版
我们想通过hooks改造它,这时 useRequest 将横空出世。
我们先做些简单的功能,需求:
- 返回网络请求的数据 - data
- 我们将请求与hook相结合 - useRequest useRequest.tsx
import { useState, useEffect } from "react";
import MyFetch, { IReq } from "../../../../lib/fetch";
function useRequest (
req: IReq,
) {
const [data, setData] = useState<any>(null);
useEffect(() => {
service();
}, []);
const service = () => {
MyFetch(req)
.then((res) => {
setData(res);
})
.catch((err) => new Error(err));
};
return {
data,
};
}
export default useRequest;
自己基于fetch封装的请求文件,自然你也可以使用优秀的 axios 代替
fetch.tsx
export interface IRes {
//与后端约定的格式
code: number;
message: string;
data: any;
}
interface IInitHeader extends RequestInit {
body?: BodyInit | null | any;
}
export interface IReq {
url: string;
body?: BodyInit | null | any;
method?: "GET" | "POST" | "PUT" | "DELETE";
requestInit?: RequestInit;
}
const checkStatus = (res: Response) => {
// 处理状态码
// if (res.status === 401) {
// window.location.href = "/login";
// }
// if (res.status === 405 || res.status === 403) {
// location.href = "/403";
// }
if (res.status === 404) {
new Error("code: 404, 接口不存在");
}
return res;
};
const addCustomHeader = (init?: IInitHeader) => {
// 自定义添加头部信息
init = init || {};
init.headers = Object.assign(init?.headers || {}, {
// 这里添加项目固定请求头信息
// 'X-SSO-USER': 'admin',
});
return init;
};
// 用于取消请求
export let controller = new AbortController();
export default function MyFetch(req: IReq) {
let { url, body, method, requestInit: init } = req;
if (!init) init = {};
// if (!init.credentials) init.credentials = "include"; // 默认same-origin,include不管同源请求,还是跨域请求,一律发送 Cookie。
if (body && typeof body === "object") init.body = JSON.stringify(body); // 转化JSON 字符串
if (method) init.method = method; // 使用传入请求方法, 不传则GET
if (body && !method) init.method = "POST"; // 兼容, 存在body忘传method,默认请求方法POST
if (init.method) init.method = init.method.toUpperCase(); // 兼容,字符串转换为大写
if (["POST", "PUT", "DELETE"].includes(init.method || "")) {
// 提交JSON数据, 默认值是'text/plain;charset=UTF-8'
init.headers = Object.assign(
{},
init.headers || {
"Content-Type": "application/json",
}
);
}
init = addCustomHeader(init);
controller = new AbortController(); // 每次需要新的controller,要不然取消请求后再次请求将报错
init = { ...init, signal: controller.signal };
return window
.fetch(url, init)
.then((res) => checkStatus(res)) //检验响应码
.then((res) => res.json())
.catch((err) => new Error(`请求失败!url: ${url} err: ${err}`));
}
export function formFetch(req: IReq) {
let { url, requestInit: init } = req;
// 表单fetch,用于上传文件等
if (!init) init = {};
init = addCustomHeader(init);
return window
.fetch(url, init)
.then((res) => checkStatus(res))
.then((res) => res.json())
.catch((err) => new Error(`请求失败!url: ${url} err: ${err}`));
}
接下来对img.tsx进行改造
img.tsx
import react from "react";
import useRequest from "../../components/hooks/useRequest";
export const PhotoExhibition = () => {
const { data: imgData } = useRequest({
url: "https://api.uomg.com/api/rand.img1?sort=汽车&format=json",
});
return (
<>
<img src={imgData?.imgurl} alt="汽车" />
</>
);
};
我们发现代码相对原来变得简洁的许多,我们页面将不在关注 请求 层面的开发。
这时候第一版算是告一段落。
手摸手第二版
接下来我们在第一版增加一些功能,我们希望请求是手动的,并不是一进页面就调接口,并希望给我们暴露更多的其它功能。
需求:
- 增加请求接口的状态 - loading
- 提供手动执行的方法 - run
- 提供取消请求接口的方法 - cancel
- 增加成功、失败、执行完成的回调 - onSuccess、onError、onFinally
- 增加自动执行与手动执行的切换标识 - manual useRequest.tsx
import { useState, useEffect } from "react";
import MyFetch, { formFetch, controller, IReq } from "../../../../lib/fetch";
export interface Options<TData, TParams extends any[]> {
manual?: boolean; // 如果设置为 true,则需要手动发送请求即手动调用run
onSuccess?: (data: TData) => void; // 成功触发
onError?: (e: Error) => void; // 失败触发
onFinally?: (data: TData) => void; // 执行完成触发
}
export interface IParams {
url?: string;
body?: BodyInit | null | any;
}
function useRequest<TData, TParams extends any[]>(
req: IReq,
options?: Options<TData, TParams>
) {
const [loading, setLoading] = useState(false);
const [data, setData] = useState<any>({});
const { onFinally, onSuccess, onError, manual = false } = options || {};
useEffect(() => {
if (!manual) service();
}, []);
const service = (params?: IParams) => {
if (params?.url) req.url = params.url;
if (params?.body) req.body = params.body;
setLoading(true);
MyFetch(req)
.then((res) => {
setData(res);
onSuccess && onSuccess(res);
})
.finally(() => {
setLoading(false);
onFinally && onFinally(data);
})
.catch(onError);
};
const run = (params?: IParams) => {
service(params);
};
const cancel = () => {
controller.abort();
};
return {
loading,
data,
run,
cancel,
};
}
export default useRequest;
接下来对img.tsx进行改造
img.tsx
import react, { useState } from "react";
import useRequest from "../../components/hooks/useRequest";
export const PhotoExhibition = () => {
const [sort, setSort] = useState("汽车");
const {
loading: imgLoading,
data: imgData,
run,
cancel,
} = useRequest(
{
url: `https://api.uomg.com/api/rand.img1?sort=${sort}&format=json`,
},
{
manual: true,
onSuccess: (result) => {
console.log(result, "onSuccess");
},
onError: (error) => {
console.log(error, "onError");
},
onFinally: (result) => {
console.log(result, "onFinally");
},
}
);
return (
<>
<input
type="text"
value={sort}
onChange={(e) => setSort(e.target.value)}
/>
<button
onClick={() => {
run({
url: `https://api.uomg.com/api/rand.img1?sort=${sort}&format=json`,
});
}}
>
search
</button>
<button onClick={cancel}>取消加载</button>
<br />
{imgLoading ? "加载中..." : <img src={imgData?.imgurl} alt="汽车" />}
</>
);
};
到了这里我们的 useRequest 基本满足项目的一般需求。
温馨提示:测试取消请求时可以把网络换成3g在测试。
手摸手第三版
好了不逼逼,直接说个常见的需求:有个input,我们并不希望它频繁触发请求。
- 防抖 - debounce
- 节流 - throttle
防抖
useRequest.tsx
import { useState, useEffect, useRef } from "react";
import MyFetch, { formFetch, controller, IReq } from "../../../../lib/fetch";
import { debounce, DebouncedFunc, throttle } from "lodash";
export interface Options<TData, TParams extends any[]> {
manual?: boolean; // 如果设置为 true,则需要手动发送请求即手动调用run
onSuccess?: (data: TData) => void; // 成功触发
onError?: (e: Error) => void; // 失败触发
onFinally?: (data: TData) => void; // 执行完成触发
debounceWait?: number; // 防抖等待时间, 单位为毫秒,设置后,进入防抖模式
throttleWait?: number; // 节流等待时间, 单位为毫秒,设置后,进入节流模式
}
export interface IParams {
url?: string;
body?: BodyInit | null | any;
}
function useRequest<TData, TParams extends any[]>(
req: IReq,
options?: Options<TData, TParams>
) {
const [loading, setLoading] = useState(false);
const [data, setData] = useState<any>({});
const {
onFinally,
onSuccess,
onError,
manual = false,
debounceWait,
throttleWait,
} = options || {};
const debouncedRef = useRef<DebouncedFunc<any>>();
useEffect(() => {
if (!manual) service();
}, []);
useEffect(() => {
if (debounceWait) {
debouncedRef.current = debounce((query: IReq) => {
request(query);
}, debounceWait);
return () => {
debouncedRef.current?.cancel();
};
}
}, [debounceWait]);
const request = (query: IReq) => {
console.log(333);
MyFetch(query)
.then((res) => {
setData(res);
onSuccess && onSuccess(res);
})
.finally(() => {
setLoading(false);
onFinally && onFinally(data);
})
.catch(onError);
};
const service = (params?: IParams) => {
if (params?.url) req.url = params.url;
if (params?.body) req.body = params.body;
setLoading(true);
if (debounceWait) {
console.log(222);
return debouncedRef.current && debouncedRef.current(req);
}
// if (throttleWait) return throttle(request, throttleWait);
request(req);
};
const run = (params?: IParams) => {
service(params);
};
const cancel = () => {
controller.abort();
};
return {
loading,
data,
run,
cancel,
};
}
export default useRequest;
在img.tsx下多传下 debounceWait 就好
img.tsx
const {
loading: imgLoading,
data: imgData,
run,
cancel,
} = useRequest(
{
url: `https://api.uomg.com/api/rand.img1?sort=${sort}&format=json`,
},
{
manual: true,
debounceWait: 2000,
onSuccess: (result) => {
console.log(result, "onSuccess");
},
onError: (error) => {
console.log(error, "onError");
},
onFinally: (result) => {
console.log(result, "onFinally");
},
}
);
接下来测试下
看起来是很OK的,我点了17下后延迟2s才触发。
温馨提示: 需要使用以下这段代码来确保 debouncedRef 的唯一性,如果你有更好的方法那更好。
const debouncedRef = useRef<DebouncedFunc<any>>();
useEffect(() => {
if (!manual) service();
}, []);
useEffect(() => {
if (debounceWait) {
debouncedRef.current = debounce((query: IReq) => {
request(query);
}, debounceWait);
return () => {
debouncedRef.current?.cancel();
};
}
}, [debounceWait]);
节流
节流与防抖同理,以下是最终的代码。
useRequest.tsx
import { useState, useEffect, useRef } from "react";
import MyFetch, { formFetch, controller, IReq } from "../../../../lib/fetch";
import { debounce, DebouncedFunc, throttle } from "lodash";
export interface Options<TData, TParams extends any[]> {
manual?: boolean; // 如果设置为 true,则需要手动发送请求即手动调用run
onSuccess?: (data: TData) => void; // 成功触发
onError?: (e: Error) => void; // 失败触发
onFinally?: (data: TData) => void; // 执行完成触发
debounceWait?: number; // 防抖等待时间, 单位为毫秒,设置后,进入防抖模式
throttleWait?: number; // 节流等待时间, 单位为毫秒,设置后,进入节流模式
}
export interface IParams {
url?: string;
body?: BodyInit | null | any;
}
function useRequest<TData, TParams extends any[]>(
req: IReq,
options?: Options<TData, TParams>
) {
const [loading, setLoading] = useState(false);
const [data, setData] = useState<any>({});
const {
onFinally,
onSuccess,
onError,
manual = false,
debounceWait,
throttleWait,
} = options || {};
const debouncedRef = useRef<DebouncedFunc<any>>();
const throttleRef = useRef<DebouncedFunc<any>>();
useEffect(() => {
if (!manual) service();
}, []);
useEffect(() => {
if (debounceWait) {
debouncedRef.current = debounce((query: IReq) => {
request(query);
}, debounceWait);
return () => {
debouncedRef.current?.cancel();
};
}
}, [debounceWait]);
useEffect(() => {
if (throttleWait) {
throttleRef.current = throttle((query: IReq) => {
request(query);
}, throttleWait);
return () => {
throttleRef.current?.cancel();
};
}
}, [throttleWait]);
const request = (query: IReq) => {
MyFetch(query)
.then((res) => {
setData(res);
onSuccess && onSuccess(res);
})
.finally(() => {
setLoading(false);
onFinally && onFinally(data);
})
.catch(onError);
};
const service = (params?: IParams) => {
if (params?.url) req.url = params.url;
if (params?.body) req.body = params.body;
setLoading(true);
if (debounceWait) return debouncedRef.current && debouncedRef.current(req);
if (throttleWait) return throttleRef.current && throttleRef.current(req);
request(req);
};
const run = (params?: IParams) => {
service(params);
};
const cancel = () => {
controller.abort();
};
return {
loading,
data,
run,
cancel,
};
}
export default useRequest;
好了,第三版已完成。更多功能可根据项目需求自己开发。
总结
- 写得不好,不喜勿喷。
- 项目中许多常用的、重复的可以写成hook,以显得代码好看,彰显牛逼。