ajax
请求的方式ajax
请求jquery
封装好的ajax
请求fetch
发起请求axios
请求angular
中自带的HttpClient
就目前前端框架开发中来说我们在开发vue
、react
的时候一般都是使用fetch
或axios
自己封装一层来与后端数据交互,至于angular
肯定是用自带的HttpClient
请求方式,但是依然存在几个致命的弱点,
ajax
请求针对同一个接口发起多次请求的解决方法,目前常见的解决方案
axios
的取消发起请求,参考文档vue
中还没看到比较好的方法rect
中可以借用类似react-query工具对请求包装一层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('错误信息');
}
};
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;
},
});