前言
-
借鉴了openapi-generator生成代码的思想
-
为什么封装自己的useRequest;
原因如下:
- 在这ts横行的年代,我们大多爬虫api文档再用json-schema-to-typescript去解析后端给的api文档来生成我们想要的模板请求方法,自己封装useRequest去配合他。实现以一个类的形式去包裹某个api类型tag的create、findOne、findMany、createMany、updatedOne、deleteOne几种情况。这样我们就可以用useRquest里面的配置去配合使用new ClassAPi
- 在登陆失效情况下我们时常要跳转登录。我们用到react-dom-router,一切都是hooks的情况下。却为什么还要用着window.location。我曾经遇到过这种问题,在stackoverflow,作者也表示尽量不要使用window.location;虽然router原理是由context和window.history弄的。但是我们不要混用,既然用就用一个;
- 配合react组件每次卸载时,取消卸载组件的后续未完成的接口请求。单单靠axios虽然可以满足重复请求取消、跳转页面的取消上一个页面的请求。但是我要做的不仅仅是页面还有每个组件
实现配置类FetchAsync
// src/api/request/index
import axios, { AxiosError, AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios'
// import jwt_decode, { JwtPayload } from 'jwt-decode'
import { transformInterceptor } from './interceptors'
import { getLockr, removeLockr, setLockr } from '@utils/localStr'
export interface ConfigurationParameters {
baseUrl: string
updateTokenUrl?: string
goLogin?: () => void
//登录接口
loginUrl: string
logoutUrl: string
errorNotify: (msg: string) => void
instance?: AxiosInstance
}
export class FetchAsync {
public baseUrl: string = ''
public updateTokenUrl?: string = ''
//请求报错提示方法
public errorNotify: (msg: string) => void
//跳转路由函数
public goLogin?: () => void
//登录路径
public loginUrl: string = ''
//登出路径
public logoutUrl: string
//设置跳转路由函数
public setGoLogin: (() => void) | undefined
public instance: AxiosInstance
constructor(configuration: ConfigurationParameters) {
this.errorNotify = configuration.errorNotify
this.baseUrl = configuration.baseUrl;
this.updateTokenUrl = configuration.updateTokenUrl
this.loginUrl = configuration.loginUrl
this.logoutUrl = configuration.logoutUrl
this.instance = axios.create({
timeout: 20000,
responseType: 'json',
})
//拦截封装
transformInterceptor(this.instance)
}
//设置跳转函数
public async setGoLoginCallback(fn: () => void) {
this.goLogin = fn;
}
}
实现封装请求拦截器
// src/api/request/interceptors.ts
import axios, { AxiosError, AxiosInstance, AxiosRequestConfig, AxiosResponse, Canceler } from 'axios'
import { getLockr, removeLockr, setLockr } from '@utils/localStr';
// 取消请求操作
// const allPendingRequestsRecord: Canceler[] = [];
const pending: {
[key in string]: Canceler
} = {};
// 取消同一个重复的ajax请求
const removePending = (key: string, isRequest: boolean = false) => {
if (pending[key] && isRequest) {
pending[key](key + ':取消重复请求');
}
delete pending[key];
};
function getContentType(headers: any) {
const keys = Object.keys(headers)
for (const k of keys) {
if (k.toLocaleLowerCase() === 'content-type') {
return headers[k]
}
}
return ''
}
/** 针对某些接口修改参数和返回值 */
export function transformInterceptor(ax: AxiosInstance) {
// 添加请求拦截器
ax.interceptors.request.use(
async (config: AxiosRequestConfig) => {
let reqData: string = '';
// 处理如url相同请求参数不同时上一个请求被屏蔽的情况
if (config.method?.toLocaleLowerCase() === 'get') {
reqData = config.url + config.method + JSON.stringify(config.params);
} else if (config.method) {
reqData = config!.url + config!.method + JSON.stringify(config.data);
}
// 如果用户连续点击某个按钮会发起多个相同的请求,可以在这里进行拦截请求并取消上一个重复的请求
removePending(reqData, true);
if (config.url!.indexOf('/auth/local') > -1) {
setLockr('jwt', '')
} else {
let token: string = await getLockr('jwt')
config.headers!.Authorization = `Bearer ${token}`;
}
config.cancelToken = new axios.CancelToken((c: any) => {
pending[reqData] = c;
});
return config
},
(err: any) => {
return Promise.reject(err)
}
)
// 添加响应拦截器
ax.interceptors.response.use(
async (res: AxiosResponse) => {
return res
},
(err: AxiosError) => {
return Promise.reject(err)
}
)
}
实现请求基类
// src/api/request/index
export class BaseAPI {
//请求登陆后是否继续请求
private isShowError: boolean = false
protected signal?: AbortSignal
private configuration: FetchAsync
private token: string
private tokenIsValid: boolean = false
constructor(protected config: { configuration: FetchAsync, signal?: AbortSignal }) {
this.configuration = config.configuration
this.signal = config.signal
this.token = getLockr('jwt')
}
// private getTokenValid() {
// this.token = getLockr('jwt')
// if (this.token !== '') {
// let decoded: JwtPayload;
// decoded = jwt_decode(this.token || '')
// let exp = decoded.exp as number
// let cur = Math.floor(Date.now() / 1000)
// let d = exp - cur
// return d
// } else {
// return -1
// }
// }
// 错误处理
private async handleApiError(error: AxiosError) {
const that = this;
let code = 500
let data: any;
// if (axios.isCancel(error)) {
// return new Promise(() => { });
// }
if (error.response) {
data = error.response.data
code = error.response.status
}
if (code === 403) {
that.configuration.errorNotify(data.error.message)
return Promise.reject(error)
}
if ([401].includes(code)) {
that.configuration.errorNotify('登录失效, 请重新登录!')
if (this.isShowError) {
return;
}
this.isShowError = true
this.tokenIsValid = false
this.token = ''
that.configuration.goLogin && that.configuration.goLogin()
setLockr('jwt', '')
return Promise.reject(error)
} else {
let msg = '未知错误'
const errorMsg = error.message || ''
if (errorMsg.includes('Network Error')) {
msg =
'请检查网络' +
(error.config && error.config.url
? error.config.url
: JSON.stringify(error.request))
} else if (errorMsg.includes('timeout') && errorMsg.includes('exceeded')) {
msg = '请求超时'
}
if (data) {
if (data.error.message === 'Invalid identifier or password') {
msg = '无效的账号密码'
} else {
msg = data.error.message
}
// @ts-ignore
}
// @ts-ignore
that.configuration.errorNotify(msg || '系统错误,请稍后重试')
return Promise.reject(error)
}
}
protected async request<T>(url: string, options: AxiosRequestConfig) {
const that = this;
let newUrl: string = '';
if (url.indexOf('http') > -1) {
newUrl = url
} else {
newUrl = this.configuration.baseUrl + url;
}
if (that.isShowError && url.indexOf(that.configuration.loginUrl) !== -1) {
return null
}
// const d = await that.getTokenValid.call(that)
//时间少于5分钟进行续签操作
// if (d < 60 * 60 * 5 && d > 0 && that.configuration.updateTokenUrl) {
// const res = await that.request('updateTokenUrl' + `?token=${that.token}`, {}) as { jwt: string }
// await setLockr('jwt', '')
// await setLockr('jwt', res.jwt)
// that.token = await getLockr('jwt')
// }
const myInstance = that.configuration.instance || axios
return myInstance.request<T>({
url: newUrl,
...options,
signal: this.signal,
responseType: options.responseType || 'json'
})
.then((res: AxiosResponse) => {
// @ts-ignore
//登入
if (url === that.configuration.loginUrl) {
that.tokenIsValid = true
setLockr('jwt', res.data.jwt)
that.token = res.data.jwt
that.isShowError = false
}
//登出
if (url === that.configuration.logoutUrl) {
removeLockr('jwt')
that.token = ''
that.tokenIsValid = false;
}
return res.data as T
})
.catch((err: AxiosError) =>
that.handleApiError(err)
throw err
})
}
}
使用
- 每个人或者团队根据API文档生成的代码风格各不一样。下面是我自动生成代码其中某个demo列子的风格
// @ts-nocheck
/**
* ------------------------------------
* !!! 不要修改,这是生成的代码 !!!
* ------------------------------------
*/
import { BaseAPI, Options } from '../request'
export default class CustomerApi extends BaseAPI {
/**
* * */
async getCustomers(
params: Strapi.QueryGetCustomers,
options: Options = {}
) {
const headers = {}
const url = `/customers`
return await this.request<Strapi.ResGetCustomers>(url, {
method: 'GET',
params,
...options,
headers: {
...headers,
...(options.headers || {}),
},
})
}
/**
* * */
async postCustomers(data: Strapi.ReqPostCustomers, options: Options = {}) {
const headers = { 'content-type': 'application/json' }
const url = '/customers'
return await this.request<Strapi.ResPostCustomers>(url, {
method: 'POST',
data,
...options,
headers: {
...headers,
...(options.headers || {}),
},
})
}
/**
* * */
async getCustomersId(
paths: Strapi.ParamGetCustomersId,
options: Options = {}
) {
const headers = {}
const url = '/customers/{id}'.replace(
'{id}',
String(paths['id'])
)
return await this.request<Strapi.ResGetCustomersId>(url, {
method: 'GET',
apiPath: '/customers/{id}',
...options,
headers: {
...headers,
...(options.headers || {}),
},
})
}
/**
* * */
async putCustomersId(
paths: Strapi.ParamPutCustomersId,
data: Strapi.ReqPutCustomersId,
options: Options = {}
) {
const headers = { 'content-type': 'application/json' }
const url = '/customers/{id}'.replace(
'{id}',
String(paths['id'])
)
return await this.request<Strapi.ResPutCustomersId>(url, {
method: 'PUT',
data,
apiPath: '/customers/{id}',
...options,
headers: {
...headers,
...(options.headers || {}),
},
})
}
/**
* * */
async deleteCustomersId(
paths: Strapi.ParamDeleteCustomersId,
options: Options = {}
) {
const headers = {}
const url = '/customers/{id}'.replace(
'{id}',
String(paths['id'])
)
return await this.request<Strapi.ResDeleteCustomersId>(url, {
method: 'DELETE',
apiPath: '/customers/{id}',
...options,
headers: {
...headers,
...(options.headers || {}),
},
})
}
}
useRequest封装
/*
* @Author: 伍东京 tokyoalex55@gmail.com
* @Date: 2022-11-01 23:09:13
* @LastEditors: 伍东京 tokyoalex55@gmail.com
* @LastEditTime: 2022-11-17 18:43:16
* @FilePath: \tuzhi-bill2\src\hooks\useRequest.ts
* @Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
*/
import { message } from 'antd';
import { useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import { ConfigurationParameters, FetchAsync } from '@api/request'
const BaseUrl = process.env.BASE_URL
const debounce = (fn: any) => {
let timer: NodeJS.Timeout | null = null;
return (msg: string) => {
if (timer === null) {
fn(msg)
timer = setTimeout(function () {
timer && clearTimeout(timer);
timer = null
}, 2000);
}
}
}
const defaultConfig: ConfigurationParameters = {
baseUrl: BaseUrl + '/api',
loginUrl: '/auth/local',
logoutUrl: '/auth',
errorNotify: debounce(message.error),
}
let configuration = new FetchAsync(defaultConfig)
export default function useRequest<T>(apiClasses: any[],config?: ConfigurationParameters): T {
const navigate = useNavigate()
const controller = new AbortController();
if (config) configuration = new FetchAsync(Object.assign({}, defaultConfig, config))
useEffect(() => {
if (!configuration.goLogin)
configuration.setGoLoginCallback(() => { navigate('/login') })
return () => {
controller.abort()
}
}, []);
const apiConfig: any = {
configuration,
}
//因为开发模式严格模式下useEffect会执行两次,会导致提前取消请求,所以在生产环境使用即可
if(process.env.NODE_EN === 'production') {
apiConfig.signal = controller.signal
}
const apis = apiClasses.map((ApiClass: any) => {
return new ApiClass(apiConfig)
}) as T
return apis
}
组件中使用
import React, { useEffect } from 'react';
import { useRequest } from 'src/hooks';
import { CustomerApi, ModelApi, OrderHeaderApi } from '@api/strapi';
type IProps = {};
const Login = (props: IProps) => {
const [orderHeaderApi, customerApi] = useRequest<
[OrderHeaderApi, CustomerApi]
>([OrderHeaderApi, ModelApi]);
useEffect(() => {
(async () => {
const response = await customerApi.getCustomersId({ id: 7 });
})();
},[]);
return <div>Login</div>;
};
export default Login;
项目源码
- 我基于create-react-app搭了个自己用的架子代码都在里面