初识SWR

几年前就了解到在React中通过SWR来发送网络请求,但一直没有机会实践(一直在写Vue的项目)。最近在处理用户体验相关的问题,关于网络请求这堆东西有太多可以说的了,想起了SWR,于是决定整理一下。

<!--more-->

参考

1. 概念

在常规的开发中,如果UI依赖数据,一般会在React的componentDidMountuseEffect或者Vue的onMount等钩子中请求网络接口,这种做法也被称为Fetch-on-Render。开发者除了处理响应数据,还需要

  • 处理loading、error等状态
  • 下次再次进入组件时,是否需要使用缓存数据
  • 重复渲染的组件导致重复请求
  • id等主key切换时由于网络延迟导致的数据竞态Race Condition

上面列举的问题都需要一些特殊的方案来解决,SWR提供了一种新的请求数据的方式。

SWR(Stale-While-Revalidate)原本是一种HTTP 缓存失效策略,这种策略首先从缓存中返回数据(过期的),同时发送 fetch 请求(重新验证),最后得到最新数据。在前端开发中,可以借助SWR优化数据请求和展示的体验。

SWR 的工作方式如下:

  1. Stale(陈旧):当发起一个数据请求时,SWR 首先会检查缓存中是否有可用的数据。如果有,它会返回缓存中的数据,即使这些数据可能是一段时间前获取的(即陈旧的)。这有助于快速展示数据,而不需要等待新数据的加载。
  2. Revalidate(重新验证):同时,SWR 会尝试发起一个新的网络请求,以获取最新的数据。即使它返回了陈旧数据,它仍会在后台发起请求以获取更新的数据。一旦新数据获取成功,它将会更新缓存,以备将来的请求使用。

从这两个概念可以看出,SWR主要是为了获取接口数据而设计的,其主要优势在于它提供了数据的实时更新和本地缓存,同时避免了在每次数据请求时都立即显示加载指示器。

这种策略在移动应用、单页应用(SPA)和其他需要频繁获取数据的情况下尤为有用。它可以提高用户体验,减少不必要的网络请求,同时保持数据的最新性。

由于最近写Vue比较多,接下来的代码将会以swrv这个库来实现,这个库是Vue3版本的SWR实现。

2. 使用场景

下面整理了一些实际业务中可以使用swr的场景。

2.1. 多个组件依赖相同接口

假设有一个请求用户信息的接口

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
})

2.2. 不通过全局状态就可以缓存接口数据

在常规实现中,如果想要缓存某个接口的数据(避免用户下次进来重新走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来实现数据状态管理的部分功能。

2.3. 本地缓存数据

在某些场景下需要对一些数据进行持久化存储在本地,这样用户下次进来的时候(比如刷新页面、重启电脑)可以恢复到上次离开页面时的状态

一般的做法是在接口响应之后将数据存储在localStorageIndexedDB中,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的记录

这样,在下次接口请求完成之后,用户就可以直接看到相关的数据了。

2.4. 避免数据竞态

先来造点假数据模拟数据静态

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认为是不同的全局状态,所以压根就不会存在数据静态的问题了。

3. 一些限制

SWR并不是解决网络数据请求的银弹,接下来会介绍一些关于SWR的限制。

3.1. 数据的实时性

如果业务场景强依赖数据的实时性或一致性,就不太适合走缓存。

比如某个页面,需要在路由监视器里面,通过接口校验用户是否有权限,没有权限的话重定向到其他页面,有权限就继续

async beforeRouteEnter(to, from, next) {
  const {data} = await validatePermission()
    if(data) {
    next()
  } else {
    next({name:"404"})
  }
}

这个时候,就无法使用validatePermission接口缓存的值,而需要等待接口返回使用新的值。

诸如校验服务端时间戳等此类的场景,就不太适合走缓存,也就不适合使用swr。

3.2. 重复的请求

swr的核心思想是:你可以尽快得到数据,并最终可以得到最新的数据。

The idea behind stale-while-revalidate is that you always get fresh data eventually.

因此,在某些情况下,使用swr可能会造成更多的重复请求!

比如swr内部默认开启了诸如revalidateOnFocus等配置项,当页面聚焦时,就会重新请求数据,(可以通过手动关闭来避免这个问题

但我感觉最大的问题在于开发者的使用习惯会被修改,默认将所有的重复请求合并都交给swr,就会导致在某些情况下产生更多不必要的请求。

比如某个用户信息的接口,在用户浏览页面的期间(只要不是个人中心的修改信息页面),我们可以默认这个数据不会发生变化(或者即使是数据发生了变化,也不是很重要),因此只需要在初始化应用的时候请求一次就够了。

如果使用了swr,在dedupingInterval之后,组件渲染时又会重新请求接口,导致发送了多余的请求。为了解决这些问题,就需要了解关于swr相关的配置,灵活性可能不如自己手动来控制。

4. 小结

SWR能够实现的功能是:展示旧的数据,同时发送请求更新数据,UI可以尽快获得用于展示的数据,对用户体验的提升还是比较明显的。

但是,需要为这个工具选择最适合业务的场景,而不能一股脑把所有网络请求都交给SWR,这也是使用SWR中比较考验开发者的地方,接下来打算在项目中尝试一下。