有关于《vue 项目axios和useRequest的二次封装整合 及 Api统一管理》
在项目实际开发应用中 与接口打交道的次数最多了 如何
优雅又高效
的调用接口就显得尤为重要了话不多说直接上干货
request.ts -- axios封装
import axios, { type AxiosRequestConfig } from "axios"
import storage from "good-storage"
import { useRequest, setGlobalOptions } from "vue-request"
// vue-router 本地封装后的入口文件
import { router } from "@/plugins/components/vue3-router"
import { message } from "ant-design-vue"
import httpsResponseCode from "./httpsResponseCode"
/** 接口响应通过格式 */
export interface HttpResponse {
code: number;
cost: number;
data: any[] | { page: number, size: number, total: number, list: [] | null } | null;
msg: string;
}
// 用于存储请求的标识,便于路由切换时取消请求
const cancelTokenStore: any = {
source: {
token: null,
cancel: null
}
};
const axiosConfig: AxiosRequestConfig = {
// 代理模式使用本方式命名接口域名
// baseURL: import.meta.env.VITE_NODE_ENV === "dev" ? undefined : `${import.meta.env.VITE_APP_API_URL}`,
baseURL: `${import.meta.env.VITE_APP_API_URL}`,
headers: {
'Content-Type': 'application/json;charset=utf-8'
},
// 是否跨站点访问控制请求
// withCredentials: true,
timeout: 5000, //超时时间,单位毫秒
timeoutErrorMessage: '超时,请稍后再尝试',
validateStatus() {
// 使用async-await,处理reject情况较为繁琐,所以全部返回resolve,在业务代码中处理异常
return true;
},
transformRequest: [
data => {
data = JSON.stringify(data);
return data;
}
],
transformResponse: [
data => {
if (typeof data === 'string' && data.startsWith('{')) {
data = JSON.parse(data);
}
return data;
}
]
}
/**
* 清空所有请求(通常在路由跳转时调用)
*/
export const clearAllPending = () => {
const CancelToken = axios.CancelToken;
cancelTokenStore.source.cancel && cancelTokenStore.source.cancel();
cancelTokenStore.source = CancelToken.source();
};
// 创建 axios 实例
const request = axios.create(axiosConfig)
// 异常拦截处理器
const errorHandler = (error: any) => {
// console.log(error)
const errMsgMap = {
"Network Error": "无法连接网络,请确认网络连接是否正常",
"Request failed with status code 500": "服务器响应异常 - 请联系管理员",
"Request failed with status code 400": "参数格式不正确 - 请联系管理员",
[`timeout of ${axiosConfig.timeout}ms exceeded`]: "请求超时 请重试",
}
const errMsg = errMsgMap[error?.response?.data?.message || error?.message] || error?.response?.data?.message || error?.message
const ret = { code: 999999, msg: errMsg, data: error?.response?.data }
if (errMsg !== "中断请求") {
message.destroy()
message.error(errMsg)
console.error("错误拦截器", "\n", ret, "\n", error)
}
return ret
}
// 定义全局参数
// window["axiosCancel"] = []
request.interceptors.request.use(async (config: AxiosRequestConfig) => {
// 去除请求地址的末尾斜杠
config.url = config.url.replaceAll(/(\/$)/g, "")
// 当前界面router链路
const matched = router.currentRoute.value.matched
// 接口访问埋点统计
const buriedPoint = {
// request_url: config.url.replace(/(\/[\d\w]+)/, "/(\\W+)"),
request_method: config.method,
request_url: config.url,
request_url_replace: config.url.replaceAll(/(\/[\w]{32})/g, "/(\\w+)").replaceAll(/(\/\d+)/g, "/(\\d+)"),
page_path: matched[matched.length - 1]?.path,
page_name: matched[matched.length - 1]?.meta?.title || matched[matched.length - 1]?.name,
page_button_name: storage.session.get("buttonName"),
matched_str: matched.filter((e: any) => !!(e?.meta?.title || e?.name)).map((e: any) => e?.meta?.title || e?.name).join("/") + `${storage.session.get("buttonName") ? "/" + storage.session.get("buttonName") : ""}`,
// matched,
}
if (config.method === "get") {
// 该模式之后 get post方式请求 统一传 { data : any } 即可
const data = config?.data
if (data) {
// 无效参数剔除
Object.keys(data).forEach(e => {
if (data[e] === undefined) delete data[e]
})
config.params = data
}
}
const method = ["post", "put", "patch", "delete"]
if (method.includes(config.method)) {
console.warn(`调用了 ${config.method} 请求`)
// config.headers['Content-Type'] = 'application/json;charset=utf-8'
// 该模式之后 get post方式请求 统一传 { data : any } 即可
const data = config?.data?.data || config?.data
if (data) {
// 无效参数剔除
Object.keys(data).forEach(e => {
if (data[e] === undefined) delete data[e]
})
config.data = data
}
}
const user = storage.get("TOKEN")
const token = user && (user.authorization || user.token || "test")
config.headers["Authorization"] = token
if (import.meta.env.VITE_HTTP_POINT == true || import.meta.env.VITE_HTTP_POINT == 'true') {
config.headers["bury-point"] = btoa(encodeURI(JSON.stringify(buriedPoint)))
}
// console.log("请求拦截器\n", { config, buriedPoint, matched })
// 本地token 续签
if (user?.exp - 300000 < new Date().getTime()) {
console.warn("监测到 token 失效, 开始重新获取");
return new Promise((resolve, reject) => {
// 让每个请求携带自定义 token 请根据实际情况自行修改
// token 失效重新拉取 (以后台给的时间-5分钟为准 <25分钟>)
axios.create({
...axiosConfig,
headers: { "Authorization": token }
}).put(`/api/token`).then(success => {
const { data: res } = success
console.log(res);
if (!res.code) {
storage.set("TOKEN", res.data)
console.log("老token", config.headers["Authorization"]);
config.headers["Authorization"] = res.data.token
console.log("新token", config.headers["Authorization"]);
resolve(config)
} else {
console.log("token获取失败 本次token以过期");
localStorage.clear()
location.reload()
}
}).catch(err => {
console.log("重新获取token接口请求失败: ", err);
localStorage.clear()
location.reload()
});
})
} else {
return config
}
}, errorHandler)
request.interceptors.response.use((response) => {
const _response = httpsResponseCode(response.data)
// console.log(`响应拦截器`, { response })
return Promise.resolve(_response)
}, errorHandler)
setGlobalOptions({
// 当 manual 设置为 true 时,你需要手动触发 run 才会发起请求
manual: false,
// 通过设置延迟的毫秒数,可以延迟 loading 变成 true 的时间,有效防止闪烁。
loadingDelay: 10,
// 当设置为 true 时,则在浏览器窗口触发 focus 和 visibilitychange 时,会重新发起请求。
refreshOnWindowFocus: true,
// 当 refreshOnWindowFocus 设置为 true 时,你可以通过设置间隔的毫秒数,来限制 refresh 的执行间隔,默认为 5000ms
refocusTimespan: 10000,
// 通过设置需要延迟的毫秒数,进入防抖模式。此时如果频繁触发 run ,则会以防抖策略进行请求。
debounceInterval: 300,
// 通过设置需要节流的毫秒数,进入节流模式。此时如果频繁触发 run ,则会以节流策略进行请求。
throttleInterval: 300,
// 当开启缓存后,你可以通过设置 cacheTime 来告诉我们缓存的过期时间。当缓存过期后,我们会将其删除。默认为 600000 毫秒,即 10 分钟
// cacheTime: 600000,
// 如果你能确保缓存下来的数据,在一定时间内不会有任何更新的,我们建议你设置一个合理的毫秒数
// staleTime: 60000,
// it will retry 5 times
errorRetryCount: 5,
// The retry interval is 5 seconds
errorRetryInterval: 1500,
})
const refRequest = {
/** 返回指定头的url链接 */
obtainUrl: (url: string, data?: { [key: string]: any }) => {
let newUrl = `${import.meta.env.VITE_APP_API_URL}/${url.replace(/^\//, "")}`
if (data) {
let urlCode = data ? "?" : ""
for (const k in data) {
const v = data[k]
if (v !== undefined) {
urlCode += `${k}=${v}&`
}
}
urlCode = urlCode.substring(0, urlCode.length - 1)
newUrl += urlCode
}
console.log(newUrl);
return encodeURI(newUrl)
},
refGet: (url: string, data?: any, options?: any) => useRequest(() => request.get(url, data ? { data } : undefined), options),
refPost: (url: string, data?: any, options?: any) => useRequest(() => request.post(url, data ? { data } : undefined), options),
refPut: (url: string, data?: any, options?: any) => useRequest(() => request.put(url, data ? { data } : undefined), options),
refPatch: (url: string, data?: any, options?: any) => useRequest(() => request.patch(url, data ? { data } : undefined), options),
refDelete: (url: string, data?: any, options?: any) => useRequest(() => request.delete(url, data ? { data } : undefined), options),
retTest: (url: string, data?: any, options?: any) => useRequest(() => {
return new Promise((resolve, reject) => {
setTimeout(() => {
return Math.random() > 0.5 ? resolve({ code: 0, data: Math.random(), msg: "success" }) : reject({ code: 500, data: Math.random(), msg: "error" })
}, 1000);
})
}, options),
}
export default request
export { request as httpsRequest, refRequest }
httpsResponseCode.ts -- 响应code码解析方法
import { message } from "ant-design-vue"
/** 退出登录 */
const loginOut = (msg: any = '授权以过期 请重新登录') => {
localStorage.clear()
setTimeout(() => window.location.reload(), 1500)
return msg
}
/** code码枚举 */
const codes = {
// 1: '接口响应异常',
/** 参数异常 */
400: (res: any) => Object.keys(res.data).map(e => res.data[e]).join(' , '),
600: () => loginOut('授权标识丢失 请重新登录'),
601: () => loginOut(),
603: '无权限访问',
10010813: '原始密码错误',
10010023: '密码过期,请重新设置密码'
}
/**
* 非吐司提示 -- 业务流程码
* @code 10010029 : 需要二次验证
* @code 100100XX : 备注内容
*/
const flowCodes: number[] = [10010029]
/**
* code码 解析方法
* @param _response
* @returns _response
*/
const httpsResponseCode = (_response: any) => {
const msg = typeof codes[_response.code] === 'function'
? codes[_response.code](_response)
: codes[_response.code] || _response.msg
if (_response.code) {
!flowCodes.includes(_response.code) && (message.destroy(), message.warn(`${import.meta.env.VITE_NODE_ENV === 'dev' ? 'code:' + _response.code + ' 一 ' : ''}${msg}`))
}
return { ..._response, msg }
}
export default httpsResponseCode
ApiDemo.ts -- 接口清单统一管理
import request from '@/utils/request'
/** 接口名注释 */
export const ApiDemos = {
/** 获取类别管理列表 */
getListPage: () => request.get(`/cms/v1/xxxx`),
/** 选择器 */
getSelector: () => request.get(`/cms/v1/xxxx`),
/** 获取数据详情 */
getDataById: (id: any) => request.get(`/cms/v1/xxxx/${id}`),
/** 添加数据 */
addData: (data: any) => request.post(`/cms/v1/xxxx`, { data }),
/** 编辑数据 */
editorData: (data: any) => request.put(`/cms/v1/xxxx/${data.id}`, { data }),
/** 删除数据 */
deleteData: (data: any) => request.delete(`/v1/xxxx/`, { data }),
}
实际使用示例
@import { ApiDemos } from '@/api/ApiDemo'
...
const demo = async () => {
// 接口实际响应参数解构 (code默认0位成功)
const { code, data } = await ApiDemos.getListPage()
if( !code ){
const { total, page, size, list } = data
// 逻辑代码
}
}
- 我正在参与掘金技术社区创作者签约计划招募活动,点击链接报名投稿。