React Api请求最佳实践react

您所在的位置:网站首页 react获取数据的方法 React Api请求最佳实践react

React Api请求最佳实践react

2023-11-26 00:51| 来源: 网络整理| 查看: 265

前言

在请求中,首先 axios 作为请求底层封装库,统一拦截,处理发送请求头和接收的错误响应。

那么更高一层的封装可以选择 swr 或者 react-query,目前 react-query 已经进入了第三个大版本,功能极其强大,虽然 swr 比较轻量,但是两者也只差 5kb 的打包大小,另外 swr 只有 16k start 不到,而 react-query 不断增长已有 18k star,随着时代发展,swr 已经不足以应付各种使用场景,使用 react-query 作为 axios 的顶层封装才是趋势大流。

那么你问我为什么 axios 层不选用 umi-request 或者 fetch,那只能是仁者见仁智者见智了,axios 的 82k star 是什么概念。

react-query

官方项目:tannerlinsley / react-query

官方文档:en-docs

和主流上层封装库比较(包括 swr):Comparison

swr 的问题

在 swr 中,使用非常简单,一个简单的 demo 如下:

import { useState } from 'react'; import useSWR from 'swr' function App() { const [status, setStatus] = useState(false) const request = (status, stringKey, numberKey) => { // 为了模拟实际中 api 的时长随机性,随机 1.5s 或 3s 后得到响应 const time = Math.random() > .5 ? 3000 : 1500 return new Promise((resolve, reject) => { setTimeout(() => { resolve(time) }, time) }) } const { data, error } = useSWR([status, 'ss', 2], request) return ( {!data && loading...} {error && error...} {data && {data}} // 通过改变 swr 的唯一 key,也就是 [status, 'ss', 2] 中的 status // 实现重新触发 api 请求的效果 setStatus((pre) => !pre) }}>click ); } export default App;

下面来细数一下:

loading 态不健壮

首先就是 loading 态,swr 我们只能用 data 的有无去判断是否在 loading,或者单独维护一个初始为 true 的 state 去表示 loading,在 swr 的 onSuccess 处给他置为 false。

但我们要一个初始 data 的话,使用 !data 直接不攻自破,我们要多次切换加载数据,在每次切换我都要手动将 state 置为 true,这也太麻烦了,不友好。

请求函数收参不健壮

我们在请求函数 request 中,如果 useSWR 的唯一标识是数组,那么传入请求函数参数的顺序是被解构后的!

也就是:

// 唯一标识 [status, 'ss', 2] const { data, error } = useSWR([status, 'ss', 2], request) // ↓ 这里传入是被解构后的唯一标识 const request = (status, stringKey, numberKey) => {}

好家伙,非要让人使用 rest 语法或者 arguments 才能一次性接收到全部参数,这好吗,这不好。

数据一致性场景中主动性差

特别是在筛选查询时,我们的 api 请求时长往往是不同的,当用户在 A 筛选条件下查询了数据,api 还未返回时,又进行了筛选条件 B 的查询,此时又执行筛选条件 C 的查询,那么如此往复,每次切换都面临着预期显示数据和真实显示数据不一致的问题。

因为界面上实际显示的数据只是最晚那一次 api 到达的数据,所以使用 axios 产生了数据不一致问题。

axios 确实有 cancel 的 token 方法,可以取消请求,umi-request 也有高阶的封装,但是每个方法都给我一个 cancel 方法这成本也太高了,在非昂贵成本的查询下,有没有一种忽略之前查询的方法?

答案是有的, 对一个 useSWR 来说,总是会忽略之前的请求,采用最新一次请求的结果,这是 swr 做的好的地方,可是对于昂贵请求,swr 不能真正的取消请求。

其次,什么时候取消,什么时候又再去获取,在 swr 中要主动获取,一律要通过唯一 key 去控制,唯一 key 意味着 state,增加一大堆 useState 是非常不友好的。

其实 swr 的功能实在是太弱小了,现在开始我们有更强大的选择。

使用 react-query 安装 yarn add react-query 配置全局实例

在 react 根节点渲染处如此配置 react-query 的全局实例:

import { QueryClientProvider, QueryClient } from 'react-query' import { ReactQueryDevtools } from 'react-query/devtools' // ↓ 初始化全局实例,通过该全局实例可以传入默认配置,这里本文不做详述 const queryClient = new QueryClient() ReactDOM.render( /* ↓ 主应用节点 */} {/* ↓ 可视化开发工具 */} , document.getElementById('root') );

如此一来,react-query 就配置好了,马上可以投入使用。

可视化 Devtools

在上文我们配置了一个 react-query 可视化开发工具,他可以在我们屏幕指定的位置显示 react-query 的图标,打开面板后可查看所有请求的状态和请求情况:

对于每个请求的 key、新鲜度、是否过期,以及每个请求的配置,都可以在此处查看,十分强大。该工具不会被打包到生产环境,可以放心使用。

官方介绍:Devtools

基础入门

看一个 react-query 的简单 demo:

import { useState } from 'react'; import { useQuery } from 'react-query' function App() { const [status, setStatus] = useState(false) const request = ({ queryKey }) => { // 为了模拟实际中 api 的时长随机性,随机 1.5s 或 3s 后得到响应 const time = Math.random() > .5 ? 3000 : 1500 return new Promise((resolve, reject) => { setTimeout(() => { resolve(time) }, time) }) } const { isLoading, isFetching, isError, data, refetch } = useQuery([status, 'ss', 2], request) return ( {isLoading && Loading...} {isFetching && Fetching...} {isError && Error} {data && {data}} // 同 swr,可以通过改变 key 重新获取数据 setStatus((pre) => !pre) }}>click ); } export default App;

还是和上面 swr 类似的 demo,看下这次哪里不一样:

健全的 loading 态

真正给出 loading 参数:isLoading 和 isFetching,两者的区别是,isLoading 是在 “硬” 加载时才会为 true,isFetching 是在每次请求时为 true,那么 isFetching 我们能通俗易懂的理解,就是每次请求时当做 loading 嘛。

那什么是 isLoading 的 “硬” 加载?其实 “硬” 加载就是没有缓存时的加载,那么缓存是什么?

缓存

react-query 每次请求都会自动将上次得到的响应作为 cache,你可以在任意地方主动调用查看该 cache,有了 cache 就可以大显神威,下次查看相同内容则直接使用该 cache 不必浪费时间加载,或者先使用 cache ,新数据在后台加载,加载好再显示给用户,而决定是直接使用 cache 还是后台去加载新数据,他们的过期时间是多少?这都是由你来配置决定的,Awesome!

了解了 react-query 缓存机制,我们就明白了,原来 isLoading 只会在第一次加载页面挂载组件,此时没有 cache 时为 true,之后每次再去获取新数据,也不会变为 true。

loaidng 态怎么用

所以 isLoading 的使用场景比较适合第一次加载页面,在为 true 时显示加载页,当有了第一次数据,后续一致使用 isFetching 去给组件显示 loading 态。当然,在第一次没有缓存的 “硬” 加载时,isFetching 也是为 true 的,我们没有加载页的场景下,只使用 isFetching 作为 loading 态就足够了。

健壮的请求参数

和 swr 相比,react-query 将传入的 key 一律放置到了请求函数的第一个参数对象的 queryKey 键上,也就是:

// 此时 key 标识为 [status, 'ss', 2] useQuery([status, 'ss', 2], request) // 传入到请求函数的第一个参数对象中的 queryKey 键上 // 也就是 queryKey 就为 [status, 'ss', 2] const request = ({ queryKey }) => {}

这就比较友好了,一次性拿到所以 key 值,那问题就来了,为什么还要放到 queryKey 中,因为 react-query 给请求函数第一个参数对象中还放置了一个键叫 pageParam ,用于无限分页查询,那么无线分页查询是什么?

无限查询:Infinite Queries

这个无限查询一般用于下滑到最底部加载更多的场景,因为有 “无限” 条数据,但是他必须要满足一定的使用主张,所以实际中可能不会那么好用,这里不做详述。

请求掌握能动性强

上文中提到了 swr 对每个请求的掌控性不强,在这方面,react-query 通过一个全局实例,来实现了主动对任意 key 的请求操作。

我们可以通过 useQueryClient 方法得到全局 QueryClient 实例,在该实例上有很多主动 api,可以充分管理整个 react-query 的请求。

那么上文我们提到 swr 想要主动重请求,要设立新的 state ,在 react-query 如何解决?

import { useState } from 'react'; import { useQuery, useQueryClient } from 'react-query' function App() { // 导入全局实例 const queryCliet = useQueryClient() const { isLoading, isFetching, isError, data, refetch } = useQuery([status, 'ss', 2], request) return ( {isLoading && Loading...} {isFetching && Fetching...} {isError && Error} {data && {data}} // ↓ 通过主动 api 来重新获取数据 queryCliet.refetchQueries(status) }}>click ); } export default App;

看如上代码我们发现通过引入全局实例 queryCliet 并在点击按钮时调用了 refetchQueries 方法重新加载了指定 key 的请求数据,等等,为啥我的 key 是 [status, 'ss', 2] 你却使用了 status ?

默认情况下,refetchQueries 传参会模糊匹配,并重新获取所有符合匹配的 api,这在多个请求中非常有用!

匹配规范可见:Query Filters

如果想精确匹配,可传入第二个参数 options 对象指定 exact 为 true:

queryCliet.refetchQueries([status, 'ss', 2], { exact: true })

那你告诉我要引入全局实例去重新调用 api,和 swr 的主动维护一个 state 差的了多少,可以,对于单个 react-query 查询,他自带一个叫 refetch 的返回函数,在任意地方你想重新获取数据就可以重新获取:

const { // ...... refetch } = useQuery([status, 'ss', 2], request) return ( // ...... // ↓ 直接调用 refetch 来重新获取 refetch() }}>click );

在数据一致性问题上,swr 和 react-query 都是默认忽略旧的请求,最终得到的 data 都是最新一次请求的结果,保证数据的一致性,可 swr 有昂贵请求不能取消的问题,而 react-query 在全局实例上提供了自定义 cancel 方法,Awesome!

const queryCliet = useQueryClient() return ( // ...... // 取消指定 key 的请求 queryCliet.cancelQueries(key) }}>click );

注意这个 cancelQueries 方法一定会阻断 isFetching 等 loading 态,但是不能侵入 axios ,因为 axios 的取消请求只能用 cancel token ,所以你要主动给 axios 返回的 promise 上挂载一个 cancel 方法,届时 react-query 就会去调用这个 promise 上的 cancel 真正取消请求:

import { CancelToken } from 'axios' const query = useQuery('todos', () => { // Create a new CancelToken source for this request const source = CancelToken.source() const promise = axios.get('/todos', { // Pass the source token to your request cancelToken: source.token, }) // Cancel the request if React Query calls the `promise.cancel` method promise.cancel = () => { source.cancel('Query was cancelled by React Query') } return promise })

更多真正取消请求的说明可见:Query Cancellation

对于昂贵请求,我们如此封装 axios 的 cancel token 是值得的,但是更多的情况,我们更看重他阻断 isFetching 和 isLoading 的 loading 态,并取消 data 的展示作用。

控制缓存出神入化

终于到了 react-query 最精髓的部分,在上文介绍 loading 态的 isLoading 和 isFetching 区别时,我们已经介绍了在 react-query 中缓存的概念,那如何应用缓存?

在 useQuery 单次查询中,可以配置第三个 options 选项,通过两个选项可以将缓存控制到出神入化:

useQuery([status, 'ss', 2], request, { // 不新鲜的时间 staleTime: 5000, // 缓存时间 cacheTime: 10000 })

staleTime :可以理解为数据保质期,在保质期内遇到同 key 的请求,不会去再次获取数据,也就是从缓存中取,瞬间切换展示,isFetching 也一直为 false。

cacheTime :数据在内存中的缓存时间,当数据在缓存期时,会按照 key 进行存储,下次遇到同 key 获取数据,会直接从缓存中取,瞬间展示,但是否后台请求新数据,要看 staleTime 的配置,当不配置 staleTime 时,遇到同 key 获取数据,虽然瞬间切换至缓存数据展示,但此时后台获取新数据,待获取完毕后瞬间切换为新数据。

乍一看其实配置了 cacheTime 虽然会复用缓存但当新数据请求到了会瞬间变为新数据对用户不太友好,所以需要一定的过渡动画或者 loading 态,因为此时在后台请求,所以 isFetching 为 true,用该标识去展示 loading 态即可。

所以最好的情况是 staleTime 和 cacheTime 一起使用,因为不过新鲜期,数据使用缓存,不会后台去请求导致显示突变,一旦过了新鲜期,下次请求直接就会展示 isFetching 的 loading 态。

比如设置为 10s 缓存,或者 30s 缓存,5s 的保质期,那么在 5s 内用户获取同 key 数据走缓存,过了 5s 重新请求展示 loading 态,但是由于存在缓存,所以 loading 时用户还可以看到上次的缓存结果,假如新数据到了没有变化,loading 关闭后,数据不会改变,体验是很好的,一旦请求失败,也能兜底上次的数据。

此外,还可以通过全局实例上的 getQueryCache 方法得到所有缓存。

queryCliet.getQueryCache() 一览 react-query

在上文我们介绍了一些 react-query 的内容,我们发现好像他的核心就在于全局实例的各种 api 极其强大,以及 useQuery 第三个 options 选项的强大。

比如对得到数据我可以直接在 useQuery 的 select 的选项中处理最终得到的 data :

useQuery([status, 'ss', 2], request, { // ↓ 注意这里取消聚焦重刷新数据是常用手法,在 swr 里经常使用 refetchOnWindowFocus: false, // ↓ 对响应数据自定义处理 select: (res) => { // 处理 res ... return res } })

通过全局实例可以对所有指定 key 的缓存,请求状态进行交互,可以联动多个查询,甚至可以在请求中先将预期内容填入,若出现问题再回滚,做到事务管理。

主动 api

react-query 的主动性是非常强大的,更多介绍详见:

useQuery

QueryClient

更多场景支持

仅仅主动性强,这就结束了吗,react-query 的支持还更加强大:

useQueries :同时进行并行可变数量的查询

Disabling/Pausing Queries :懒查询

Query Retries :查询重试

Paginated / Lagged Queries :分页查询

Placeholder Query Data :空白展位

Initial Query Data :支持默认数据(会直接进入缓存)

Query Invalidation:查询标记无效

Optimistic Updates :错误回滚

其他

在上文中,我们没有介绍 useMutation ,这是一个对非查询式的请求做封装的 hooks ,通过该 hooks 可以将请求纳入 react-query 管辖,从而在生命周期主动调用全局实例的 api,主动 set 数据或者操作缓存等,提前产生一些可预期的副作用,这块比较复杂,实际中一般我们不会给非查询请求加上 react-query 这么强的主张,所以可选择使用。

总结

axios 之上便是 react-query 之流,一个 api 请求其实没有必要特别复杂,但当体会到 react-query 的精髓后,会发现更多的东西 react-query 已经帮你内部实现了,和 swr 一样,简单的使用几行代码一切尽在不言中 ~



【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3