登录
下面是对Axios的一个typeScript封装,配置上增添了是否弹出提示、请求未完成时取消请求的功能。
可以拿去修改后使用调整。
注意,使用了 antd 的提示框功能,可以自行修改。
首先,先全局添加几个会用得到的ts类型。
文件:vite-env.d.ts
type ErrMsg = string
type Result<T> = [ErrMsg | null, T | null, any?]
type PromiseResult<T> = Promise<Result<T>>
下面是正式封装
文件:@/utils/request.ts
import Axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse, Canceler, AxiosError, CancelToken, CancelTokenSource } from 'axios';
// 后面将要使用的创建请求服务的配置
const serviceConfig = {
// 默认配置们
baseURL: import.meta.env.VITE_API,
timeout: 1000 * 60 * 10, // 超时时间, 毫秒
// withCredentials: true, // 发送请求时,是否带 cookie
headers: {
'Content-Type': 'application/json; charset=utf-8',
}
}
/**
* 创建一个取消请求的source
* 设置某个请求的配置 config.cancelToken = source.token
* 再调用 source.cancel() 即可取消这个请求
* @returns {CancelTokenSource}
*/
export function createCancelSource(): CancelTokenSource {
return Axios.CancelToken.source()
}
// 扩充传参,ts类型主要此处生成,所以可以写在这个文件中,且这个类型是拓展了AxiosRequestConfig
interface MyAxiosConfig extends AxiosRequestConfig {
skipHandle?: boolean, // 是跳过处理
notTip?: boolean, // 出错是否不会自动弹出提示(skipHandle 值不能为true)
skipToken?: boolean, // 不再统一添加token
}
class AxiosServer {
private instance: AxiosInstance;
constructor(config: MyAxiosConfig) {
this.instance = Axios.create(config)
// 全局请求前拦截
// this.instance.interceptors.request.use(req => req, err => err) 暂不需要,有request方法封装了
// 全局请求后拦截
// this.instance.interceptors.response.use(res => res, err => err) 暂不需要,有request方法封装了
}
// 要返回的实例
public async request<T = any>(config: MyAxiosConfig): PromiseResult<T> {
config.headers = config.headers || {}
// 如果能拿到后台返回的数据,则此对象有值
let serviceResult: AxiosResponse | null = null;
// 拿不到后台相应的数据,则此对象有值,比如网络错误、跨域报错等等
let serviceError: AxiosError | null = null;
// 统一增加Authorization请求头, skipToken 跳过增加token
const token = '' // 此处需要得到token,比如:cookies.get('token');
if (token && !config.skipToken) {
config.headers['Authorization'] = `Bearer ${token}`
}
try {
// 去发起请求
serviceResult = await this.instance.request(config)
// 只要http请求状态是200系列,就会走这里
} catch(error: any) {
serviceResult = null
console.error(error)
if (error.response) {
// 包含 response 字段,说明只是http状态码非200系列,但后台仍返回了对应的响应数据结构
// 可能是http状态码非200系列:如果服务器返回错误的状态码,400~599,会抛出一个HTTP错误(HTTP Error)。
error = error as AxiosError
serviceResult = error.response
} else {
// * 通用错误:比如JSON解析错误等,会抛出一个通用错误(General Error)。
// * 请求被前端中断:如果请求被前端中断,会抛出一个取消错误(Cancel Error)。
// * 网络错误:如果请求失败,比如网络不可用或请求超时,会抛出一个网络错误(Error)。
serviceError = error as AxiosError
}
}
// 正常会走这里,即使返回的http状态码是500,只要后台响应了数据,就会走这里
if (serviceResult) {
// 去解析后台返回的数据结构
return this.dataHandler(serviceResult, config)
} else {
// 后台没能响应数据,比如:
// * 通用错误,比如JOSN解析错误
// * 请求被前端中断
// * 网络错误、请求超时、设备未联网、后台服务未启动等等
return this.errorHandler(serviceError, config)
}
}
// 返回结果的处理程序
private async dataHandler<T = any>(axiosResponse: AxiosResponse, config: MyAxiosConfig): PromiseResult<T> {
// 不需处理,直接返回
if (config.skipHandle) return [null, null, axiosResponse]
// 后端返回的消息体,假设数据类型见下
type ResponseBody = {
code: number,
data: any,
msg: string | null,
}
const responseBody: ResponseBody | null = axiosResponse?.data || null
let errMsg = ''
// 添加返回参数的处理
// 登录过期
// if (axiosResponse?.status === 424) {
// // 刷新token后,重新再发起请求
// // await refreshToken() // 刷新token的操作
// return this.request(config)
// }
// code标记非正常,记录错误信息弹出
// if (responseBody?.code !== 0) {
// errMsg = responseBody?.msg || '服务出错'
// }
// 需要弹框
if (errMsg && !config.notTip) {
alert(errMsg)
}
return [errMsg, responseBody as T, axiosResponse]
}
// 服务出错/取消请求的处理
private errorHandler(error: AxiosError | any, config: MyAxiosConfig): Result<null> {
// 不需处理,直接返回
if (config.skipHandle) return [null, null, error]
let errMsg: ErrMsg = '服务出错!'
let isAxiosError = error?.isAxiosError || false
if (isAxiosError) {
// 是 axios 错误,只要不是代码运行错误,比如解析json错误、执行不存在的方法等等,都走这里
error = error as AxiosError
if (error?.message?.includes('timeout')) {
// 超时
let timeout = error?.config?.timeout || 0
errMsg = `请求超时 | 当前超时时间:${timeout / 1000}S` // 请求超时
} else if (error?.message?.includes('Network Error')) {
// 网络错误或者后端接口服务未启动
errMsg = '网络错误,请检查设备联网或稍后再试!'
} else if (error?.message?.includes('canceled')) {
// 接口被代码手动中断,不应提示
errMsg = ''
} else {
errMsg = error?.message || '服务出错,请稍后再试!'
}
} else {
// 通用错误,比如解析json错误、执行不存在的方法,throw 抛出的错误等等
error = error as any
errMsg = error.message || ''
}
// 需要弹框
if (errMsg && !config.notTip) {
alert(errMsg)
}
return [errMsg, null, error]
}
}
// 创建一个 axios 服务
const service = new AxiosServer(serviceConfig)
// 导出的请求方法
export const request = service.request.bind(service);
一个项目,很可能只会初始化一个接口服务,其他接口全都引用并使用这个接口服务,下面提供一个使用示例。
下面的示例了两个接口,一个post一个get请求。
login接口支持传入otherConfig,比如请求头、是否跳过自动植入token的配置都可以从这里传入,其他接口也可以如此配置,视情况而定。
两个接口,我都定义了入参类型和出参类型,这样使用这两个接口时,可以限定入参,响应的类型也可以直接看到,如果如省事,可以直接让 T = any。
文件:@/api/some.ts
import { request } from '@/utils/request'
// 登录的请求入参
export type LoginParams = {
account: string,
password: string,
}
// 登录的响应数据
export type LoginData = {
name: string,
age: number,
parent?: string,
}
// 登录
export function login<T = LoginData>(data: LoginParams, otherConfig: Partial<MyAxiosConfig> = {}): PromiseResult<T> {
return request<T>({
url: '/login',
method: 'post',
data: data,
...otherConfig,
})
}
// 用户权限列表的请求入参
export type RoleListParams = {
userId: string,
}
// 用户权限列表的响应数据的一条
export type RoleData = {
roleId: string,
roleName: string,
updateTime: string,
}
// 获取用户权限列表
export function roleList<T = RoleData[]>(data: RoleListParams): PromiseResult<T> {
return request<T>({
url: '/roleList',
method: 'get',
params: data,
})
}
下面是一个在react的tsx中使用接口的示例,省略了部分代码。
提供了使用取消请求控制器、不自动添加请求头的演示。
import { CancelTokenSource } from 'axios'
const [loading, setloading] = useState(false)
// 取消请求的控制器
const cancelSourceLogin = useRef<CancelTokenSource | null>(null)
async function toLogin() {
if (loading) return
setloading(true)
// 添加取消请求的控制器,非必须
cancelSourceLogin.current = createCancelSource()
// 某些条件下,可以手动取消请求
// cancelSourceLogin.current?.cancel()
const [errMsg, data, res] = await login({
account: '小明',
password: '小明的密码'
}, {
// 第二个参数,传入其他的配置,比如请求头、取消请求的标记
cancelToken: cancelSourceLogin.current.token,
skipToken: true, // 不需要添加token的请求头
})
setloading(false)
cancelSourceLogin.current = null
if (!errMsg) {
message.success('登录成功')
}
}