初识SWR
几年前就了解到在React中通过SWR来发送网络请求,但一直没有机会实践(一直在写Vue的项目)。最近在处理用户体验相关的问题,关于网络请求这堆东西有太多可以说的了,想起了SWR,于是决定整理一下。
参考
概念
在常规的开发中,如果UI依赖数据,一般会在React的componentDidMount
、useEffect
或者Vue的onMount
等钩子中请求网络接口,这种做法也被称为Fetch-on-Render
。开发者除了处理响应数据,还需要
- 处理loading、error等状态
- 下次再次进入组件时,是否需要使用缓存数据
- 重复渲染的组件导致重复请求
- id等主key切换时由于网络延迟导致的数据竞态
Race Condition
上面列举的问题都需要一些特殊的方案来解决,SWR提供了一种新的请求数据的方式。
SWR(Stale-While-Revalidate
)原本是一种HTTP 缓存失效策略,这种策略首先从缓存中返回数据(过期的),同时发送 fetch 请求(重新验证),最后得到最新数据。在前端开发中,可以借助SWR优化数据请求和展示的体验。
SWR 的工作方式如下:
- Stale(陈旧):当发起一个数据请求时,SWR 首先会检查缓存中是否有可用的数据。如果有,它会返回缓存中的数据,即使这些数据可能是一段时间前获取的(即陈旧的)。这有助于快速展示数据,而不需要等待新数据的加载。
- Revalidate(重新验证):同时,SWR 会尝试发起一个新的网络请求,以获取最新的数据。即使它返回了陈旧数据,它仍会在后台发起请求以获取更新的数据。一旦新数据获取成功,它将会更新缓存,以备将来的请求使用。
从这两个概念可以看出,SWR主要是为了获取接口数据而设计的,其主要优势在于它提供了数据的实时更新和本地缓存,同时避免了在每次数据请求时都立即显示加载指示器。
这种策略在移动应用、单页应用(SPA)和其他需要频繁获取数据的情况下尤为有用。它可以提高用户体验,减少不必要的网络请求,同时保持数据的最新性。
由于最近写Vue比较多,接下来的代码将会以swrv这个库来实现,这个库是Vue3版本的SWR实现。
使用场景
下面整理了一些实际业务中可以使用swr的场景。
多个组件依赖相同接口
假设有一个请求用户信息的接口
function sleep(ms) {
return new Promise((resolve) => {
setTimeout(resolve, ms)
})
}
export async function fetchUserInfo() {
console.log('fetch use info api')
await sleep(1000)
return {
name: 'shymean' + +new Date()
}
}
现在有Comp1和Comp2两个组件都依赖这个fetchUserInfo
的接口。为了避免重复请求,常规的做法是将请求的功能交给公共父组件,然后通过props的形式将数据透传给依赖这个响应数据的子组件。
现在看看swrv
是如何实现的。首先封装统一的请求逻辑
export function useUserInfo() {
const { data, error } = useSWRV('/api/user', fetchUserInfo)
return {
data,
error
}
}
每个组件自己负责处理自己需要的数据,组件1
<template>
<div>
<div>comp1</div>
<div v-if="error">failed to load</div>
<div v-if="!data">loading...</div>
<div v-else>hello {{ data.name }}</div>
</div>
</template>
<script setup>
import{useUserInfo} from '../hooks/user'
const {data,error} = useUserInfo()
</script>
组件2
<template>
<div>
<div>comp2</div>
<div v-if="error">failed to load</div>
<div v-if="!data">loading...</div>
<div v-else>hello {{ data.name }}</div>
</div>
</template>
<script setup>
import{useUserInfo} from '../hooks/user'
const {data,error}= useUserInfo()
</script>
当同时渲染这两个组件时,可以发现接口最终只被调用了一次
<template>
<div>
<Comp1 />
<Comp2 />
</div>
</template>
配置项dedupingInterval
可以控制在多少毫秒内相同key的请求避免重复请求,默认值为2000
const { data, error } = useSWRV('/api/user', fetchUserInfo, {
dedupingInterval: 2000
})
不通过全局状态就可以缓存接口数据
在常规实现中,如果想要缓存某个接口的数据(避免用户下次进来重新走loading->展示的流程),需要借助全局状态来实现。
来看看SWR如何实现。我们修改一下代码,将Comp2
改成条件渲染的形式
<template>
<div>
<Comp1 />
<Comp2 v-if="visible" />
<button @click="toggleComp"> show comp2</button>
</div>
</template>
<script setup>
import { ref } from 'vue'
import Comp1 from './components/Comp1.vue'
import Comp2 from './components/Comp2.vue'
const visible = ref(false)
function toggleComp() {
visible.value = !visible.value
}
</script>
当点击按钮展示Comp2的时候,可以发现Comp2并没有向Comp1那样首次渲染时展示loading,而是直接展示了跟Comp1接口请求完毕后相同的数据,等待接口请求完毕,Comp1和Comp2的UI都会自动更新展示新的响应数据。
这个功能跟我们将接口请求数据放在全局状态Vuex
或者Pinia
是一样的,只是我们现在不需要再维护全局状态了
- 首次请求将相应缓存到全局状态
- 其他组件使用全局状态渲染数据,同时发送新的请求更新数据
- 请求响应回来,更新全局状态,更新UI
export const useUserStore = defineStore({
id: 'user',
state(){
return {
userInfo: null
}
},
actions:{
async fetchUserInfo(){
const data = await fetchUserInfo()
this.userInfo = data
}
}
})
使用全局状态有一个比较常见的问题是数据错误缓存,比如我们有个全局状态存放文章详情articleDetail
,在初始化的时候会根据文章id请求接口并将数据缓存到articleDetail
中。当文章id变化时,我们需要直接展示loading,重新请求接口,更新页面UI,而不是先展示之前缓存的不同id的旧数据,再更新成新数据。为了解决这个问题,往往在离开页面时,还需要将articleDetail
重置成空。
由于swr的数据缓存是根据key来的,只要将id等参数放在key中,就可以避免这个问题!接口响应更新后,之前相同key请求返回的数据也会同步更新UI。
也就是说,我们可以通过swr来实现数据状态管理的部分功能。
本地缓存数据
在某些场景下需要对一些数据进行持久化存储在本地,这样用户下次进来的时候(比如刷新页面、重启电脑)可以恢复到上次离开页面时的状态
一般的做法是在接口响应之后将数据存储在localStorage
或IndexedDB
中,swrv内置了cache
配置项,用来指定缓存策略
import useSWRV from 'swrv'
import LocalStorageCache from 'swrv/esm/cache/adapters/localStorage'
export function useUserInfo() {
const { data, error } = useSWRV('/api/user', fetchUserInfo, {
dedupingInterval: 300,
cache: new LocalStorageCache('swrv'),
})
return {
data,
error
}
}
在这个接口请求之后看,可以看见本地LocalStorage中已经有了一条key为swrv
的记录
这样,在下次接口请求完成之后,用户就可以直接看到相关的数据了。
避免数据竞态
先来造点假数据模拟数据静态
export async function fetchArticleDetail(id){
console.log('fetchArticleDetail', id)
if(id === 1){
await sleep(2000)
return {
content:'this is article 1'
}
}else {
await sleep(100)
return {
content:'this is article 2'
}
}
}
请求1的数据时,多一点延迟,当我们按照请求1->在1900ms内请求2时,如果使用了全局状态来保存响应,由于请求1的数据会后返回,就会覆盖请求2的响应,导致页面停留在了id2,但数据展示的是id1,这就发生了数据静态
通过控制swr的key,可以很轻松避免这个问题,
export function useArticleDetail(id) {
const fetcher = () => {
return fetchArticleDetail(id.value)
}
const { data, error } = useSWRV(() => '/api/article/' + id.value, fetcher)
return { data, error }
}
写一个ArticleDetail组件
<template>
<div>
<div>article</div>
<div v-if="error">failed to load</div>
<div v-if="!data">loading...</div>
<div v-else>{{ data.content }}</div>
</div>
</template>
<script setup>
import { computed } from 'vue';
import { useArticleDetail } from '../hooks/user'
const props = defineProps({
id: {
type: Number,
required: true
}
})
const id = computed(() => {
return props.id
})
const { data, error } = useArticleDetail(id)
</script>
因为不同的key认为是不同的全局状态,所以压根就不会存在数据静态的问题了。
一些限制
SWR并不是解决网络数据请求的银弹,接下来会介绍一些关于SWR的限制。
数据的实时性
如果业务场景强依赖数据的实时性或一致性,就不太适合走缓存。
比如某个页面,需要在路由监视器里面,通过接口校验用户是否有权限,没有权限的话重定向到其他页面,有权限就继续
async beforeRouteEnter(to, from, next) {
const {data} = await validatePermission()
if(data) {
next()
} else {
next({name:"404"})
}
}
这个时候,就无法使用validatePermission
接口缓存的值,而需要等待接口返回使用新的值。
诸如校验服务端时间戳等此类的场景,就不太适合走缓存,也就不适合使用swr。
重复的请求
swr的核心思想是:你可以尽快得到数据,并最终可以得到最新的数据。
The idea behind stale-while-revalidate is that you always get fresh data eventually.
因此,在某些情况下,使用swr可能会造成更多的重复请求!
比如swr内部默认开启了诸如revalidateOnFocus
等配置项,当页面聚焦时,就会重新请求数据,(可以通过手动关闭来避免这个问题
但我感觉最大的问题在于开发者的使用习惯会被修改,默认将所有的重复请求合并都交给swr,就会导致在某些情况下产生更多不必要的请求。
比如某个用户信息的接口,在用户浏览页面的期间(只要不是个人中心的修改信息页面),我们可以默认这个数据不会发生变化(或者即使是数据发生了变化,也不是很重要),因此只需要在初始化应用的时候请求一次就够了。
如果使用了swr,在dedupingInterval
之后,组件渲染时又会重新请求接口,导致发送了多余的请求。为了解决这些问题,就需要了解关于swr相关的配置,灵活性可能不如自己手动来控制。
小结
SWR能够实现的功能是:展示旧的数据,同时发送请求更新数据,UI可以尽快获得用于展示的数据,对用户体验的提升还是比较明显的。
但是,需要为这个工具选择最适合业务的场景,而不能一股脑把所有网络请求都交给SWR,这也是使用SWR中比较考验开发者的地方,接下来打算在项目中尝试一下。
你要请我喝一杯奶茶?
版权声明:自由转载-非商用-保持署名和原文链接。
本站文章均为本人原创,参考文章我都会在文中进行声明,也请您转载时附上署名。