
vue3的请求封装
上述代码包含了部分其他内容,不展开,主要还是得靠自己理解。此处给对象一个remove方法,判断是否是路由拦截处调用根据情况取消请求。由于使用了ts 因此新增了部分 接口 来声明变量类型及包含的数据。上述a-demo-api.ts中代码并不全,待使用到接口时,还需要声明响应数据的结构。
axios请求封装讲的太多了,自己都有点腻了,应该是最后一次吧,今天讲vue3+ts版本关于axios的请求封装。实现功能如下:
1、可配置是否展示全局动画
2、可配置同个接口重复请求时是否取消前面未响应的请求
3、可配置路由跳转时,对未响应的接口取消请求
直接上代码吧
// api/axios.tsimport axios from 'axios'import type { AxiosInstance, AxiosRequestConfig, AxiosResponse, InternalAxiosRequestConfig} from 'axios'import { ElMessage } from 'element-plus'import { useAppStoreHook } from '@/store/modules/app'const useAppStore = useAppStoreHook()import { useUserStoreHook } from '@/store/modules/user'
interface RequestObj { [key: string]: { cancelSource: any noShowLoading: boolean // 是否展示加载遮罩 changeRouteRemove: boolean // 是否路由跳转时 取消未响应的接口请求 isCancel: boolean // 是否是取消请求 }}
interface NewAddConfig { preventDuplicateRequests?: boolean // 是否配置防止重复请求 noShowLoading?: boolean // 是否展示加载遮罩 changeRouteRemove?: boolean // 是否路由跳转时 取消未响应的接口请求}
export interface UserAxiosRequestConfig extends NewAddConfig, AxiosRequestConfig {}
interface NewAxiosRequestConfig extends NewAddConfig, InternalAxiosRequestConfig {}
interface NewAxiosResponse extends AxiosResponse { config: NewAxiosRequestConfig}
const CancelToken = axios.CancelTokenclass Axios { // axios 实例 instance: AxiosInstance
// 基础配置,url和超时时间 baseConfig: AxiosRequestConfig = { baseURL: '/proxy', timeout: 10000 }
// 请求对象 requestObj: RequestObj = {}
// 正在请求的接口数量 requestingNum = 0
// 取消请求方法, remove(url: string, isRouteChange: boolean): void { // 没有这个请求,直接返回 if (!this.requestObj[url]) return
// 路由处调用的remove方法, 且此接口切换路由时不进行 取消请求操作 直接返回 if (isRouteChange && !this.requestObj[url].changeRouteRemove) return
this.requestObj[url].cancelSource.cancel({ ...this.requestObj[url] })
if (isRouteChange) { delete this.requestObj[url] } }
constructor(config?: AxiosRequestConfig) { // 使用axios.create创建axios实例 this.instance = axios.create(Object.assign(this.baseConfig, config))
this.instance.interceptors.request.use( (config: NewAxiosRequestConfig) => { // 如果没配置不展示加载动画 则请求开始 请求数量+1 if (!config.noShowLoading) { this.requestingNum++ useAppStore.setLoadingStatus(true) }
const url = config.url // 配置 preventDuplicateRequests 为false 则不校验防止重复请求 if (url && config.preventDuplicateRequests !== false) { this.remove(url, false) this.requestObj[url] = { cancelSource: CancelToken.source(), isCancel: true, noShowLoading: !!config.noShowLoading, changeRouteRemove: !(config.changeRouteRemove === false) // 配置为false时才不清除 } config.cancelToken = this.requestObj[config.url].cancelSource.token }
// 一般会请求拦截里面加token const useUserStore = useUserStoreHook() const token = useUserStore.gettersToken() if (token) { /** * 当 NewAxiosRequestConfig 继承自 AxiosRequestConfig时 会报 headers不一定存在,于是加了下面的判断, * 后面查看axios/index.d.ts代码,发现还有一层 InternalAxiosRequestConfig 包含headers的引用类型,然后可去除下面的判断 * */ // if (!config.headers) { // config.headers = {} // } config.headers['Authorization'] = 'Bearer ' + token }
return config }, (err: any) => { return Promise.reject(err) } )
this.instance.interceptors.response.use( async (res: NewAxiosResponse) => { const { config } = res // 如果没配置不展示加载动画 则响应结束 请求数量-1 if (!config.noShowLoading) { this.requestingNum-- if (this.requestingNum <= 0) { useAppStore.setLoadingStatus(false) } }
// 响应结束 去除 请求对象 的key值 if (config.url && this.requestObj[config.url]) { delete this.requestObj[config.url] }
const data = res.data // code为200 或 返回的是文件流 直接返回 if (data.code === 200 || config.responseType === 'blob') { return data } ElMessage({ showClose: true, message: `${data.message}`, type: 'error' }) if (data.code === 401) { const useUserStore = useUserStoreHook() await useUserStore.logout() window.location.reload() } return Promise.reject(new Error(data.message || 'Error')) }, (err: any) => { // 这里用来处理http常见错误,进行全局提示 let message = ''
// 取消请求中message.noShowLoading不为true 或者 非取消请求中 config.noShowLoading 不为true if ( (err?.message?.isCancel && !err?.message?.noShowLoading) || (err.config && !err.config.noShowLoading) ) { this.requestingNum-- if (this.requestingNum <= 0) { useAppStore.setLoadingStatus(false) } }
message = err?.message?.isCancel ? '' : err.message
if (err.response && err.response.status) { switch (err.response.status) { case 400: message = '请求错误(400)' break case 401: message = '未授权,请重新登录(401)' // 这里可以做清空storage并跳转到登录页的操作 break case 403: message = '拒绝访问(403)' break case 404: message = '请求出错(404)' break case 408: message = '请求超时(408)' break case 500: message = '服务器错误(500)' break case 501: message = '服务未实现(501)' break case 502: message = '网络错误(502)' break case 503: message = '服务不可用(503)' break case 504: message = '网络超时(504)' break case 505: message = 'HTTP版本不受支持(505)' break default: message = `连接出错(${err.response.status})!` } } // 这里错误消息可以使用全局弹框展示出来 if (message) { // 比如element plus 可以使用 ElMessage ElMessage({ showClose: true, message: `${message},请检查网络或联系管理员!`, type: 'error' }) }
// 这里是AxiosError类型,所以一般我们只reject我们需要的响应即可 return Promise.reject(err) } ) }}// 对于使用非此默认配置的,可传参 再暴露出多个配置axiosexport default new Axios()
// /api/modules/a-demo-api.tsimport axios from '@/api/axios'import type { UserAxiosRequestConfig } from '@/api/axios'const xiaobuAxios = axios.instance
// get 请求demo 1export function getDemo1( data = {}, config: UserAxiosRequestConfig = { noShowLoading: true // 配置不展示加载动画 }) { return xiaobuAxios.get(`/xiaobu-admin/getDemo1`, { params: data, ...config })}
// get 请求 demo 2export function getDemo2( data = {}, config: UserAxiosRequestConfig = { preventDuplicateRequests: false // 配置不防止重复请求 }) { return xiaobuAxios(`/xiaobu-admin/getDemo2`, { method: 'get', url: 'url', params: data, ...config })}
// post 请求demo 1export function postDemo1(data = {}, config: UserAxiosRequestConfig = {}) { return xiaobuAxios.post('/xiaobu-admin/postDemo1', data, config)}
// post 请求demo 2export function postDemo2(data = {}, config: UserAxiosRequestConfig = {}) { return xiaobuAxios({ url: '/xiaobu-admin/postDemo2', method: 'post', data, ...config })}
// delete 请求demo 1 使用params作为参数 参数会拼接在请求路径上export function deleteDemo1( data = {}, config: UserAxiosRequestConfig = { noShowLoading: true // 配置不展示加载动画 }) { return xiaobuAxios.get(`/xiaobu-admin/deleteDemo1`, { params: data, ...config })}
// delete 请求 demo 2 使用params作为参数 参数会拼接在请求路径上export function deleteDemo2( data = {}, config: UserAxiosRequestConfig = { preventDuplicateRequests: false // 配置不防止重复请求 }) { return xiaobuAxios(`/xiaobu-admin/deleteDemo2`, { method: 'delete', url: 'url', params: data, ...config })}
// delete 请求demo 3 使用data作为参数 参数不展示在 请求路径上export function deleteDemo3(data = {}, config: UserAxiosRequestConfig = {}) { return xiaobuAxios.get(`/xiaobu-admin/deleteDemo3`, { data, ...config })}
// delete 请求 demo 4 使用data作为参数 参数不展示在 请求路径上export function deleteDemo4(data = {}, config: UserAxiosRequestConfig = {}) { return xiaobuAxios(`/xiaobu-admin/deleteDemo4`, { method: 'delete', url: 'url', data, ...config })}
// put 请求demo 1export function putDemo1(data = {}, config: UserAxiosRequestConfig = {}) { return xiaobuAxios.put('/xiaobu-admin/putDemo1', data, config)}
// put 请求demo 2export function putDemo2(data = {}, config: UserAxiosRequestConfig = {}) { return xiaobuAxios({ url: '/xiaobu-admin/putDemo2', method: 'put', data, ...config })}
// patch 请求demo 1export function patchDemo1(data = {}, config: UserAxiosRequestConfig = {}) { return xiaobuAxios.patch('/xiaobu-admin/patchDemo1', data, config)}
// patch 请求demo 2export function patchDemo2(data = {}, config: UserAxiosRequestConfig = {}) { return xiaobuAxios({ url: '/xiaobu-admin/patchDemo2', method: 'patch', data, ...config })}
store部分
// store/modules/app.tsimport { defineStore } from 'pinia'import { store } from '@/store'
const useAppStore = defineStore('app', { state: () => { return { loadingStatus: false // 是否展示加载动画 } },
getters: { gettersLoadingStatus: (state) => { return () => state.loadingStatus } },
actions: { setLoadingStatus(status: boolean) { this.loadingStatus = status } }})
export function useAppStoreHook() { return useAppStore(store)}
// store/index.tsimport { App } from 'vue'import { createPinia } from 'pinia'const store = createPinia()
export function setupStore(app: App<Element>) { app.use(store)}
export { store }
router路由拦截部分
import NProgress from 'nprogress' // progress barimport { useUserStoreHook } from '@/store/modules/user'const useUserStore = useUserStoreHook()import { handleAliveRoute } from './utils'import xiaobuAxios from '@/api/axios'
async function beforeEach(to, from, next) { if (to.meta?.keepAlive) { const newMatched = to.matched handleAliveRoute(newMatched, 'add') // 页面整体刷新和点击标签页刷新 if (from.name === undefined || from.name === 'Redirect') { handleAliveRoute(newMatched) } } NProgress.start() // 路由跳转时根据配置判断会否需要取消前面未响应的请求 Object.keys(xiaobuAxios.requestObj).forEach((url) => { xiaobuAxios.remove(url, true) })
const token = useUserStore.gettersToken() if (token) { if (to.name === 'Login') { next({ name: 'Redirect', params: { path: '/' } }) } else { const userInfo = useUserStore.gettersUserInfo() if (userInfo) { next() } else { await useUserStore.getUserInfo() next({ ...to, replace: true }) } } } else { if (to?.meta?.unNeedLogin) { next() } else { next(`/login?redirect=${encodeURIComponent(to.fullPath)}`) } }}
export default beforeEach
总结
上述代码包含了部分其他内容,不展开,主要还是得靠自己理解。此处给对象一个remove方法,判断是否是路由拦截处调用根据情况取消请求。由于使用了ts 因此新增了部分 接口 来声明变量类型及包含的数据。上述a-demo-api.ts中代码并不全,待使用到接口时,还需要声明响应数据的结构。
pinia中接口使用例子
// store/modules/user.tsimport { defineStore } from 'pinia'import { store } from '@/store'import { commonApi } from '@/api'import { UserInfoResult } from '@/api/mudules/resultType'import storage, { tokenKey, userKey } from '@/utils/storage'const storageData = storage.info()
const useUserStore = defineStore('user', { state: () => { return { token: storageData[tokenKey] || '', userInfo: storageData[userKey] || '' } },
getters: { gettersToken: (state) => { return () => state.token }, gettersUserInfo: (state) => { return () => state.userInfo } },
actions: { setToken(data: { token: string }) { this.token = data.token storage.set(tokenKey, data.token) }, // 退出登录 async logout() { return new Promise<UserInfoResult>((resolve, reject) => { commonApi .logout() .then((data) => { console.log('🚀 ~ file: user.ts:36 ~ .then ~ data:', data) this.userInfo = '' this.token = '' storage.clearAll() resolve(data) }) .catch((err) => { reject(err) }) }) }, // 获取用户信息 async getUserInfo() { return new Promise<UserInfoResult>((resolve, reject) => { commonApi .getUserInfo() .then((data) => { console.log('🚀 ~ file: user.ts:53 ~ .then ~ data:', data) this.userInfo = data.data.userInfo storage.set(userKey, data.data.userInfo) resolve(data) }) .catch((err) => { reject(err) }) }) } }})
export function useUserStoreHook() { return useUserStore(store)}
// api/modules/common-api.tsimport axios from '@/api/axios'import type { UserAxiosRequestConfig } from '@/api/axios'const xiaobuAxios = axios.instance
import { UserInfoResult } from '@/api/mudules/resultType'// 获取用户信息export function getUserInfo( data: object = {}, config: UserAxiosRequestConfig = { noShowLoading: true, changeRouteRemove: false }) { return xiaobuAxios.get<UserInfoResult>(`/xiaobu-admin/getUserInfo`, { params: data, ...config })}
// api/modules/resultType.tsexport type UserInfoResult = { code: number data?: { userInfo: any }}
TS用的时候是真的烦,但是是真的严谨,哪里有漏洞就给你跑不动,确保了项目的严谨及可靠,所以ts真香。
编辑器红色波浪线问题
TypeScript 引入第三方包却报错:"无法找到模块"
下面我拿react-router-dom库做例子
解决办法有两种:
方法一:安装库的TypeScript声明文件
方法二:添加自己写库的TypeScript类型声明
在方法一可行的情况下,推荐使用方法一,但是不是所有库都有 TypeScript 的声明文件,所以方法一无效才用方法二。
方法一:
根据报错提示哪个库缺TypeScript声明文件安装对应的就是了(建议用淘宝镜像快点)
npm install -D @types/库的名字
举个例子:npm install @types/react-router-dom --save-dev
方法二:自己添加特定库的ts类型声明
(1)新建文件夹typings
(2)新建文件:react-router-dom.d.ts
(3)增加库的声明
(4)然后在TS配置文件里增加typings文件引用
(5)然后路由就正常使用了
更多推荐
所有评论(0)