https://ahooks.js.org/zh-CN/guide
https://github.com/umijs/umi-request
useRequest规范
请求层治理:
- 统一请求库,新建应用,不需要重复实现一套请求层逻辑
- 规范请求接口设计规范
- 统一接口文档,应用接口设计一致
- 减少开发者在接口设计、文档维护、请求层逻辑开发上花费的沟通和人力成本
useRequest特点
- 自动请求,data,loading
- 手动请求
- run,runAsync
- manual:true
- cancel 取消请求
- refreshDeps: [] 请求依赖
- 轮询 pollingInterval: 60000
- loadingDelay:300
- 防抖 debounceInterval
- 节流 throttleInterval
- 屏幕聚焦重新请求 refreshOnWindowFocus
- 错误重试 retryCount
- SWR(stale-while-revalidate),缓存
- cacheKey
useRequest 参考
https://www.yuque.com/mayiprototeam/gfyt69/ggqy8q
const {
loading: boolean,
data?: TData,
error?: Error,
params: TParams || [],
run: (...params: TParams) => void,
runAsync: (...params: TParams) => Promise<TData>,
refresh: () => void,
refreshAsync: () => Promise<TData>,
// 通过 mutate,直接修改 data
mutate: (data?: TData | ((oldData?: TData) => (TData | undefined))) => void,
cancel: () => void,
} = useRequest<TData, TParams>(
service: (...args: TParams) => Promise<TData>,
{
manual?: boolean,
defaultParams?: TParams,
onBefore?: (params: TParams) => void,
onSuccess?: (data: TData, params: TParams) => void,
onError?: (e: Error, params: TParams) => void,
onFinally?: (params: TParams, data?: TData, e?: Error) => void,
}
);
run 轮询请求
manual = true ,即可阻止 useRequest 初始化执行。只有触发 run 时才会开始执行
polling 轮询文档 https://ahooks.js.org/zh-CN/hooks/use-request/polling
import { useRequest } from 'ahooks';
import { message } from 'antd';
function App() {
const { data, loading, run, cancel } = useRequest(fetchUser, {
manual: true, // 手动请求
retryCount: 3, // 请求异常后,3 次重试;第一次重试等待 2s,第二次重试等待 4s
pollingInterval: 60000, // ms 毫秒,轮询模式,定时 1分钟触发一次
pollingErrorRetryCount: 5, // 轮询错误重试次数
pollingWhenHidden: false, // 页面隐藏时,暂时停止轮询;显示时继续上次轮询
loadingDelay: 400, // ms 延迟 loading 变成 true 的时间,防止闪烁
onError: (error) => {
// 错误信息提示
message.error(error.message);
},
});
useEffect(() => {
run(id); // run传参
return () => {
cancel() // cancel 停止轮询
}
}, [id])
}
export function fetchUser() {
return fetch('/api/user')
}
- retryCount场景:API 偶尔会在短时间内出现故障
- http请求偶尔返回异常,最好为您的请求实施重试机制
- loadingDealy 假如请求在 300ms 内返回,则 loading 不会变成 true,避免了页面展示 Loading… 的情况。
- debounceWait: 防抖等待时间
- debounceMaxWait: 允许被延迟的最大值
- debounceLeading: 在延迟开始前执行调用
- debounceTrailing: 在延迟结束后执行调用
- 事件之后再最后一次触发的300秒后执行,场景:输入,查询;因为输入一直再触发,所以要等输入完毕之后在做操作
manual 手动触发请求
options.manual = true,初始化不会启动轮询,要通过 run/runAsync 触发开始
- run (…params: TParams) => void
- 自动捕获异常,通过 onError函数捕获异常
- runAsync (…params: TParams) => Promise
- 返回 Promise的异步函数,需要自己捕获异常
- runAsync只有真正执行的时候,才会返回Promise,没有执行时,不会有任何返回
- cancel 可以中止正在等待执行的函数
options.ready
参数,当其值为false
时,请求永远都不会发出run/runAsync
触发的请求都不会执行 ```tsx import { useRequest } from ‘@hooks’;
async function getUsername() { return Promise.resolve(‘jack’); }
export default () => { const { data, error, loading, run } = useRequest(getUsername, { manual: true, // 阻止初始化执行,只有触发 run 时才会开始执行。 })
if (error) return
return ( ) }
<a name="uoUNE"></a>
### visibilitychange 标签页隐藏或显示事件
浏览器标签页被隐藏或显示的时候,会触发 visibilitychange事件<br />[https://developer.mozilla.org/zh-CN/docs/Web/API/Document/visibilitychange_event](https://developer.mozilla.org/zh-CN/docs/Web/API/Document/visibilitychange_event)
- document.visibilityState取值:hidden,visible;
- document.hidden取值:true/false
<a name="tysxy"></a>
### visibilitychange场景
- 监听返回、分享、跳转、切入后台;
- 用于控制播放音乐视频、轮播等循环动画效果等;
- 可用于统计某个页面用户停留时长,即显示时开始计算,隐藏时结束计算
<a name="Em6tK"></a>
### refreshDeps 依赖参数请求
依赖改变,刷新请求
```jsx
const { data, run, loading } = useRequest(() => getUserSchool(userId), {
// 当数组内容变化后,发起请求,类似 useEffect的参数
refreshDeps: [userId],
debounceWait: 1000, // ms 频繁触发 run/runAsync,会以防抖策略进行请求,一秒
// throttleWait: 1000, // 节流
})
// refresh 刷新
const { data, refresh } = useRequest(() => getUserSchool(userId));
useEffect(() => {
refresh();
}, [userId]);
自动请求
useRequest 集成了 umi-request
- useRequest的用法和 axios基本一样
- 安装了 ahooks,就直接使用 useRequest请求,不需要在安装 umi-request
- 如果第一个参数不是 Promise,会通过 umi-request 来发起网络请求 ```javascript // 初始化立即执行 const { data, error, loading } = useRequest(‘/api/userInfo’); const { data, error, loading } = useRequest(getUsername)
// 用法 2 const { data, error, loading } = useRequest({ url: ‘/api/changeUsername’, method: ‘post’, });
// 用法 3
const { data, error, loading, run } = useRequest(userId=> {
// userId 是 run执行时,传进来的
return /api/userInfo/${userId}
});
// 用法 4 const { loading, run } = useRequest((username) => ({ url: ‘/api/changeUsername’, method: ‘post’, data: { username }, }));
<a name="LfdOn"></a>
### 并行请求 ❌
也就是同一个接口,我们需要维护多个请求状态<br />ahooks 3.x 取消了并行请求<br />[https://github.com/alibaba/hooks/issues/1378](https://github.com/alibaba/hooks/issues/1378)<br />原因:
- useRequest 实现并行能力复杂度太高
- 建议将 请求和UI 封装成一个组件实现并行请求的能力。而不是把所有请求都放到父级;
- 虚拟滚动场景,确实会有问题。这种场景建议自行管理请求状态
<a name="dtotT"></a>
### useRequest 生命周期
```javascript
const { data, loading, run, cancel } = useRequest(fetchUser, {
manual: true, // 手动请求
// 请求之前
onBefore: () => { },
// 请求成功触发
onSuccess: (res) => { console.log(res); },
// 请求完成
onFinally: () => { },
// 请求失败
onError: (error) => {
// 错误信息提示
message.error(error.message);
},
});
const { data, loading, run } = useRequest(data => ({
// data是 run传进来的
url: "http://127.0.0.1:8080/api/user",
method: "POST",
body: JSON.stringify(data),
headers: {
'Content-Type': 'application/json;charset=utf-8'
}
}), {
manual: true,
onSuccess: (result) => {
if (result.status === 'ok') {
message.success('流程已经提交');
}
}
});
usePagination分页
基于 useRequest 实现,封装了常见的分页逻辑;page、pageSize、total 管理
https://ahooks.js.org/zh-CN/hooks/use-pagination
筛选条件变化,重置分页,重新发起网络请求
usePagination 分页请求,不支持手动触发、不支持轮询等
import React from 'react';
import { usePagination } from 'ahooks';
import { List } from 'antd';
async function getUserList({
current,
pageSize,
}): Promise<{ total: number; list: UserListItem[] }> {
return fetch('/api/user', {current, pageSize})
}
export default () => {
const { data, loading, pagination } = usePagination(getUserList);
return (
<List
loading={loading}
dataSource={data}
renderItem={(item) => (
<List.Item>
<List.Item.Meta
avatar={<Avatar src="https://joesch.moe/api/v1/random" />}
title={<a href="https://ant.design">{item.title}</a>}
description="Ant Design for background applications"
/>
</List.Item>
)}
pagination={{
position: 'bottom',
align: 'right',
current: pagination.current,
pageSize: pagination.pageSize,
onChange: pagination.onChange,
onShowSizeChange: pagination.onChange,
showQuickJumper: true,
showSizeChanger: true,
total: data.total
}}
/>
);
};
网络请求失败自动重试
SWR 缓存 & 预加载
swr,stale-while-revalidate 在发起网络请求时,会优先返回之前缓存的数据
stale-whie-revalidate是 HTTP RFC 5861 中描述的一种 Cache-Control 扩展,属于对缓存到期的控制
- options.cacheKey,将当前请求成功的数据缓存起来
- 下次组件初始话时,如果有缓存数据,会优先返回缓存数据,然后在背后发送新请求,先使用旧的,如果有新的数据,就会拿新的替换
- staleTime 数据保持新鲜时间
- 在这个时间内,认为数据时新鲜的,不会重新发送请求
- 设置为 -1,表示数据永远新鲜
- cacheTime 数据缓存时间
- 超过这个时间,会清空这条数据缓存,缓存的回收时间,默认5分钟后回收
- 设置为-1,表示缓存数据永远不会过期
import { useRequest,clearCache } from 'ahooks'
const { data, loading, run, refresh, cancel } = useRequest(fetchUser, {
cacheKey: 'cacheUser',
staleTime: 60000 * 10, // ms
retryCount: 5, // 错误重试
});
对于一些数据不是经常变化的接口,使用 SWR 后,可以极大提高用户使用体验
配置 cacheKey ,即可进入 SWR 模式
同一个 cacheyKey 的数据是全局共享的。通过这个特性,可以实现“预加载”功能;
- 当我们改变其中某个cacheKey的内容时,其他相同cacheKey的内容均会同步;
- 缓存的数据包括data,params,我们可以记忆上一次请求的条件,并在下次初始化;
- 在发起网络请求时,会优先返回之前缓存的数据,然后在背后发起新的网络请求,最终用新的请求结果重新触发组件渲染;
- 只有成功的请求数据才会缓存
swr 特性在特定场景,对用户非常友好
// Cache-Control:max-age=600,stale-while-revalidate=30
const { data, loading } = useRequest(getArticle, {
cacheKey: 'articleKey',
refreshOnWindowFocus: true, // 屏幕重新聚焦或可见时,重新发起网络请求
});
asyncWithRetry
/**
* 网络请求失败,重新请求
* @param requestFunction request
* @param param {object} {retry: 3}
* @returns
*/
export async function asyncWithRetry(requestFunction, { retry = 3 } = {}) {
return new Promise((resolve, reject) => {
let timer = null;
function request(ms) {
requestFunction()
.then(res => {
if (timer) clearTimeout(timer)
resolve(res)
})
.catch(e => {
if (ms > 0) {
timer = setTimeout(() => request(ms - 1), 500)
} else {
reject(e)
}
});
}
request(retry)
});
}
axios.interceptors.response.use() 请求重试
https://www.yuque.com/fcant/web/wf6igm
requestWithRetry
/**
* 网络请求失败,重新请求
* @param requestFunction {function} request 请求
* @param errorFunction {function} 请求错误处理
* @param retry {number} 默认 3
* @returns
*/
export async function requestWithRetry(requestFunction, options) {
const {
onError,
retry = 3,
} = options ?? {};
try {
return await requestFunction();
} catch (e) {
if (retry <= 0) return onError ? onError(e) : null;
retry -= 1;
return await requestWithRetry(requestFunction, { onError, retry });
}
}
封装 useRequest
import request, { RequestOptionsInit } from 'umi-request';
import useAsync from './useAsync';
function useRequest(service: any, options?: any): any {
let promiseService: () => Promise<any>;
if (typeof service === 'string') {
promiseService = () => request(service);
} else if (typeof service === 'object') {
const { url, ...rest } = service;
promiseService = () => request(url, rest);
} else {
promiseService = (...args) =>
new Promise((resolve) => {
const result = service(...args);
if (typeof result === 'string') {
request(result).then((data) => {
resolve(data);
});
} else if (typeof result === 'object') {
const { url, ...rest } = result;
request(url, rest).then((data) => {
resolve(data);
});
}
});
}
return useAsync(promiseService, options);
}