使用 rtk-query 优化你的数据请求
一、目前前端常见的发起ajax请求的方式
- 1、使用原生的
ajax请求 - 2、使用
jquery封装好的ajax请求 - 3、使用
fetch发起请求 - 4、第三方的比如
axios请求 - 5、
angular中自带的HttpClient
就目前前端框架开发中来说我们在开发vue、react的时候一般都是使用fetch或axios自己封装一层来与后端数据交互,至于angular肯定是用自带的HttpClient请求方式,但是依然存在几个致命的弱点,
- 1、对当前请求数据不能缓存,
- 2、一个页面上由多个组件组成,但是刚好有遇到复用相同组件的时候,那么就会发起多次
ajax请求
📢 针对同一个接口发起多次请求的解决方法,目前常见的解决方案
- 1、使用
axios的取消发起请求,参考文档 - 2、
vue中还没看到比较好的方法 - 3、在
rect中可以借用类似react-query工具对请求包装一层 - 4、对于
angular中直接使用rxjs的操作符shareReplay
二、rtk-query的介绍
rtk-query是redux-toolkit里面的一个分之,专门用来优化前端接口请求,目前也只支持在react中使用,本文章不去介绍如何在redux-toolkit的使用方式,我相信在网上也能陆续的搜索到对应的资料,但是对于rtk-query的除了官网,几乎是没有的,有也是一些残卷,简单的demo使用,并不能适用于企业实际项目开发中,本人在项目中使用redux-toolkit,axios,react-query的基础上优化实际项目中,看到官网上有rtk-query的介绍,经过一段时间的研究和实际项目中使用逐渐取代了项目中的axios和react-query
📢
rtk-query的使用环境,必须是react版本大于 17,可以使用hooks的版本,因为使用rtk-query的查询都是hooks的方式,如果你项目简单redux都未使用到,本人不建议你用rtk-query,可能直接使用axios请求更加的简单方便。
在rtk-query中我们可以使用中间件和拦截器优雅的处理异常信息,使用代码拆分将不同类型的接口拆分到不同的模块下
三、环境的搭建
-
1、使用脚手架创建一个
typescript的工程npx create-react-app react-reduxjs-toolkit --template typescript -
2、安装依赖包
npm install @reduxjs/toolkit react-redux -
3、创建
store文件夹来存放状态管理:src/store➜ store git:(dev2) ✗ tree . . ├── api # 接口请求的 │ ├── base.ts # 基础的 │ └── posts.ts # 帖子的接口 ├── hooks.ts # 自定义hooks优化在组件中使用的时候不能联想出来 ├── index.ts └── store.ts1 directory, 5 files -
4、
base.ts中提供拆分代码的基础服务,参考文档import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react';export const baseApi = createApi({baseQuery: fetchBaseQuery({ baseUrl: 'http://localhost:5000' }),reducerPath: 'baseApi',// 缓存,默认时间是秒,默认时长60秒keepUnusedDataFor: 5 * 60,refetchOnMountOrArgChange: 30 * 60,endpoints: () => ({}), }); -
5、在
posts.ts文件中是关于帖子的一切请求,如果是用户的请求,我们可以同理创建一个user.ts的文件//React entry point 会自动根据endpoints生成hooks import { baseApi } from './base'; interface IPostVo {id: number;name: string; } //使用base URL 和endpoints 定义服务 const postsApi = baseApi.injectEndpoints({endpoints: (builder) => ({// 查询列表getPostsList: builder.query<Promise<IPostVo[]>, void>({query: () => '/posts',transformResponse: (response: { data: Promise<IPostVo[]> }) => {return response.data;},}),// 根据id去查询,第一个参数是返回值的类型,第二个参是传递给后端的数据类型getPostsById: builder.query<{ id: number; name: string }, number>({query: (id: number) => `/posts/${id}`,}),// 创建帖子createPosts: builder.mutation({query: (data) => ({url: '/posts',method: 'post',body: data,}),}),// 根据id删除帖子deletePostById: builder.mutation({query: (id: number) => ({url: `/posts/${id}`,method: 'delete',}),}),// 根据id修改帖子modifyPostById: builder.mutation({query: ({ id, data }: { id: number; data: any }) => ({url: `posts/${id}`,method: 'PATCH',body: data,}),}),}),overrideExisting: false, }); //导出可在函数式组件使用的hooks,它是基于定义的endpoints自动生成的 export const {useGetPostsListQuery,useGetPostsByIdQuery,useCreatePostsMutation,useDeletePostByIdMutation,useModifyPostByIdMutation,// 惰性的查询useLazyGetPostsListQuery,useLazyGetPostsByIdQuery, } = postsApi; export default postsApi; -
6、
store.ts文件中对数据的组合import {configureStore,combineReducers,Dispatch,AnyAction, } from '@reduxjs/toolkit'; import { setupListeners } from '@reduxjs/toolkit/dist/query/react'; import { baseApi } from './api/base';const rootReducer = combineReducers({[baseApi.reducerPath]: baseApi.reducer, });// 中间件集合 const middlewareHandler = (getDefaultMiddleware: any) => {const middlewareList = [...getDefaultMiddleware()];return middlewareList; }; //API slice会包含自动生成的redux reducer和一个自定义中间件 export const rootStore = configureStore({reducer: rootReducer,middleware: (getDefaultMiddleware) =>middlewareHandler(getDefaultMiddleware), });export type RootState = ReturnType<typeof rootStore.getState>;setupListeners(rootStore.dispatch); -
7、在
src/index.ts中使用store仓库import React from 'react'; import ReactDOM from 'react-dom'; import { Provider } from 'react-redux'; import App from './App'; import reportWebVitals from './reportWebVitals'; import { rootStore } from './store';ReactDOM.render(<React.StrictMode><Provider store={rootStore}><App /></Provider></React.StrictMode>,document.getElementById('root') ); reportWebVitals(); -
8、在
app.tsx在组建中使用import { useEffect } from 'react'; import {useGetPostsListQuery,useLazyGetPostsListQuery, } from './store/api/posts.service'; import { useDispatch } from 'react-redux'; import { postsSlice } from './store/slice/post.slice'; // Test组件中依旧使用useGetPostsListQuery()方法,可以查看到两个组件中都成功获取到数据,但是发起请求只有一次 import { Test } from './Test';function App() {// 主动拉取数据const { data: postList } = useGetPostsListQuery();console.log(postList, 'app组件组件中');// 惰性拉取数据const [trigger, { data }] = useLazyGetPostsListQuery();const postsListHandler = () => {trigger();};useEffect(() => {if (data) {console.log(data, '接收到的数据');}// eslint-disable-next-line}, [data]);return (<div className='App'><header className='App-header'><button onClick={postsListHandler}>点击按钮查询全部数据</button><Test /></header></div>); }export default App; -
9、测试这样就简单实现了通过代码拆分优化请求的方式来请求后端接口,细节的问题可以继续查阅文档
四、中间件的使用
日志中间件的使用
-
1、日志中间件的使用,我们在开发环境的时候要使用日志中间件,便于观察
redux状态的变动npm install redux-logger -
2、在
src/store/store.ts中配置日志中间件import logger from 'redux-logger';... // 中间件集合 const middlewareHandler = (getDefaultMiddleware: any) => {const middlewareList = [...getDefaultMiddleware(),];if (process.env.NODE_ENV === 'development') {middlewareList.push(logger);}return middlewareList; };
错误中间件
-
1、官网地址
-
2、我们在中间件可以处理后端抛出的错误比如 403、500 等错误信息
-
3、配置错误中间件
import {MiddlewareAPI,isRejectedWithValue,Middleware, } from '@reduxjs/toolkit';// 错误中间件 export const rtkQueryErrorLogger: Middleware =(api: MiddlewareAPI) => (next: Dispatch<AnyAction>) => (action: any) => {console.log(action, '中间件中非错误的时候', api);// 只能拦截不是200的时候if (isRejectedWithValue(action)) {console.log(action, '中间件');// console.log(action.error.data.message, '错误信息');console.warn(action.payload.status, '当前的状态');console.warn(action.payload.data?.message, '错误信息');console.warn('中间件拦截了');// TODO 自己实现错误提示给页面上}return next(action);};// 中间件集合 const middlewareHandler = (getDefaultMiddleware: any) => {const middlewareList = [rtkQueryErrorLogger, ...getDefaultMiddleware()];if (process.env.NODE_ENV === 'development') {middlewareList.push(logger);}return middlewareList; };
五、拦截器的使用
上面的中间件是可以处理接口的错误请求,但是实际上常见的httpstatus并不能满足我们实际业务开发,后端开发也一般只要到了后端就返回httpstatus=200,然后自定义code的状态码来反馈错误信息,这时候拦截器就发挥他的作用了
-
1、参考文档
-
2、改造项目中的
src/store/base.ts的文件,加入拦截器的方式import { QueryReturnValue } from '@reduxjs/toolkit/dist/query/baseQueryTypes'; import {BaseQueryFn,createApi,FetchArgs,fetchBaseQuery,FetchBaseQueryError,FetchBaseQueryMeta, } from '@reduxjs/toolkit/query/react';// 定义拦截器 const baseQuery = fetchBaseQuery({baseUrl: 'http://localhost:5000/', }); const baseQueryWithIntercept: BaseQueryFn<string | FetchArgs,unknown,FetchBaseQueryError > = async (args, api, extraOptions) => {const result: QueryReturnValue<any,FetchBaseQueryError,FetchBaseQueryMeta> = await baseQuery(args, api, extraOptions);console.log(result, '拦截器');const { data, error } = result;// 如果遇到错误的时候if (error) {const { status } = error as FetchBaseQueryError;const { request } = meta as FetchBaseQueryMeta;const url: string = request.url;// 根据状态来处理错误printHttpError(Number(status), url);// TODO 自己处理错误信息提示给前端}if (Object.is(data?.code, 0)) {return result;}throw new Error(data.message); };export const baseApi = createApi({baseQuery: baseQueryWithIntercept, //fetchBaseQuery({ baseUrl: 'http://localhost:5000' }),reducerPath: 'baseApi',// 缓存时间,以秒为单位,默认是60秒keepUnusedDataFor: 2 * 60,// refetchOnMountOrArgChange: 30 * 60,endpoints: () => ({}), }); -
3、
printHttpError方法打印错httpStatus的错误信息,自己继续完善/*** 打印http请求错误的时候* @param httpStatus* @param path*/ export const printHttpError = (httpStatus: number, path: string): void => {switch (httpStatus) {case 400:console.log(`错误的请求:${path}`);break;// 401: 未登录// 未登录则跳转登录页面,并携带当前页面的路径case 401:console.log('你没有登录,请先登录');window.location.reload();break;// 跳转登录页面case 403:console.log('登录过期,请重新登录');// 清除全部的缓存数据window.localStorage.clear();window.location.reload();break;// 404请求不存在case 404:console.log('网络请求不存在');break;// 其他错误,直接抛出错误提示default:console.log('我也不知道是什么错误');break;} }; -
4、处理后端返回
httpStatus=200的时候根据code来判断异常的情况export const fetchWithIntercept: BaseQueryFn<string | FetchArgs,unknown,FetchBaseQueryError > = async (args, api, extraOptions) => {const result: QueryReturnValue<any,FetchBaseQueryError,FetchBaseQueryMeta> = await baseQuery(args, api, extraOptions);console.log(result, '拦截器');const { data, error, meta } = result;const { request } = meta as FetchBaseQueryMeta;const url: string = request.url;// 如果遇到httpStatus!=200-300错误的时候if (error) {const { status } = error as FetchBaseQueryError;// 根据状态来处理错误printHttpError(Number(status), url);}// 正确的时候,根据各自后端约定来写的if (Object.is(data?.code, 0)) {return result;} else {// TODO 打印提示信息printPanel({ method: request.method, url: request.url });// TODO 根据后端返回的错误提示到组件中,直接这里弹框提示也可以return Promise.reject('错误信息');} };
- 5、注意点,使用了拦截器后中间件就失效,具体原因在文档上还没找到说明
六、结合数据持久化插件将请求的数据持久化到本地
-
1、安装依赖包
npm install redux-persist -
2、修改
src/store/store.ts文件import { persistStore, persistReducer } from 'redux-persist'; import storage from 'redux-persist/lib/storage';const persistConfig = {key: 'root',storage, }; const rootReducer = combineReducers({[baseApi.reducerPath]: baseApi.reducer, }); const persistedReducer = persistReducer(persistConfig, rootReducer);... export const rootStore = configureStore({reducer: persistedReducer,middleware: (getDefaultMiddleware) => middlewareHandler(getDefaultMiddleware), });export const persistor = persistStore(rootStore); export type RootState = ReturnType<typeof rootStore.getState>; -
3、修改根目录下的
index.tsx文件import { rootStore, persistor } from './store';ReactDOM.render(<React.StrictMode><Provider store={rootStore}><PersistGate persistor={persistor}><App /></PersistGate></Provider></React.StrictMode>,document.getElementById('root') ); -
4、刷新浏览器查看是否在本地存储中有数据

在这里
baseApi其实没一点用途的,如果要持久化数据还需要手动来创建切片,这时候就使用到了@reduxjs/toolkit的知识点 -
5、一份完整的
store.ts文件import { configureStore, combineReducers } from '@reduxjs/toolkit'; import { persistStore, persistReducer } from 'redux-persist'; import storage from 'redux-persist/lib/storage'; import logger from 'redux-logger'; import { setupListeners } from '@reduxjs/toolkit/dist/query/react';import { baseApi } from './api/base.service'; import { postsSlice } from './slice/post.slice';const persistConfig = {key: 'root',storage, }; const rootReducer = combineReducers({[baseApi.reducerPath]: baseApi.reducer, }); const persistedReducer = persistReducer(persistConfig, rootReducer);// 中间件集合 const middlewareHandler = (getDefaultMiddleware: any) => {const middlewareList = [...getDefaultMiddleware({serializableCheck: {ignoredActions: ['persist/PERSIST'],},}),];if (process.env.NODE_ENV === 'development') {middlewareList.push(logger);}return middlewareList; }; //API slice会包含自动生成的redux reducer和一个自定义中间件 export const rootStore = configureStore({reducer: persistedReducer,middleware: (getDefaultMiddleware) => middlewareHandler(getDefaultMiddleware), });export const persistor = persistStore(rootStore); export type RootState = ReturnType<typeof rootStore.getState>;setupListeners(rootStore.dispatch);
六、使用切片的方式来实现将请求的数据存储到本地中
-
1、创建文件
store/slice/posts.ts文件import { createSlice } from '@reduxjs/toolkit'; import { IPostVo } from '../api/posts.service';interface PostsState {/**后端数据返回的 */postList: IPostVo[]; }const initialState: PostsState = {postList: [], };export const postsSlice = createSlice({name: 'Posts',initialState,reducers: {clearPosts: (state: PostsState) => {state.postList = [];},setPosts: (state: PostsState, action) => {state.postList = action.payload;},},extraReducers: {}, }); -
2、在
store.ts中配置切片import { postsSlice } from './slice/post.slice';... const rootReducer = combineReducers({[baseApi.reducerPath]: baseApi.reducer,// 自定义要存储的数据posts: postsSlice.reducer, }); -
3、在组件中将请求回来的数据存储到本地中
import { useDispatch } from 'react-redux';const [trigger, { error, data }] = useLazyGetPostsListQuery();useEffect(() => {if (data) {console.log(data, '接收到的数据');dispatch(postsSlice.actions.setPosts(data));}// eslint-disable-next-line }, [data]); -
4、获取数据后重新查看浏览器
-
5、如果是在别的组件中要使用持久化的数据直接使用
import { RootState, useSelector } from 'src/store'; const postsList: IPostVo[] =useSelector((state: RootState) => state.posts.postsList) ?? []; -
6、📢点,这里的
useSelector要使用我们自定义的,在store/hooks.ts文件中import {useSelector as useReduxSelector,TypedUseSelectorHook, } from 'react-redux'; import { RootState } from './store';export const useSelector: TypedUseSelectorHook<RootState> = useReduxSelector;
七、给请求添加请求头
-
1、在
base.ts中配置请求头const baseUrl: string = process.env.REACT_APP_BASE_API_URL as string; const baseQuery = fetchBaseQuery({baseUrl,prepareHeaders: (headers) => {headers.set('x-origin', 'admin-web');const token: string = storage.getItem(authToken);if (token) {headers.set(authToken, token);}return headers;}, });
八、参考代码
本文来自互联网用户投稿,文章观点仅代表作者本人,不代表本站立场,不承担相关法律责任。如若转载,请注明出处。 如若内容造成侵权/违法违规/事实不符,请点击【内容举报】进行投诉反馈!
