# Axios请求封装
Vue3+TS使用Axios对请求进行统一拦截,通过TS对传入格式和返回格式进行校验。
# 一、Axios请求拦截处理
// request.js
import axios from 'axios'
import storage from 'store'
import type { AxiosInstance, AxiosRequestConfig, AxiosResponse, AxiosError, InternalAxiosRequestConfig } from 'axios'
import { ACCESS_TOKEN } from '@/constants/app'
import { Notification } from '@arco-design/web-vue'
let hasInvalid = false // 请求已失效,禁止重复提示
// 创建 axios 实例
const request: AxiosInstance = axios.create({
// API 请求的默认前缀
baseURL: import.meta.env.VITE_APP_API_BASE_URL,
timeout: 1000 * 60 * 10 // 请求超时时间
})
// 异常拦截处理器
const errorHandler = (error: AxiosError) => {
if (error.response) {
const data: any = error.response.data
if (error.response.status === 404) {
Notification.warning({
title: '不存在该页面',
content: data.msg
})
return new Promise(() => {})
}
if (error.response.status === 401) {
if (hasInvalid) return new Promise(() => {})
hasInvalid = true
Notification.warning({
title: '授权验证失败',
content: '请重新登录'
})
storage.clearAll()
setTimeout(() => {
hasInvalid = false
window.location.reload()
}, 1000)
return new Promise(() => {})
}
if (error.response.status === 415) {
if (hasInvalid) return new Promise(() => {})
hasInvalid = true
Notification.warning({
title: '系统授权过期',
content: '请重新登录'
})
storage.clearAll()
setTimeout(() => {
hasInvalid = false
window.location.href = '/'
}, 1000)
return new Promise(() => {})
}
}
return Promise.reject(error)
}
// request interceptor
request.interceptors.request.use((config: InternalAxiosRequestConfig) => {
const token = storage.get(ACCESS_TOKEN)
// 如果 token 存在
// 让每个请求携带自定义 token 请根据实际情况自行修改
if (token) {
config.headers['Authorization'] = token
}
return config
}, errorHandler)
// response interceptor
request.interceptors.response.use((response: AxiosResponse) => {
return response.data
}, errorHandler)
// 通过一个方法传入泛型,自定义接口返回格式
function createRequest<T = ApiResponseData<any>>(config: AxiosRequestConfig): Promise<T> {
return request(config)
}
export default createRequest
# 二、接口API定义
对于项目中不同模块的接口,可以专门创建对应的接口处理文件来进行分类处理,这样在调用时只需关注调用哪个接口和传什么参数就行了。在项目中统一在api文件下进行处理接口。
# Api目录
├── src
│ ├── api # Api ajax请求处理
│ │ ├── public # 公共接口
│ │ │ ├── index.ts # api接口定义
│ │ │ └── types.ts # api类型校验
│ │ ├── empower # 授权接口接口
│ │ │ ├── index.ts # api接口定义
│ │ │ └── types.ts # api类型校验
# 定义统一返回格式
通过全局声明文件定义一个统一的接口返回格式,并通过泛型支持每个接口自定义返回格式。
// api.d.ts
/**
* @desc 接口请求数据通用返回格式
* */
interface ApiResponseData<T> {
code: number
data: T
message: string
[key: string]: any
}
# Api定义
// empower.ts
/**
* @desc 用户授权接口
* 接口对传入参数和返回结果进行类型检查
* */
import request from '@/utils/request'
import type * as Login from './types'
// 接口地址
const api = {
login: '/api/login', // 登录
logout: '/auth/logout', // 退出登录
info: '/api/owner/info', // 获取用户信息
captcha: '/api/captcha', // 获取验证码
updatePwd: '/api/user/updatePwd' // 修改密码
}
// 登录
export function loginApi(data: Login.LoginReq) {
return request<Login.LoginRes>({
url: api.login,
method: 'post',
data
})
}
// 获取用户信息
export function captchaApi() {
return request<Login.CaptchaRes>({
url: api.captcha,
method: 'get'
// params
})
}
// 退出登录
export function logoutApi() {
return request({
url: api.logout,
method: 'post'
})
}
// 修改密码
export function updatePwdApi(data: Login.UpdatePwdReq) {
return request({
url: api.updatePwd,
method: 'post',
data
})
}
empower接口类型声明
// types.ts
// 登录
export interface LoginReq {
name: string
password: string
}
export type LoginRes = ApiResponseData<UserInfo>
// 用户信息
export type UserInfoRes = ApiResponseData<UserInfo>
// 修改密码
export interface UpdatePwdReq {
old_pwd: string
new_pwd: string
}
// 验证码
interface Captcha {
img: string
key: string
}
export type CaptchaRes = ApiResponseData<Captcha>
# 请求配置
除了基础的接口请求字段配置外,有些接口还需要额外的配置,如FormData格式传输,获取二进制文件,接口上传进度等,需要对config配置字段进行类型校验。
/**
* @description 全局公共 Api
* @author changz
* */
import request from '@/utils/request'
import type * as Public from './types'
// 接口地址
const api = {
uploadFile: '/api/approval-flow/uploadFile', // 上传文件
download: '/api/task/download' // 下载文件
}
// 上传文件
export function uploadFileApi(data: Public.UploadFormData, config: Public.ConfigData) {
return request({
url: api.uploadFile,
method: 'post',
data,
onUploadProgress: config.onUploadProgress
})
}
// 下载文件
export function downloadApi(params: Public.DownloadReq) {
return request({
url: api.download,
method: 'get',
params,
responseType: 'blob'
})
}
config配置和FormData格式
import FormData from 'form-data'
// 上传文件
// 定义FormData格式上传类型
export interface UploadFormData extends FormData {
append<T extends string | Blob>(
name: string,
value: T
): void
}
// 接口配置,类似于进度条headers等
export interface ConfigData {
onUploadProgress?: ((progressEvent: any) => void)
// ...其他配置
}
// export type UploadProgress = ((progressEvent: any) => void)
// 下载文件
export interface DownloadReq {
id: number
}
export type DownloadRes = ApiResponseData<any>
# 三、使用
使用时要传入定义好的参数和返回值,不能随便传入和返回。
<template>
<div class="empower">
<div class="empower-wrap">
<div class="wrap-form">
<a-form ref="empower" :model="formData" :rules="formRule" :label-col-props="{span: 4}" :wrapper-col-props="{span: 18}">
<a-form-item field="name" label="用户名" :validate-trigger="['blur']">
<a-input v-model="formData.name" size="large" placeholder="请输入用户名" allow-clear>
<template #prefix><icon-user /></template>
</a-input>
</a-form-item>
<a-form-item field="password" label="密码" :validate-trigger="['blur']">
<a-input-password v-model="formData.password" size="large" placeholder="请输入密码" allow-clear>
<template #prefix><icon-lock /></template>
</a-input-password>
</a-form-item>
<!-- <a-form-item field="code" label="验证码" :validate-trigger="['blur']">
<a-input v-model="formData.code" size="large" placeholder="请输入验证码" allow-clear @press-enter="submitForm">
<template #prefix><icon-safe /></template>
</a-input>
<div class="form-img" @click="getVerifyCode">
<img v-if="!codeLoad" :src="formData.verifyImg" alt="">
<icon-loading v-else />
</div>
</a-form-item> -->
<a-form-item :wrapper-col-props="{offset: 4}">
<a-button type="primary" :loading="submitLoad" @click="submitForm">提交</a-button>
</a-form-item>
</a-form>
</div>
</div>
</div>
</template>
<script setup lang="ts">
/**
* @desc 登录
* @author changz
* */
import { ref, reactive, getCurrentInstance, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { useEmpowerStore } from '@/stores/modules/empower'
import type { FormInstance } from '@arco-design/web-vue/es/form'
import { captchaApi, loginApi } from '@/api/empower'
const instance = getCurrentInstance()
const router = useRouter()
const empowerStore = useEmpowerStore()
const empower = ref<FormInstance>()
const submitLoad = ref(false)
// const codeLoad = ref(false)
const formData = reactive({
name: 'admin',
password: 'admin',
code: '',
verifyImg: '',
key: ''
})
const formRule = reactive({
name: [{ required: true, message: '请输入用户名' }],
password: [{ required: true, message: '请输入密码' }],
code: [{ required: true, message: '请输入验证码' }]
})
onMounted(() => {
// getVerifyCode()
})
// 获取验证码
// const getVerifyCode = () => {
// codeLoad.value = true
// captchaApi().then(res => {
// codeLoad.value = false
// if (res.code !== 200) {
// instance?.proxy?.$notification.warning({
// title: '提示',
// content: res.msg
// })
// return
// }
// const { img, key } = res.data
// formData.verifyImg = img
// formData.key = key
// formData.code = ''
// }).catch(err => {
// codeLoad.value = false
// instance?.proxy?.$notification.warning({
// title: '提示',
// content: err.message
// })
// })
// }
const submitForm = () => {
empower.value?.validate(errors => {
if (!errors) {
// const { name, password, code, key } = formData
const { name, password } = formData
const params = {
name,
password
// code,
// key
}
submitLoad.value = true
loginApi(params)
.then((res) => {
submitLoad.value = false
if (res.code !== 200) {
instance?.proxy?.$notification.error({
title: '错误',
content: res.message
})
return
}
router.push({ path: '/' })
// 延迟 1 秒显示欢迎信息
setTimeout(() => {
instance?.proxy?.$notification.success({
title: '欢迎',
content: '欢迎回来'
})
}, 1000)
})
.catch(err => {
submitLoad.value = false
instance?.proxy?.$notification.error({
title: '错误',
content: err.message
})
})
} else {
instance?.proxy?.$message.warning('表单填写不完整!')
}
})
}
</script>
<style lang="less" scoped>
.empower {
width: 100%;
height: 100%;
background: url('../../assets/images/login-bg.jpg') center center no-repeat;
background-size: 100% 100%;
.empower-wrap {
.position_center();
width: 500px;
.wrap-form {
width: 100%;
padding: 50px 30px 20px 30px;
background-color: #fff;
border-radius: 5px;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
.form-img {
width: 140px;
height: 34px;
img {
width: 100%;
height: 100%;
}
}
}
}
}
</style>
上传文件接口:
<template>
<h1>{{ title }}</h1>
<a-upload :show-file-list="false" @before-upload="beforeUpload" :custom-request="customRequest">
<template #upload-button>
<a-button type="primary" :loading="uploadLoad">
<template #icon>
<icon-upload />
</template>
<template #default>上传图片</template>
</a-button>
</template>
</a-upload>
</template>
<script setup lang="ts" name="UserCenter">
import { ref, getCurrentInstance } from 'vue'
import FormData from 'form-data'
import type { RequestOption } from '@arco-design/web-vue/es/upload/interfaces'
import { uploadFileApi } from '@/api/public'
const instance = getCurrentInstance()
const title = ref<string>('个人信息')
const uploadLoad = ref(false)
const percent = ref(0)
const beforeUpload = (file: File) => {
const { name, size } = file
const fileExtension = name.split('.').pop()
const limitType = fileExtension === 'jpg' || fileExtension === 'jpeg' || fileExtension === 'png' || fileExtension === 'gif'
if (!limitType) {
instance?.proxy?.$message.error('请上传 JPG、PNG、JPEG 或 GIF 格式图片!')
}
const limitSize = size / 1024 / 1024 < 8
if (!limitSize) {
instance?.proxy?.$message.error('文件不可大于 8MB!')
}
return limitType && limitSize
}
// 获取图片
const customRequest = (option: RequestOption): any => {
const { fileItem } = option
const { file } = fileItem
const params = new FormData()
params.append('file', file)
uploadLoad.value = true
// const controller = new AbortController()
uploadFileApi(params, {
onUploadProgress: (e: ProgressEvent) => {
const completeProgress = (((e.loaded / e.total) * 100) / 100) | 0
percent.value = completeProgress
}
})
.then(res => {
uploadLoad.value = false
if (res.code !== 200) {
instance?.proxy?.$notification.warning({
title: '提示',
content: res.msg
})
return false
}
instance?.proxy?.$message.success('上传成功')
console.log(res)
percent.value = 0
})
.catch(err => {
uploadLoad.value = false
instance?.proxy?.$notification.warning({
title: '提示',
content: err.message
})
})
}
</script>
← 权限控制 添加Arco Design →