数据请求
数据请求
数据请求库的特性
假设有个最简单的需求,MovieList 页面需要从服务端获取数据并展示。这里先抛出几个问题:
- 请求的方法写在哪里?请求来的数据放在哪里?
- 如果请求还未完成,组件已被 Unmount,有没有做特殊处理?
- 请求的参数来自哪里,参数变化会触发重新请求吗?调用写在哪里?
- 请求对应的几个状态: loading,refreshing,error 在组件上处理了么?
- 如果请求需要轮询,怎么做?有没有处理页面 visibilitychange 的情况?
- 如果需要在参数变化后重新请求,如果参数频繁更新,会出现竞态(旧的请求因为慢,晚于后发的请求 resolve)的问题吗?
这里我们以 umijs 的 useRequest 为例,讨论常用的数据请求库应该包含哪些特性。
基础网络请求
function getUsername() {
return Promise.resolve("jack");
}
export default () => {
const { data, error, loading } = useRequest(getUsername);
if (error) return <div>failed to load</div>;
if (loading) return <div>loading...</div>;
return <div>Username: {data}</div>;
};
这是一个最简单的网络请求示例。在这个例子中 useRequest 接收了一个 Promise 函数。在组件初始化时,会自动触发 getUsername 执行,并自动管理 data、loading、error 等数据,我们只需要根据状态来写相应的 UI 实现即可。
手动请求
对于“写”请求,我们一般需要手动触发,比如添加用户,编辑信息,删除用户等等。useRequest 只需要配置 manual = true,即可阻止初始化执行。只有触发 run 时才会开始执行。
export default () => {
const { run, loading } = useRequest(changeUsername, { manual: true });
return (
<Button onClick={() => run("new name")} loading={loading}>
Edit
</Button>
);
};
轮询
对于需要保持新鲜度的数据,我们通常需要不断发起网络请求以更新数据。useRequest 只要配置 poilingInterval 即可自动定时发起网络请求。
export default () => {
const { data } = useRequest(getUsername, { pollingInterval: 1000 });
return <div>Username: {data}</div>;
};
同时通过设置 pollingWhenHidden,我们可以智能的实现在屏幕隐藏时,暂停轮询。等屏幕恢复可见时,继续请求,以节省资源。当然你也可以通过 run/cancel 来手动控制定时器的开启和关闭。
并行请求
同一个接口,我们需要维护多个请求状态。示例中的并行请求有几个特点:
- 删除 n 个不同的用户,则需要维护 n 个请求状态。
- 多次删除同一个用户,则只需要维护最后一个请求。
useRequest 通过设置 fetchKey,即可对请求进行分类。相同分类的请求,只会维护一份状态。不同分类的请求,则会维护多份状态。在下面的代码中,我们通过 userId 将请求进行分类,同时我们可以通过 fetches[userId] 拿到当前分类的请求状态!
export default () => {
const { run, fetches } = useRequest(deleteUser, {
manual: true,
fetchKey: (id) => id, // 不同的 ID,分类不同
});
return (
<div>
<Button
loading={fetches.A?.loading}
onClick={() => {
run("A");
}}
>
删除 1
</Button>
<Button
loading={fetches.B?.loading}
onClick={() => {
run("B");
}}
>
删除 2
</Button>
<Button
loading={fetches.C?.loading}
onClick={() => {
run("C");
}}
>
删除 3
</Button>
</div>
);
};
防抖 & 节流
通常在边输入边搜索的场景中,我们会用到防抖功能,以节省不必要的网络请求。通过 useRequest,只需要配置一个 debounceInterval,就可以非常简单的实现对网络请求的节流操作。
export default () => {
const { data, loading, run, cancel } = useRequest(getEmail, {
debounceInterval: 500,
manual: true,
});
return (
<div>
<Select onSearch={run} loading={loading}>
{data &&
data.map((i) => (
<Option key={i} value={i}>
{i}
</Option>
))}
</Select>
</div>
);
};
缓存 & SWR & 预加载
SWR 是 stale-while-revalidate 的简称,最主要的能力是:我们在发起网络请求时,会优先返回之前缓存的数据,然后在背后发起新的网络请求,最终用新的请求结果重新触发组件渲染。swr 特性在特定场景,对用户非常友好。在 SWR 场景下,我们会对接口数据进行缓存,当下次请求该接口时,我们会先返回缓存的数据,同时,在背后发起新的网络请求,待新数据拿到后,重新触发渲染。
对于一些数据不是经常变化的接口,使用 SWR 后,可以极大提高用户使用体验。比如下面的图片例子,当我们第二次访问该文章时,直接返回了缓存的数据,没有任何的等待时间。同时,我们可以看到“最新访问时间”在 2 秒后更新了,这意味着新的请求数据返回了。
const { data, loading } = useRequest(getArticle, {
cacheKey: "articleKey",
});
同时需要注意,同一个 cacheyKey 的数据是全局共享的。通过这个特性,我们可以实现“预加载”功能。比如鼠标 hover 到文章标题时,我们即发送读取文章详情的请求,这样等用户真正点进文章时,数据早已经缓存好了。
屏幕聚焦重新请求
通过配置 refreshOnWindowFocus,我们可以实现,在屏幕重新聚焦或可见时,重新发起网络请求。这个特性有什么用呢?它可以保证多个 tab 间数据的同步性。也可以解决长间隔之后重新打开网站的数据新鲜度问题。
集成请求库
考虑到使用便捷性,useRequest 集成了 umi-request。如果第一个参数不是 Promise,我们会通过 umi-request 来发起网络请求。当然如果你想用 axios,也是可以的,通过 requstMethod 即可定制你自己的请求方法。
// 用法 1
const { data, error, loading } = useRequest("/api/userInfo");
// 用法 2
const { data, error, loading } = useRequest({
url: "/api/changeUsername",
method: "post",
});
// 用法 3
const { data, error, loading, run } = useRequest(
(userId) => `/api/userInfo/${userId}`
);
// 用法 4
const { loading, run } = useRequest((username) => ({
url: "/api/changeUsername",
method: "post",
data: { username },
}));
分页
中台应用中最多的就是表格和表单了。对于一个表格,我们要处理非常多的请求逻辑,包括不限于:
- page、pageSize、total 管理
- 筛选条件变化,重置分页,重新发起网络请求
useRequest 通过配置 paginated = true,即可进入分页模式,自动帮你处理表格常见逻辑,同时我们对 antd Table 做了特殊支持,只用简单几行代码,就可以实现下面图中这样复杂的逻辑,提效百倍。
export default () => {
const [gender, setGender] = useState("male");
const { tableProps } = useRequest(
(params) => {
return getTableData({ ...params, gender });
},
{
paginated: true,
refreshDeps: [gender],
}
);
const columns = [];
return <Table columns={columns} rowKey="email" {...tableProps} />;
};
加载更多
加载更多的场景也是日常开发中常见的需求。在加载场景中,我们一般需要处理:
- 分页 offset、pageSize 等管理
- 首次加载,加载更多状态管理
- 上拉自动加载更多
- 组件第二次加载时,希望能记录之前的数据,并滚动到之前的位置
useRequest 通过设置 loadMore = true,即可进入加载更多模式,配合其它参数,可以帮你处理上面所有的逻辑。
const { data, loading, loadMore, loadingMore } = useRequest(
(d) => getLoadMoreList(d?.nextId, 3),
{
loadMore: true,
cacheKey: "loadMoreDemoCacheId",
fetchKey: (d) => `${d?.nextId}-`,
}
);