1. Axios 请求库二次封装
vue-element-plus-admin 项目基于 Axios 实现了一套完整的 HTTP 请求解决方案,通过多层封装使得接口调用更加简洁、统一,并提供了完善的类型支持和错误处理机制。
1.1 整体架构设计
项目中 HTTP 请求相关代码主要位于 src/axios
目录,采用了分层封装的架构:
bash
src/axios/
├── config.ts # 拦截器配置
├── index.ts # 对外暴露的请求方法
├── service.ts # Axios 实例创建与核心配置
└── types/ # 类型定义
└── index.ts
这种设计实现了关注点分离,使得各层职责明确:
service.ts
负责创建 Axios 实例和基础配置config.ts
处理请求和响应拦截逻辑index.ts
提供友好的请求接口,是业务代码的直接调用层types
目录提供相关类型定义,确保类型安全
1.2 Axios 实例创建与配置
在 src/axios/service.ts
中,项目创建了 Axios 实例并进行了基础配置:
typescript
import axios from 'axios'
import { defaultRequestInterceptors, defaultResponseInterceptors } from './config'
import { REQUEST_TIMEOUT } from '@/constants'
export const PATH_URL = import.meta.env.VITE_API_BASE_PATH
const axiosInstance = axios.create({
timeout: REQUEST_TIMEOUT,
baseURL: PATH_URL
})
关键配置:
- 请求超时:从常量中获取超时设置,提高可维护性
- 基础URL:使用环境变量配置,支持不同环境切换不同的API地址
- 实例独立:创建独立的 Axios 实例,避免全局污染
1.3 请求和响应拦截器
在 src/axios/config.ts
中,定义了默认的请求和响应拦截器:
typescript
// 请求拦截器
const defaultRequestInterceptors = (config: InternalAxiosRequestConfig) => {
// 处理 POST 请求数据格式
if (
config.method === 'post' &&
config.headers['Content-Type'] === 'application/x-www-form-urlencoded'
) {
config.data = qs.stringify(config.data)
}
// 处理 FormData 格式
else if (
TRANSFORM_REQUEST_DATA &&
config.method === 'post' &&
config.headers['Content-Type'] === 'multipart/form-data' &&
!(config.data instanceof FormData)
) {
config.data = objToFormData(config.data)
}
// GET 请求参数处理
if (config.method === 'get' && config.params) {
let url = config.url as string
url += '?'
const keys = Object.keys(config.params)
for (const key of keys) {
if (config.params[key] !== void 0 && config.params[key] !== null) {
url += `${key}=${encodeURIComponent(config.params[key])}&`
}
}
url = url.substring(0, url.length - 1)
config.params = {}
config.url = url
}
return config
}
// 响应拦截器
const defaultResponseInterceptors = (response: AxiosResponse) => {
// 文件流直接返回
if (response?.config?.responseType === 'blob') {
return response
}
// 正常响应
else if (response.data.code === SUCCESS_CODE) {
return response.data
}
// 错误处理
else {
ElMessage.error(response?.data?.message)
// 401状态码特殊处理,自动登出
if (response?.data?.code === 401) {
const userStore = useUserStoreWithOut()
userStore.logout()
}
}
}
拦截器主要功能:
- 请求数据转换:根据不同的 Content-Type 自动转换请求数据格式
- GET 参数格式化:优化 GET 请求的参数处理
- 响应统一处理:根据状态码和响应类型进行不同处理
- 错误提示:自动显示后端返回的错误信息
- 认证过期处理:遇到 401 状态码自动执行登出流程
1.4 请求方法封装
在 src/axios/index.ts
中,项目对 Axios 的请求方法进行了更高层次的封装:
typescript
const request = (option: AxiosConfig) => {
const { url, method, params, data, headers, responseType } = option
const userStore = useUserStoreWithOut()
return service.request({
url: url,
method,
params,
data: data,
responseType: responseType,
headers: {
'Content-Type': CONTENT_TYPE,
[userStore.getTokenKey ?? 'Authorization']: userStore.getToken ?? '',
...headers
}
})
}
export default {
get: <T = any>(option: AxiosConfig) => {
return request({ method: 'get', ...option }) as Promise<IResponse<T>>
},
post: <T = any>(option: AxiosConfig) => {
return request({ method: 'post', ...option }) as Promise<IResponse<T>>
},
delete: <T = any>(option: AxiosConfig) => {
return request({ method: 'delete', ...option }) as Promise<IResponse<T>>
},
put: <T = any>(option: AxiosConfig) => {
return request({ method: 'put', ...option }) as Promise<IResponse<T>>
},
cancelRequest: (url: string | string[]) => {
return service.cancelRequest(url)
},
cancelAllRequest: () => {
return service.cancelAllRequest()
}
}
这一层封装提供了以下优势:
- 简化调用:简化了请求方法的调用方式
- 统一认证:自动添加认证令牌到请求头
- 类型支持:通过泛型提供了强类型支持
- 请求取消功能:提供了便捷的请求取消方法
2. 统一请求拦截与响应处理
vue-element-plus-admin 项目实现了完整的请求拦截和响应处理机制,确保了网络请求的一致性和可靠性。
2.1 请求拦截器链
项目设置了多层请求拦截器,每个拦截器负责不同的功能:
typescript
// src/axios/service.ts
// 请求取消拦截器
axiosInstance.interceptors.request.use((res: InternalAxiosRequestConfig) => {
const controller = new AbortController()
const url = res.url || ''
res.signal = controller.signal
abortControllerMap.set(
import.meta.env.VITE_USE_MOCK === 'true' ? url.replace('/mock', '') : url,
controller
)
return res
})
// 自定义请求拦截器
axiosInstance.interceptors.request.use(defaultRequestInterceptors)
这种设计实现了:
- 请求可取消:每个请求都关联一个 AbortController,允许在需要时取消请求
- 拦截器分层:不同功能的拦截逻辑分开处理,提高代码可维护性
- Mock 模式适配:针对 Mock 环境做了特殊处理
2.2 响应拦截器链
同样,项目设置了多层响应拦截器:
typescript
// src/axios/service.ts
// 请求取消管理
axiosInstance.interceptors.response.use(
(res: AxiosResponse) => {
const url = res.config.url || ''
abortControllerMap.delete(url)
return res
},
(error: AxiosError) => {
console.log('err: ' + error) // for debug
ElMessage.error(error.message)
return Promise.reject(error)
}
)
// 业务响应处理
axiosInstance.interceptors.response.use(defaultResponseInterceptors)
响应拦截器的功能:
- 清理请求映射:成功响应后,从 abortControllerMap 中删除对应请求
- 统一错误处理:捕获并提示网络层面的错误
- 业务结果处理:处理业务层面的响应结果和错误
2.3 请求取消机制
项目实现了灵活的请求取消机制,基于 AbortController API:
typescript
const abortControllerMap: Map<string, AbortController> = new Map()
const service = {
// ... 其他方法
// 取消单个请求
cancelRequest: (url: string | string[]) => {
const urlList = Array.isArray(url) ? url : [url]
for (const _url of urlList) {
abortControllerMap.get(_url)?.abort()
abortControllerMap.delete(_url)
}
},
// 取消所有请求
cancelAllRequest() {
for (const [_, controller] of abortControllerMap) {
controller.abort()
}
abortControllerMap.clear()
}
}
在组件中的使用示例(src/views/Function/Request.vue
):
typescript
// 发送请求
const getRequest1 = async () => {
if (pending.value.has('/request/1')) {
return
}
try {
pending.value.add('/request/1')
const res = await request1()
console.log('【res】:', res)
} catch (error) {
console.log('【error】:', error)
} finally {
pending.value.delete('/request/1')
}
}
// 取消请求
const clickRequest1 = () => {
if (pending.value.has('/request/1')) {
request.cancelRequest('/request/1')
pending.value.delete('/request/1')
return
}
getRequest1()
}
// 取消所有请求
const cancelAll = () => {
request.cancelAllRequest()
pending.value.clear()
}
这种请求取消机制的优势:
- 避免竞态条件:防止多个相同请求同时发出
- 提升用户体验:允许用户取消长时间运行的请求
- 资源优化:避免不必要的网络请求占用资源
- 组件卸载处理:组件卸载时可以取消相关请求
3. Mock 数据系统集成
vue-element-plus-admin 项目集成了强大的 Mock 数据系统,使开发过程中不依赖后端接口也能进行前端开发。
3.1 Mock 系统架构
项目使用 vite-plugin-mock
作为 Mock 服务的核心,配置位于 vite.config.ts
中:
typescript
// vite.config.ts
plugins: [
// ... 其他插件
env.VITE_USE_MOCK === 'true'
? viteMockServe({
ignore: /^\_/,
mockPath: 'mock',
localEnabled: !isBuild,
prodEnabled: isBuild,
injectCode: `
import { setupProdMockServer } from '../mock/_createProductionServer'
setupProdMockServer()
`
})
: undefined,
// ... 其他插件
]
Mock 系统的主要特点:
- 可配置性 :通过环境变量
VITE_USE_MOCK
控制是否启用 - 开发与生产环境支持:既支持开发模式,也支持生产构建
- 目录规范 :统一使用
mock
目录管理所有 Mock 数据 - 命名约定 :以
_
开头的文件会被忽略,便于组织辅助文件
3.2 Mock 数据结构
项目中的 Mock 文件遵循一致的结构,例如 mock/user/index.mock.ts
:
typescript
import { SUCCESS_CODE } from '@/constants'
const timeout = 1000
const List: {
username: string
password: string
role: string
roleId: string
permissions: string | string[]
}[] = [
{
username: 'admin',
password: 'admin',
role: 'admin',
roleId: '1',
permissions: ['*.*.*']
},
{
username: 'test',
password: 'test',
role: 'test',
roleId: '2',
permissions: ['example:dialog:create', 'example:dialog:delete']
}
]
export default [
// 登录接口
{
url: '/mock/user/login',
method: 'post',
timeout,
response: ({ body }) => {
const data = body
let hasUser = false
for (const user of List) {
if (user.username === data.username && user.password === data.password) {
hasUser = true
return {
code: SUCCESS_CODE,
data: user
}
}
}
if (!hasUser) {
return {
code: 500,
message: '账号或密码错误'
}
}
}
},
// 其他接口...
]
每个 Mock 文件导出一个数组,每个数组项定义了一个模拟接口,包含:
- URL:接口路径
- Method:请求方法
- Timeout:模拟的响应延时
- Response:一个函数,根据请求返回响应数据
3.3 生产环境的 Mock 处理
对于生产环境,项目提供了专门的 Mock 服务器配置,位于 mock/_createProductionServer.ts
:
typescript
import { createProdMockServer } from 'vite-plugin-mock/es/createProdMockServer'
const modules = import.meta.glob('./**/*.mock.ts', {
import: 'default',
eager: true
})
const mockModules: any[] = []
Object.keys(modules).forEach(async (key) => {
if (key.includes('_')) {
return
}
mockModules.push(...(modules[key] as any))
})
export function setupProdMockServer() {
createProdMockServer(mockModules)
}
这段代码实现了:
- 动态加载 :使用 Vite 的
import.meta.glob
动态加载所有 Mock 文件 - 过滤规则 :跳过以
_
开头的文件 - 统一注册:将所有 Mock 接口注册到生产 Mock 服务器
3.4 Mock 与实际请求的切换
项目提供了便捷的方式在 Mock 和实际 API 之间切换:
-
通过环境变量切换 :在 .env 文件中配置
VITE_USE_MOCK
和VITE_API_BASE_PATH
-
代码中的适配:
typescript
// src/axios/service.ts
abortControllerMap.set(
import.meta.env.VITE_USE_MOCK === 'true' ? url.replace('/mock', '') : url,
controller
)
这种设计使得项目可以无缝在 Mock 数据和实际 API 之间切换,方便开发和测试。
4. 接口管理与组织
vue-element-plus-admin 项目采用了模块化的方式组织 API 接口,使得接口调用清晰、可维护,并提供了完善的类型支持。
4.1 接口目录结构
项目的 API 接口定义位于 src/api
目录,按功能模块划分:
bash
src/api/
├── common/ # 通用接口
├── dashboard/ # 仪表盘相关接口
│ ├── analysis/ # 分析页接口
│ └── workplace/ # 工作台接口
├── department/ # 部门管理接口
├── login/ # 登录相关接口
├── menu/ # 菜单接口
├── request/ # 请求示例接口
├── role/ # 角色接口
└── table/ # 表格相关接口
这种组织方式的优点:
- 关注点分离:接口按业务模块划分,便于维护
- 清晰的命名空间:避免接口命名冲突
- 可扩展性:新增功能模块时容易扩展
4.2 接口定义规范
每个接口模块通常包含两个文件:
index.ts
:定义接口函数types.ts
:定义接口相关的类型
例如,登录模块的接口定义:
typescript
// src/api/login/index.ts
import request from '@/axios'
import type { UserType } from './types'
interface RoleParams {
roleName: string
}
export const loginApi = (data: UserType): Promise<IResponse<UserType>> => {
return request.post({ url: '/mock/user/login', data })
}
export const loginOutApi = (): Promise<IResponse> => {
return request.get({ url: '/mock/user/loginOut' })
}
export const getUserListApi = ({ params }: AxiosConfig) => {
return request.get<{
code: string
data: {
list: UserType[]
total: number
}
}>({ url: '/mock/user/list', params })
}
// ... 其他接口定义
typescript
// src/api/login/types.ts
export interface UserLoginType {
username: string
password: string
}
export interface UserType {
username: string
password: string
role: string
roleId: string
}
这种定义方式的优势:
- 类型安全:通过 TypeScript 提供完整的类型支持
- 接口参数明确:明确定义每个接口的请求参数和响应数据类型
- 一致的命名规范 :接口命名遵循
xxxApi
的格式,易于识别 - 可重用的类型:通过 types.ts 集中管理类型定义,便于重用
4.3 接口调用示例
在组件中调用接口的标准方式,以登录为例:
typescript
// 导入接口
import { loginApi } from '@/api/login'
// 调用接口
const handleLogin = async () => {
try {
const loginRes = await loginApi({
username: form.username,
password: form.password
})
// 处理响应数据
userStore.setToken(loginRes.token)
// ... 其他操作
} catch (error) {
// 错误处理
console.error(error)
}
}
这种调用方式的好处:
- 简洁明了:调用代码简洁,关注业务逻辑
- 错误处理:通过 try/catch 处理异常情况
- 类型推导:IDE 可以提供参数和返回值的类型提示
4.4 接口模拟与前后端分离
项目实现了完全的前后端分离,通过以下方式支持前端独立开发:
- 接口定义先行:先定义好接口和类型,再进行具体实现
- Mock 数据支持:使用 Mock 数据模拟后端接口响应
- 环境变量切换:轻松切换开发环境、测试环境、生产环境的 API
这种设计使得前端开发可以不依赖实际后端接口,提高了开发效率和并行工作能力。
5. 全局错误处理机制
vue-element-plus-admin 项目实现了多层次的错误处理机制,确保了系统在遇到异常情况时能够优雅地处理并提供良好的用户体验。
5.1 网络请求错误处理
在 Axios 拦截器中,项目统一处理了网络请求错误:
typescript
// src/axios/service.ts
axiosInstance.interceptors.response.use(
(res: AxiosResponse) => {
// ... 正常响应处理
},
(error: AxiosError) => {
console.log('err: ' + error) // for debug
ElMessage.error(error.message) // 显示错误信息
return Promise.reject(error) // 继续传递错误
}
)
这段代码处理了网络层面的错误,如:
- 网络连接问题
- 请求超时
- CORS 错误
- 请求被取消
5.2 业务逻辑错误处理
对于业务逻辑层面的错误,在响应拦截器中进行处理:
typescript
// src/axios/config.ts
const defaultResponseInterceptors = (response: AxiosResponse) => {
// ... 文件流和正常响应处理
// 错误处理
else {
ElMessage.error(response?.data?.message) // 显示后端返回的错误信息
// 特殊状态码处理 - 401 未授权
if (response?.data?.code === 401) {
const userStore = useUserStoreWithOut()
userStore.logout() // 自动登出
}
}
}
这段代码处理了业务层面的错误,包括:
- 显示错误信息:自动弹出错误提示
- 特殊错误处理:针对认证过期等特殊情况进行处理
- 自动跳转:根据错误类型执行相应的跳转逻辑
5.3 组件中的错误处理
在组件中调用接口时,项目采用 try/catch 进行更细粒度的错误处理:
typescript
// src/views/Function/Request.vue
const getRequest1 = async () => {
if (pending.value.has('/request/1')) {
return
}
try {
pending.value.add('/request/1')
const res = await request1()
console.log('【res】:', res)
} catch (error) {
console.log('【error】:', error)
// 这里可以添加特定的错误处理逻辑
} finally {
pending.value.delete('/request/1')
// 无论成功或失败,都需要执行的逻辑
}
}
组件级别的错误处理提供了:
- 精细化控制:可以针对特定接口定制错误处理逻辑
- 状态管理:在错误发生后正确清理状态
- 用户体验优化:针对错误提供更友好的反馈
5.4 全局未捕获错误处理
对于未被上述机制捕获的错误,可以通过 Vue 的全局错误处理机制进行兜底:
typescript
app.config.errorHandler = (err, instance, info) => {
// 处理未捕获的错误
console.error('Global Error:', err)
// 可以发送到错误监控系统
}
虽然项目中没有明确展示这部分代码,但这是处理未预期错误的推荐做法。
5.5 请求重试与并发控制
项目实现了请求并发控制和状态跟踪机制,以避免重复请求和提高用户体验:
typescript
// src/views/Function/Request.vue
const pending = ref<Set<string>>(new Set()) // 追踪进行中的请求
const getRequest1 = async () => {
// 防止重复请求
if (pending.value.has('/request/1')) {
return
}
try {
// 记录请求状态
pending.value.add('/request/1')
const res = await request1()
// 处理响应...
} catch (error) {
// 处理错误...
} finally {
// 清理状态
pending.value.delete('/request/1')
}
}
这种机制提供了:
- 防重复请求:避免用户短时间内发起同一请求
- 可视化状态:可以显示哪些请求正在进行中
- 请求管理:可以集中管理和取消请求
6. 请求重试与并发控制
vue-element-plus-admin 项目实现了灵活的请求管理机制,包括请求取消、并发控制和状态追踪。
6.1 请求取消机制
项目基于最新的 AbortController API 实现了请求取消功能:
typescript
// src/axios/service.ts
const abortControllerMap: Map<string, AbortController> = new Map()
// 请求拦截器中设置取消控制器
axiosInstance.interceptors.request.use((res: InternalAxiosRequestConfig) => {
const controller = new AbortController()
const url = res.url || ''
res.signal = controller.signal
abortControllerMap.set(
import.meta.env.VITE_USE_MOCK === 'true' ? url.replace('/mock', '') : url,
controller
)
return res
})
// 响应拦截器中清理控制器
axiosInstance.interceptors.response.use(
(res: AxiosResponse) => {
const url = res.config.url || ''
abortControllerMap.delete(url)
return res
},
// 错误处理...
)
// 取消单个请求
const cancelRequest = (url: string | string[]) => {
const urlList = Array.isArray(url) ? url : [url]
for (const _url of urlList) {
abortControllerMap.get(_url)?.abort()
abortControllerMap.delete(_url)
}
}
// 取消所有请求
const cancelAllRequest = () => {
for (const [_, controller] of abortControllerMap) {
controller.abort()
}
abortControllerMap.clear()
}
这种实现的优点:
- 现代化:使用最新的 AbortController API,替代已废弃的 CancelToken
- 集中管理:所有请求的取消控制器都集中管理
- 灵活性:支持取消单个请求或所有请求
6.2 请求状态跟踪
在实际应用中,项目通过响应式数据结构跟踪请求状态:
typescript
// src/views/Function/Request.vue
const pending = ref<Set<string>>(new Set()) // 使用 Set 存储进行中的请求
// 发起请求前检查并添加状态
if (pending.value.has('/request/1')) {
return // 已有同名请求正在进行,直接返回
}
pending.value.add('/request/1') // 记录请求状态
// 请求完成后清理状态
finally {
pending.value.delete('/request/1')
}
// 将 Set 转换为数组用于显示
const setToArray = (set: Set<string>) => {
const arr: string[] = []
set.forEach((item) => {
arr.push(item)
})
return arr
}
在模板中显示请求状态:
html
<p>正在请求的接口:{{ setToArray(pending) }}</p>
这种设计提供了:
- 可视化反馈:用户可以看到哪些请求正在进行
- 防重复请求:避免同一请求被重复发起
- 状态跟踪:便于开发调试和问题排查
6.3 请求管理组件实现
项目中的 Request 组件示例了完整的请求管理实现:
typescript
// 处理请求/取消切换的按钮点击
const clickRequest1 = () => {
if (pending.value.has('/request/1')) {
// 如果请求正在进行中,则取消
request.cancelRequest('/request/1')
pending.value.delete('/request/1')
return
}
// 否则发起新请求
getRequest1()
}
// 取消所有请求
const cancelAll = () => {
request.cancelAllRequest()
pending.value.clear()
}
在模板中:
html
<BaseButton type="primary" @click="clickRequest1">请求/取消request1</BaseButton>
<BaseButton type="primary" @click="cancelAll">关闭所有请求</BaseButton>
这种交互方式使得用户可以:
- 灵活控制:单个按钮实现请求/取消的切换
- 批量操作:一键取消所有进行中的请求
- 直观反馈:界面实时显示请求状态
6.4 Token 过期处理
项目实现了自动处理 Token 过期的机制:
typescript
// src/axios/config.ts
if (response?.data?.code === 401) {
const userStore = useUserStoreWithOut()
userStore.logout()
}
在 Request 组件中有专门用于测试该功能的按钮:
typescript
const tokenExpired = () => {
expired() // 调用一个会返回 401 状态的接口
}
html
<BaseButton type="primary" @click="tokenExpired">token过期</BaseButton>
这种机制可以:
- 自动处理失效认证:无需用户手动处理 Token 过期
- 一致的用户体验:在认证问题时提供标准的处理流程
- 安全性:确保认证失效时及时清理用户状态
7. 总结
vue-element-plus-admin 项目的 HTTP 请求与数据处理模块展现了现代前端应用中 API 交互的最佳实践,具有以下特点:
7.1 架构亮点
- 多层封装设计:从底层 Axios 实例到业务接口的多层封装,实现关注点分离
- 完善的类型支持:通过 TypeScript 提供端到端的类型安全
- 统一的错误处理:包括网络错误、业务错误和特殊状态码处理
- 灵活的 Mock 系统:支持开发和生产环境的数据模拟
- 模块化的接口组织:按业务功能组织 API,提高可维护性
- 先进的请求管理:包括请求取消、状态跟踪和并发控制
7.2 实践启示
从这个项目的HTTP请求与数据处理实现中,我们可以总结出以下实践启示:
- 统一入口原则:所有请求通过统一封装的方法发起,确保一致性
- 分层处理原则:请求和响应的处理逻辑分层实现,便于维护和扩展
- 类型安全原则:利用 TypeScript 定义完善的接口类型,减少运行时错误
- 错误处理原则:在多个层次处理不同类型的错误,提供好的用户体验
- 模块化组织原则:按业务功能组织 API 文件,提高代码可读性和可维护性
- 开发便利原则:通过 Mock 系统支持前后端分离开发,提高效率
7.3 应用场景
这套 HTTP 请求与数据处理方案特别适用于以下场景:
- 企业级中后台应用:需要处理大量、复杂的 API 交互
- 前后端分离项目:需要在前端独立开发阶段模拟后端接口
- 多环境部署:需要在开发、测试、生产等环境之间无缝切换
- 需要细粒度权限控制:需要处理复杂的认证和授权逻辑
- 重视用户体验:需要对网络请求状态提供良好的视觉反馈
7.4 扩展思考
这套方案还可以在以下方向进行扩展:
- 请求重试机制:对特定类型的失败请求自动进行重试
- 接口缓存策略:为某些不常变化的数据实现本地缓存
- 请求队列和优先级:实现请求排队和优先级管理
- 断网恢复机制:在网络恢复后自动重发失败的请求
- 请求性能监控:收集和分析请求性能指标,用于优化
vue-element-plus-admin 项目的 HTTP 请求与数据处理模块为我们展示了一套完整、实用的解决方案,值得在实际项目中借鉴和应用。