群友问我用过 SWR 吗?一个新的请求数据思路

原文来源于:程序员成长指北;作者:oil欧哟

如有侵权,联系删除

前言

如果你是一名经验丰富的 react 开发者,那么你肯定有遇到过以下几种情况:

  • 请求库封装复杂,手动实现各种缓存验证去重逻辑,还需要维护请求加载或错误状态

  • 由于组件的重复渲染导致的 重复请求

  • 用户将网站长时间挂在后台导致缓存中的 数据过期

  • 请求方法写在很顶层的组件,将请求数据一层层传递给依赖的自组件使用,导致 组件 props 冗长

以上几种场景各自都有特殊的处理方式,例如为 axios 增加类似防抖的重复请求处理,计算用户无请求发送时间以确保数据更新,或者为了方便请求响应数据的传递引入庞大的状态管理库。

如果你认为这些方式相对比较复杂或者不够优雅,那么这篇文章带给你一个新的请求数据思路——SWR

SWR 是什么?

SWR 是 Next.js 背后的团队 vecel 开源的一个 用于数据请求的 React Hooks 库

官方介绍:“SWR” 这个名字来自于 stale-while-revalidate:一种由 HTTP RFC 5861 推广的 HTTP 缓存失效策略。这种策略首先从缓存中返回数据(过期的),同时发送 fetch 请求(重新验证),最后得到最新数据。

使用 SWR,组件将会不断地自动获得最新数据流。
UI 也会一直保持快速响应

SWR 的使用非常简单,下面是一个搭配 axios 进行请求的例子:

import axios from'axios'

constfetcher = url => axios.get(url).then(res => res.data)

functionApp () {
  const { data, error } = useSWR('/api/data', fetcher)
  // ...
}

在这个例子中我们可以看到,我们使用 useSWR 这个 hook 发起一个请求,hook 接收两个参数:

  • 第一个参数是请求的路径,同时它也作为一个 key 值用于缓存数据。

  • 第二个参数是一个异步请求方法,它参数就是 hook 接收到的第一个参数,返回值为请求到的数据

这个 hook 的返回值也有两个,data 为 fetcher 中获取到的数据,error 则为请求失败时的错误。

useSWR 既然是一个 hook ,说明 data 已经是一个状态数据了,我们不需要再手动 useState 维护请求到数据,当 data 改变时 UI 会随着改变。

我们传统的请求方式可能大部分是这样子的:

const [data, setData] = useState([]);
const [loading, setLoading] = useState(false);
const getData = axios.get('/oiloil').then((res) => res.data);

useEffect(async () => {
  setLoading(true);
  
  const res = awaitgetData().catch(err=>{
     //handle error
  });
  
  setData(res);
  setLoading(false);
}, []);

上面的例子中我们得手动去维护请求数据和加载状态,而且 useEffect 中现在还没有写依赖,如果有时请求中依赖某些状态,那么这里的请求触发时机就会变得没那么可控了。

我们使用 useSWR 模拟上面的例子简单实现对比一下:

// useData.ts

constuseData = () => {

const { data, error ,mutate} = useSWR<any[]>(
    "/oiloil",
    (url: string) => axios.get(url).then((res) => res.data)
  );
  
  return {
    data,
    error,
    isLoading: !error && !data,
    reload: mutate,
  };
}

exportdefault useData
// ComponentA.tsx
const {data, error, isLoading, reload} = useData

这里我单独抽离了一个 useData 这个自定义 hook 用于请求 /oiloil 这个接口的数据,当我们在组件中使用 hook 的时候就直接发送了请求,如果我们后面需要重复请求可以直接调用 reload 方法,而且通过 !error && !data 我们还可以获取到接口是否正在请求中这个状态。

这里虽然代码没有简短多少,但是我们的 useData hook 是可以复用的,我们可以在任何组件中直接使用它来获取数据,不需要维护新的状态,而且如果 useData 的调用时机与 ComponentA 相同,它们会使用同一个状态,不需要进行重复请求,也不需要额外定义很多的组件 props

这两种请求方式的数据流如下图所示:

图片

当然这里仅仅是 hook 带来的好处,下面我们详细讲讲 SWR 可以在我们实际开发的场景中提供什么帮助吧~

实际使用场景

数据缓存

首先就是 SWR 的核心功能 数据缓存 了。我们每一次发送请求后,后端响应的数据都会被缓存下来,当我们下一次请求相同接口时,SWR 依然会发送请求,但是它会先将上一次请求的数据直接给你,然后再去发送请求。

当新的请求结束,得到响应数据后,如果它与第一次请求的响应值不同,那么 SWR 就会直接更新 state ,这样你的 UI 也会渲染上最新的数据了。

下面是一张使用缓存前后页面渲染流程的对比图:

图片

光看这张图你可能还比较难 get 到使用缓存的好处,下面我讲一个实际的场景:

在我们常见的表格组件中,最后一列往往都是用于一些删除或者编辑操作的,如下图:

图片

当我们加载表格时,我们会发送请求以获取表格需要的数据,在请求的过程中我们可能会展示一个加载动画或者骨架屏。

如果我们在表格数据加载完成后,我们操作一下表格中的数据,例如删掉其中一条,此时在发送删除请求成功后,我们一般会重新请求一下表格的数据,那么此时 又会出现一次加载动画或者骨架屏。直到新的请求拿到后再渲染新数据。这样用户体验就没那么好。

但如果我们使用 SWR 的话,删除后不会进入加载状态,而是在重新请求表格数据后将表格渲染新的数据。对于用户来说就是我点击了删除后,那条数据直接消失了,而且还避免了表格在 有数据的情况与加载动画切换时 组件会快速闪一下的问题。

请求错误重试

接着就是 请求重试 了,大家可以尝试着搜一搜 axios 请求错误重试 这个关键字,可以在很多文章中看到大家对 aioxs 响应拦截器进行一些封装处理,实现当满足某种错误条件时进行错误重试,可以自己配置 重试次数 和 重试时延。 当然封装的方式是五花八门的。

而在 SWR 中,它本身自带了 错误重试 的功能的,当出现请求错误时,SWR 使用 指数退避算法 重发请求。该算法允许应用从错误中快速恢复,而不会浪费资源频繁地重试。错误重试的功能默认是开启的,当然你也可以手动关闭。

如果你不满足于 SWR 使用的指数退避算法,而是想要自己来控制请求的重试,那也非常简单。官方示例如下:

useSWR('/api/user', fetcher, {
  onErrorRetry: (error, key, config, revalidate, { retryCount }) => {
    // 404 时不重试。
    if (error.status === 404) return

    // 特定的 key 时不重试。
    if (key === '/api/user') return

    // 最多重试 10 次。
    if (retryCount >= 10) return

    // 5秒后重试。
    setTimeout(() =>revalidate({ retryCount: retryCount }), 5000)
  }
})

上面的例子可以看到,我们通过 useSWR 第三个参数配置一个 onErrorRetry 函数,函数的参数中包含了一些请求信息以及重试次数,这样我们需要进行自定义错误重试的时候配置起来非常方便。

除了在单个请求中配置,你也可以通过 SWR 的全局配置,为所有的请求设置相同的策略。全局配置方式如下:

<SWRConfig value={options}>
  <Component/>
</SWRConfig>

使用 SWRConfig 包裹在你的组件外层,一般我们会放在 App.tsx 中以保证包裹了所有的组件,然后在 value 中传入你的全局配置。

数据突变(mutate)

当我们调用 useSWR 这个 hook 时,它会自动为我们发送请求,例如我们刚刚进入页面时调用就会去获取渲染页面的初始数据,那如果我们知道当前页面的数据已经变更了要如何重新请求呢?

这里我们可以使用 useSWRConfig() 所返回的 mutate 函数,来广播重新验证的消息给其他的 SWR hook。使用同一个 key 调用 mutate(key) 即可。下面的官方提供的例子:

import useSWR, { useSWRConfig } from'swr'

functionApp () {
  const { mutate } = useSWRConfig()

  return (
    <div>
      <Profile />
      <button onClick={() => {
        // 将 cookie 设置为过期
        document.cookie = 'token=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;'

        // 告诉所有具有该 key 的 SWR 重新验证
        mutate('/api/user')
      }}>
        Logout
      </button>
    </div>
  )
}

mutate 的意思就是突变,我们调用 mutate 也就是在显式的告诉 swr 我的数据已经发生变化啦,赶紧给我更新一波。

你需要重新请求的 key 传入 mutate 方法即可,重新发送请求后如果数据发生了变更 swr 会为我们更新缓存并重新渲染,如果你需要特殊的处理也可以在第二个参数传入 options 选项,options 包含了以下几个配置项:

  • optimisticData:立即更新客户端缓存的数据,通常用于 optimistic UI。

  • revalidate:一旦完成异步更新,缓存是否重新请求。

  • populateCache:远程更新的结果是否写入缓存,或者是一个以新结果和当前结果作为参数并返回更新结果的函数。

  • rollbackOnError:如果远程更新出错,是否进行缓存回滚。

这里我们可以发现 mutate 方法如果只能通过 hook 的方式获取的话,我们就只能在 组件或者自定义 hook 中实现一些重新请求逻辑了,但有时我们需要在例如普通函数中触发重新请求该怎么办呢?

例如当我们 目前操作的用户权限突然被调低 了,在获取数据时后端响应了状态码 403 ,我们想要在 axios 的响应拦截中配置一个:如果遇到状态码为 403 的响应数据就重新获取一下用户的权限以重新渲染页面,将一些当前用户权限不该显示的内容隐藏,我们可以这么实现:

import axios from'axios';
import { mutate } from'swr';

axios.interceptors.response.use(
    (response) => response,
    (error) => {
      if (error.response) {
        switch (error.response.status) {
          case403: {
            mutate('/user/me');
            break;
          }
          case500: {
           // ... do something
            break;
          }
          default: {
             // ... do something
          }
        }
      }
      returnPromise.reject(error);
    }
  );

将 mutate 函数直接从 swr 中引入,而不是使用 hook 的方式获取,这种方式也可以用来实现预请求数据。

更多使用姿势可以参考文档:swr.vercel.app/zh-CN/docs/…

Typescript 支持

SWR 的 typescipt 支持非常好,毕竟自身就是用 ts 实现的。如果我们想要在使用 hook 时为请求的响应值提供类型,只需要传入一个泛型就OK,如下例:

// 🔹 B. 指定 data 类型:
// `fetcher` 一般会返回 `any`.
const { data } = useSWR<User>('/api/user', fetcher)

当然你也可以直接在 Fetcher 中传入泛型,例如大家常用的 axios,这样你在 Fetcher 中进行数据处理时也可以获得类型提示。

// 🔹 A. 使用一个有类型的 fetcher:
// `getUser` 是 `(endpoint: string) => User`.
const { data } = useSWR('/api/user', getUser)

推荐使用方式

经过一段时间的实际使用,我们在项目中将每个获取数据的请求根据 数据类型 进行分类,并以 hook 的方式进行二次封装:

import axios from'axios';
import useSWR from'swr';

import { UserResponse } from'types/User';

constuseUser = () => {
  const { data, error, isLoading, isValidating, mutate } = useSWR<UserResponse>('/user', (url) =>
    axios.get(url).then((res) => res.data.payload)
  );

  return {
    data,
    reload: mutate,
    isLoading,
    isValidating,
    isError: error,
  };
};

exportdefault useUser;

以上例子就是一个获取用户数据的一个 hook ,实际使用的过程中还会出现 hook 嵌套的情况,例如我需要获取用户的列表,再根据某个用户的 id 去获取相应的用户详情。

由于两个请求是有依赖关系的,我们需要先从 useUser 中获取用户 id 后再发送新的请求,那我们可以这么写:

import axios from'axios';
import useSWR from'swr';

import useUser from'./useUser';

constuseUserDetail = () => {
  const { data } = useUser();
  const { data, error, isLoading, isValidating, mutate } = useSWR(
    data[0].id ? `users/${data[0].id}/detail` : null,
    (url: string) => axios.get(url).then((res) => res.data.payload)
  );

  return {
    data,
    reload: mutate,
    isLoading,
    isValidating,
    isError: error,
  };
};

exportdefault useUserDetail;

useDetail 用于获取用户详情,这个 hook 中 useSWR 的 key 值是一个三目表达式,当 key 为 null 时,SWR 将不会发送请求,直到 key 有值才会发送请求,以确保请求间的依赖关系正常。

这里的 isLoading 表示目前暂无缓存,正在进行初次加载。 isValidating 则表示已经有缓存了,但是由于重新聚焦屏幕,或者手动触发数据更新数据重新验证的加载。

在实际使用时,例如表格加载的场景,初次进入表格我们可以判断 isLoading 来展示一个骨架屏:

图片

而后续的表格刷新,如果我们不想每次刷新都变为骨架屏,而是展示一个简单的加载动画提升用户的使用体验,我们就可以使用 isValidating

图片

这里额外提一点,如果你不想在表格每次加载都展示加载动画,比如只有在请求实践超过了 500ms 才响应时展示加载动画,你可以通过防抖来实现:

import { Center, Spinner } from'@chakra-ui/react';
import { useDebounce } from'ahooks';
import { memo } from'react';

constTableLoading: React.FC<{ isOpen: boolean }> = ({ isOpen }) => {
  // To prevent the loading animation from flickering frequently, it will only be displayed if the loading time exceeds 500 ms
  const debouncedLoading = useDebounce(isOpen, {
    wait: 500,
  });

  return debouncedLoading ? (
    <Center
    >
      <Spinner />
    </Center>
  ) : null;
};

exportdefaultmemo(TableLoading);

这里直接使用了 ahook 的 useDebounce hook,当 isOpen 变化后如果 500ms 后还没有变化就会展示加载动画,这样在网络流畅的情况下,用户几乎感知不到数据的加载,用户体验嘎嘎提升。

注意 hook 的执行时机,避免重复请求

这里我举个例子:假设页面中有一个表格,点击表格首个单元格可以弹出展示详情的弹窗如下图:

图片

点击详情弹出弹窗:

图片

我们可以通过如下伪代码简单实现下:

constPage = () => {
  const { data } = useSWR(
    "/api/table",
    axios.get(url).then((res) => res.data.payload)
  );
  const [modalIsOpen, setModalIsOpen] = useState(false);

  return (
    <>
      <table>{/* ...省略表格实现 */}</table>
      <Modal isOpen={modalIsOpen} />
    </>
  );
};

constModal = ({ isOpen }) => {
  const { data } = useSWR(
    "/api/table",
    axios.get(url).then((res) => res.data.payload)
  );
  // 在这里判断弹窗是否弹出
  return isOpen && <div>{/* ...省略弹窗实现 */}</div>;
};

分析一下,这里我们在页面和 Modal 组件中都使用了 SWR 请求同一个数据,当页面渲染时,Modal 组件中的 useSWR 与页面中的 useSWR 几乎同时触发,在一定时间内重复的请求会被 SWR 删除,因此只会发送一个请求。

图片

但是如果我们将控制弹窗是否显示的判断从 Modal 组件移到 Page 中,如下所示:

constPage = () => {
  const { data } = useSwr(
    "/api/table",
    axios.get(url).then((res) => res.data.payload)
  );
  const [modalIsOpen, setModalIsOpen] = useState(false);

  return (
    <>
      <table>{/* ...省略表格实现 */}</table>
      
      // 移动到在这里判断弹窗是否弹出
      {modalIsOpen && <Modal />}
    </>
  );
};

constModal = () => {
  const { data } = useSwr(
    "/api/table",
    axios.get(url).then((res) => res.data.payload)
  );
  return<div>{/* ...省略弹窗实现 */}</div>;
};

原本只需要在渲染页面的时候获取一次数据。而修改后每次打开弹窗都会随着 Modal 组件的挂载和卸载重新执行 Modal 组件内的 useSwr 方法,造成重复请求,如果你的 hook 还是嵌套使用的,那么重复请求的数量就更大了。

这里需要注意一下,在 React 官方文档中提到了 hooks-rules :

不要在循环,条件或嵌套函数中调用 Hook,  确保总是在你的 React 函数的最顶层以及任何 return 之前调用他们。

这个规则其实与上述的例子没有太大关联,React 文档中的规则是为了 避免 state 混乱,而上面的例子则是告诉大家 调用 useSWR 要尽量在同一个时机以避免重复请求 ,大家不要混淆了。

在实际项目中,由于业务逻辑复杂,不可能像上面的代码这么清晰,因此在开发和 review 的过程中要谨慎,避免踩坑。

总结

这篇文章介绍了 SWR 的的优势及使用场景,它非常适合例如 SaaS 产品或者后台管理系统这种对于数据实时性有一定要求的项目。 

在使用FFmpeg进行音视频处理时,重采样是一个常见的操作,可以使用libswresample库来进行重采样。 下面是一个示例代码,用于将一个AVFrame的数据进行重采样: ```c++ #include <iostream> #include <string> #include <cstring> #include <cstdlib> #include <cstdio> #include <cmath> extern "C" { #include <libavutil/opt.h> #include <libavutil/channel_layout.h> #include <libavutil/samplefmt.h> #include <libswresample/swresample.h> } #define MAX_AUDIO_FRAME_SIZE 192000 int main(int argc, char* argv[]) { // 输入音频参数 int in_sample_rate = 44100; int in_channels = 2; AVSampleFormat in_sample_fmt = AV_SAMPLE_FMT_FLTP; int in_channel_layout = av_get_default_channel_layout(in_channels); // 输出音频参数 int out_sample_rate = 48000; int out_channels = 2; AVSampleFormat out_sample_fmt = AV_SAMPLE_FMT_S16; int out_channel_layout = av_get_default_channel_layout(out_channels); // 初始化重采样器 SwrContext* swr_ctx = swr_alloc_set_opts(nullptr, out_channel_layout, out_sample_fmt, out_sample_rate, in_channel_layout, in_sample_fmt, in_sample_rate, 0, nullptr); if (!swr_ctx) { std::cout << "Failed to allocate SwrContext." << std::endl; return -1; } // 初始化重采样器 if (swr_init(swr_ctx) < 0) { std::cout << "Failed to initialize SwrContext." << std::endl; swr_free(&swr_ctx); return -1; } // 分配输入帧和输出帧 AVFrame* in_frame = av_frame_alloc(); if (!in_frame) { std::cout << "Failed to allocate AVFrame." << std::endl; return -1; } AVFrame* out_frame = av_frame_alloc(); if (!out_frame) { std::cout << "Failed to allocate AVFrame." << std::endl; av_frame_free(&in_frame); return -1; } // 设置输入帧参数 in_frame->format = in_sample_fmt; in_frame->channel_layout = in_channel_layout; in_frame->sample_rate = in_sample_rate; // 分配输入帧数据 if (av_frame_get_buffer(in_frame, 0) < 0) { std::cout << "Failed to allocate input frame data." << std::endl; av_frame_free(&in_frame); av_frame_free(&out_frame); return -1; } // 分配输出帧数据 int out_nb_samples = av_rescale_rnd(in_frame->nb_samples, out_sample_rate, in_sample_rate, AV_ROUND_UP); out_frame->format = out_sample_fmt; out_frame->channel_layout = out_channel_layout; out_frame->sample_rate = out_sample_rate; out_frame->nb_samples = out_nb_samples; if (av_frame_get_buffer(out_frame, 0) < 0) { std::cout << "Failed to allocate output frame data." << std::endl; av_frame_free(&in_frame); av_frame_free(&out_frame); return -1; } // 填充输入帧数据 // 这里的data和linesize要根据输入格式进行设置 float* in_data = reinterpret_cast<float*>(in_frame->data[0]); int in_linesize = in_channels * in_frame->nb_samples * sizeof(float); memset(in_data, 0, in_linesize); for (int i = 0; i < in_frame->nb_samples; ++i) { for (int j = 0; j < in_channels; ++j) { in_data[j + i * in_channels] = sin(i * 2 * M_PI * 440.0 / in_sample_rate); } } // 进行重采样 uint8_t* out_data = out_frame->data[0]; int out_linesize = out_channels * out_nb_samples * sizeof(int16_t); int out_samples = swr_convert(swr_ctx, &out_data, out_nb_samples, const_cast<const uint8_t**>(in_frame->data), in_frame->nb_samples); // 打印输出帧的数据 for (int i = 0; i < out_channels * out_samples; ++i) { std::cout << static_cast<int16_t*>(out_data)[i] << std::endl; } // 释放资源 av_frame_free(&in_frame); av_frame_free(&out_frame); swr_free(&swr_ctx); return 0; } ``` 这个示例代码中,首先初始化了重采样器,然后分配了输入帧和输出帧。接着填充了输入帧数据,这里使用了一个简单的正弦波作为输入数据。最后调用swr_convert函数进行重采样,输出的数据存储在out_data中。 需要注意的是,这里的data和linesize要根据输入格式进行设置,否则会导致重采样失败。此外,如果输入帧和输出帧的格式不同,还需要进行数据类型转换,例如从浮点型到整型。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值