react应用的数据请求和缓存方案选型
背景
现在开发的前端项目基本流程是从服务端获取数据。其中的全局数据会在项目初始化时使用 redux 保存在内存中,比如参数管理部分;局部数据会在每次使用时发起新的请求,比如节点管理部分。
这样的流程在实践过程中存在以下问题
- 数据获取的逻辑处理比较原始,风格不统一,实现过程所需时间较长且不宜维护,比如 loading 状态等每次需要手动维护,当修改全局数据后需要手动调用对应接口进行更新等。
 - 页面每次加载都会发起新的请求,造成很多不必要的数据传输和加载等待时间。
 
因此我们需要对这一过程进行优化,以提高代码质量和节约时间,进而增加人效(格局打开 🤏)。
解决方案选择
总结一下,我们需要的是一个数据请求和缓存管理的工具。
目前比较流行的包括以下三个:
其中SWR是简化版的react-query,更轻量但是功能很少(主要提供了useSWR一个 hook,其他需要自己实现),因此暂不考虑。
另外两个功能基本一致,具体对比可参考这里,其中react-query使用更为广泛。
但是这里依然推荐RTK Query,理由如下
RTK Query是redux团队提供的基于redux的数据请求和缓存工具,目前项目已经广泛使用redux,对现有代码影响较小,且能继续使用redux生态丰富的工具,比如redux devtools。- 之前为了简化 
redux的使用安装过redux-toolkit,不需要安装另外的包。 
缺点是去年 6 月刚刚发布,issues 等资料较少,但这个类型的库本身并不复杂,文档比较全,根据目前使用情况来看影响不大。
RTK Query 的具体介绍
RTK Query 提供的功能中我们日常使用较多的比如
- 根据配置自动生成对应的请求 hook,并会返回 loading 等状态
 - 通过缓存和验证更新,避免对相同数据的重复请求
 - 通过乐观更新提高 UI 响应速度
 
基本使用
使用createApi创建每个 endpoint,每个 endpoint 对应一个网络请求,并返回对应的 hook
import { createApi, fetchBaseQuery } from "@reduxjs/toolkit/query/react";
import type { Pokemon } from "./types";
// Define a service using a base URL and expected endpoints
export const pokemonApi = createApi({
  reducerPath: "pokemonApi",
  //这里执行实际的http请求
  baseQuery: fetchBaseQuery({ baseUrl: "https://pokeapi.co/api/v2/" }),
  //这里定义所有的endpoint
  endpoints: (builder) => ({
    getPokemonByName: builder.query<Pokemon, string>({
      query: (name) => `pokemon/${name}`,
    }),
  }),
});
//这里是自动生成的对应hook
export const { useGetPokemonByNameQuery } = pokemonApi;
hook 在组件中的使用
import { getPokemonByName } from "./api";
function MaybePost({ name }: { name: string }) {
  const { data } = getPokemonByName(name);
  return <div>...</div>;
}
具体使用可参考redux 文档
进阶使用
自定义请求方法
baseQuery选项中的fetchBaseQuery封装了fetch,可以直接使用也可以利用项目中自定义过的axios。
const baseQuery = (): BaseQueryFn<AxiosRequestConfig> => async (params) => {
  try {
    //封装过的axios实例
    let result = await commonAxiosInstance(params);
    return { data: result.data };
  } catch (axiosError) {
    let err = axiosError as AxiosError;
    return {
      error: { status: err.response?.status, data: err.response?.data },
    };
  }
};
其中的错误处理除了使用 axios 中的拦截以外,还可以使用对应中间件。
export const rtkQueryErrorLogger: Middleware = (api: MiddlewareAPI) => (
  next
) => (action) => {
  // RTK Query uses `createAsyncThunk` from redux-toolkit under the hood, so we're able to utilize these matchers!
  if (isRejectedWithValue(action)) {
    console.warn("We got a rejected action!");
    const msg = action?.payload?.data?.message;
    msg && message.error(msg);
  }
  return next(action);
};
hook 种类
RTK Query将数据请求分为 query 和 mutation 两种 ,语义上前者表示查询,后者表示对服务端数据有修改的操作。
为了适应每种场景,每种请求会包含多种类型的 hook。
各类 hook 对比见这里
缓存的管理
缓存是该类工具的核心功能,根据endpoint + serialized arguments作为可复用标记。
除了直接使用缓存的方法外,还提供了无论缓存是否有效都重新请求的方法。
缓存的有效期默认 60 秒,即当使用当前缓存的组件为 0 达 60 秒后自动清除。
为了在缓存失效时自动请求,需要设置相关tags,即在mutaion中设置invalidatesTags表示当进行相关操作后对应缓存失效,在query中设置providesTags表示对应缓存失效已经自动请求以更新缓存。
import { createApi, fetchBaseQuery } from "@reduxjs/toolkit/query";
import { Post, User } from "./types";
const api = createApi({
  baseQuery: fetchBaseQuery({
    baseUrl: "/",
  }),
  tagTypes: ["Post"],
  endpoints: (build) => ({
    addPost: build.mutation<Post, Omit<Post, "id">>({
      query: (body) => ({
        url: "post",
        method: "POST",
        body,
      }),
      invalidatesTags: ["Post"],
    }),
    editPost: build.mutation<Post, Partial<Post> & Pick<Post, "id">>({
      query: (body) => ({
        url: `post/${body.id}`,
        method: "POST",
        body,
      }),
      invalidatesTags: ["Post"],
    }),
  }),
});
乐观更新
比如修改列表中的一项数据时,我们可以不重新请求列表,而直接修改缓存或者先修改缓存等列表请求后再重新赋值,提高页面响应性能,具体可参考这里。
转载自:https://juejin.cn/post/7057779073438711815