vue3的请求封装
上述代码包含了部分其他内容,不展开,主要还是得靠自己理解。此处给对象一个remove方法,判断是否是路由拦截处调用根据情况取消请求。由于使用了ts 因此新增了部分 接口 来声明变量类型及包含的数据。上述a-demo-api.ts中代码并不全,待使用到接口时,还需要声明响应数据的结构。
axios请求封装讲的太多了,自己都有点腻了,应该是最后一次吧,今天讲vue3+ts版本关于axios的请求封装。实现功能如下:
1、可配置是否展示全局动画
2、可配置同个接口重复请求时是否取消前面未响应的请求
3、可配置路由跳转时,对未响应的接口取消请求
直接上代码吧
// api/axios.ts
import 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.CancelToken
class 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)
}
)
}
}
// 对于使用非此默认配置的,可传参 再暴露出多个配置axios
export default new Axios()
// /api/modules/a-demo-api.ts
import axios from '@/api/axios'
import type { UserAxiosRequestConfig } from '@/api/axios'
const xiaobuAxios = axios.instance
// get 请求demo 1
export function getDemo1(
data = {},
config: UserAxiosRequestConfig = {
noShowLoading: true // 配置不展示加载动画
}
) {
return xiaobuAxios.get(`/xiaobu-admin/getDemo1`, { params: data, ...config })
}
// get 请求 demo 2
export function getDemo2(
data = {},
config: UserAxiosRequestConfig = {
preventDuplicateRequests: false // 配置不防止重复请求
}
) {
return xiaobuAxios(`/xiaobu-admin/getDemo2`, {
method: 'get',
url: 'url',
params: data,
...config
})
}
// post 请求demo 1
export function postDemo1(data = {}, config: UserAxiosRequestConfig = {}) {
return xiaobuAxios.post('/xiaobu-admin/postDemo1', data, config)
}
// post 请求demo 2
export 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 1
export function putDemo1(data = {}, config: UserAxiosRequestConfig = {}) {
return xiaobuAxios.put('/xiaobu-admin/putDemo1', data, config)
}
// put 请求demo 2
export function putDemo2(data = {}, config: UserAxiosRequestConfig = {}) {
return xiaobuAxios({
url: '/xiaobu-admin/putDemo2',
method: 'put',
data,
...config
})
}
// patch 请求demo 1
export function patchDemo1(data = {}, config: UserAxiosRequestConfig = {}) {
return xiaobuAxios.patch('/xiaobu-admin/patchDemo1', data, config)
}
// patch 请求demo 2
export function patchDemo2(data = {}, config: UserAxiosRequestConfig = {}) {
return xiaobuAxios({
url: '/xiaobu-admin/patchDemo2',
method: 'patch',
data,
...config
})
}
store部分
// store/modules/app.ts
import { 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.ts
import { 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 bar
import { 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.ts
import { 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.ts
import 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.ts
export 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)