使用 rtk-query 优化你的数据请求
一、目前前端常见的发起ajax请求的方式1、使用原生的ajax请求2、使用jquery封装好的ajax请求3、使用fetch发起请求4、第三方的比如axios请求5、angular中自带的HttpClient就目前前端框架开发中来说我们在开发vue、react的时候一般都是使用fetch或axios自己封装一层来与后端数据交互,至于angular肯定是用自带的HttpClient请求方式,但是依然
一、目前前端常见的发起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.ts 1 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; }, });
八、参考代码
更多推荐
所有评论(0)