尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

Vue项目axios二次封装实战:从基础拦截器到高级特性

Vue项目axios二次封装实战:从基础拦截器到高级特性 1. 项目概述为什么我们需要二次封装axios在Vue项目里你肯定用过axios。它简单、强大是处理HTTP请求的首选。但如果你还在每个组件里直接import axios from ‘axios’然后写一堆重复的.then和.catch那说明你的项目还停留在“能用”的阶段离“好用”和“好维护”还有一段距离。我见过太多项目因为早期图省事后期接口一多各种请求配置、错误处理、加载状态管理散落在各个角落改一处而动全身维护起来苦不堪言。二次封装axios本质上不是为了炫技而是为了解决这些工程化痛点。它就像给你的项目搭建一个统一的“通信基站”。所有进出项目的网络请求都经过这个基站进行标准化处理统一添加身份令牌、格式化请求数据、拦截并处理全局错误、管理请求的加载状态、甚至做接口缓存和重试。这样做的好处是显而易见的业务组件可以更专注于业务逻辑本身而不用操心网络层的脏活累活整个应用的网络行为变得可预测、可管理团队协作时大家遵循同一套请求规范代码风格和健壮性都能得到保障。所以今天要聊的不是简单地教你写一个request.js文件而是基于我多年在多个中大型Vue项目中的实战经验拆解一套完整、健壮、可扩展的axios二次封装方案。我们会从最基础的拦截器开始一步步构建出包含错误分级处理、请求取消、接口类型安全、甚至模拟数据等高级特性的请求层。无论你是刚接触Vue的新手还是正在为老项目技术债头疼的资深开发者这套方案都能给你带来直接的参考价值。2. 核心设计思路与架构规划在动手写代码之前理清设计思路至关重要。一个随意的封装后期可能比不封装还要糟糕。我们的目标是构建一个高内聚、低耦合、易于扩展的请求层。这里我分享一下我的设计思路你可以对照自己的项目需求进行调整。2.1 分层架构设计我倾向于将请求层进行清晰的分层这能让职责更分明Axios实例层这是最底层负责创建axios实例配置基础URL、超时时间等全局设置。一个项目可能根据后端微服务划分创建多个不同的axios实例例如userApiInstance,orderApiInstance。拦截器层这是核心增强层。分为请求拦截器和响应拦截器。请求拦截器在请求发出前统一处理。典型任务包括注入认证Token、设置Content-Type、为GET请求序列化参数、显示全局Loading动画等。响应拦截器在收到响应后统一处理。典型任务包括剥离后端统一响应包装、处理网络错误和业务错误、隐藏Loading动画、对特定错误码如401未授权执行统一跳转等。业务封装层将常用的请求方法GET, POST, PUT, DELETE等进行二次封装暴露更简洁、语义化的API给业务组件调用。例如封装一个get方法自动处理参数序列化封装一个post方法自动根据数据格式设置请求头。类型定义与Mock层可选但推荐对于使用TypeScript的项目定义清晰的请求/响应数据类型接口能极大提升开发体验和代码可靠性。同时集成Mock.js或类似的库在前端独立开发时模拟后端数据实现前后端并行开发。2.2 关键特性考量在设计时我们需要决定封装要支持哪些特性自动携带Token如何安全地存储和获取用户令牌如从Vuex/Pinia或LocalStorage并在请求时自动添加到Header中。错误统一处理如何区分网络错误、HTTP状态码错误、后端自定义业务错误每种错误应该如何友好地反馈给用户例如网络错误提示“请检查网络”401错误跳转登录页业务错误Toast提示具体原因请求取消在页面切换或组件销毁时如何自动取消未完成的请求避免内存泄漏和不可预期的状态更新这在SPA应用中尤为重要。Loading状态管理是否需要一个全局的Loading状态来控制页面顶部的进度条或某个区域的加载图标如何避免多个并行请求时Loading状态的闪烁重试机制对于某些偶发性的网络错误如超时是否要加入自动重试逻辑接口防抖/节流对于频繁触发的查询接口如搜索框是否需要在封装层支持防抖控制我的建议是从核心需求开始逐步迭代。一个最基础的封装必须包含实例创建、请求/响应拦截器、错误处理。其他高级特性可以根据项目复杂度按需引入。下面我们就从零开始实现这个核心。3. 基础封装实战从零搭建请求层让我们开始动手。首先在你的Vue项目假设是Vue 3 Vite中创建一个专用的目录来管理请求层例如src/utils/request/。3.1 创建Axios实例与基础配置在src/utils/request/index.js中我们首先创建axios实例。// src/utils/request/index.js import axios from ‘axios‘; import { ElMessage } from ‘element-plus‘; // 示例使用Element Plus的提示组件你可替换为任何UI库或自定义提示 // 创建axios实例 const service axios.create({ baseURL: import.meta.env.VITE_APP_BASE_API, // 从环境变量读取基础API地址 timeout: 10000, // 请求超时时间毫秒 headers: { ‘Content-Type‘: ‘application/json;charsetutf-8‘, // 默认请求头 }, }); // 这里先导出基础的service实例拦截器将在下一步添加 export default service;注意baseURL通过环境变量VITE_APP_BASE_API配置这非常关键。它允许你在开发、测试、生产环境中使用不同的后端地址。在你的.env.development和.env.production文件中分别配置即可。3.2 实现请求拦截器请求拦截器的主要任务是在请求发出前对配置进行修改。// 继续在 src/utils/request/index.js 中编写 // 请求拦截器 service.interceptors.request.use( (config) { // 在发送请求之前做些什么 // 1. 自动携带Token const token localStorage.getItem(‘access_token‘); // 示例从localStorage获取实际可能来自Vuex/Pinia if (token) { config.headers.Authorization Bearer ${token}; } // 2. 针对GET请求参数序列化axios默认对数组等处理不友好可在此统一处理 if (config.method ‘get‘ config.params) { let url config.url ‘?‘; for (const propName of Object.keys(config.params)) { const value config.params[propName]; // 过滤掉空值避免传给后端无效参数 if (value ! undefined value ! null value ! ‘‘) { if (typeof value ‘object‘) { // 处理数组或对象参数根据后端要求序列化这里示例为JSON字符串 config.params[propName] JSON.stringify(value); } // 其他类型参数axios会正常处理 } else { delete config.params[propName]; // 删除空参数 } } // 注意这里修改的是config.paramsaxios最终会将其拼接到url上 } // 3. 显示全局Loading如果需要 // showFullScreenLoading(); // 假设你有一个全局Loading控制函数 return config; }, (error) { // 对请求错误做些什么通常很少发生如配置错误 // hideFullScreenLoading(); // 关闭Loading console.error(‘Request Interceptor Error:‘, error); return Promise.reject(error); } );实操心得关于Token存储在生产环境中直接放在localStorage虽然方便但可能存在XSS攻击风险。对于安全性要求极高的应用可以考虑使用HttpOnly的Cookie或者结合Vuex/Pinia并将Token存储在内存中但需处理好页面刷新后的重新认证流程。这里为了示例清晰使用了localStorage。3.3 实现响应拦截器响应拦截器是错误处理的核心逻辑相对复杂。// 继续在 src/utils/request/index.js 中编写 // 响应拦截器 service.interceptors.response.use( (response) { // hideFullScreenLoading(); // 请求成功关闭Loading const res response.data; // axios的响应数据在data字段中 // 假设后端返回的统一格式为 { code: 200, data: {}, message: ‘success‘ } const { code, message } res; // 判断业务状态码是否成功这里假设200为成功 if (code 200) { // 直接返回后端接口的data字段这样业务层拿到的就是纯净的数据 return res.data; } else { // 业务逻辑错误例如code50001 代表参数错误 // 1. 统一提示错误信息使用你项目中的UI提示组件 ElMessage({ message: message || ‘业务逻辑错误‘, type: ‘error‘, duration: 5 * 1000, // 显示5秒 }); // 2. 处理特定的业务错误码例如未登录、权限不足等 if (code 401) { // 触发登出操作清空token和用户信息 localStorage.removeItem(‘access_token‘); // 跳转到登录页带上当前路由以便登录后回跳 window.location.href /login?redirect${encodeURIComponent(window.location.pathname)}; // 或者使用Vue Router: router.push(/login?redirect${route.fullPath}); } else if (code 403) { // 权限不足可以跳转到无权限页面 // router.push(‘/403‘); } // 3. 将错误继续抛出这样在业务代码的catch中还能捕获到 return Promise.reject(new Error(message || ‘业务错误‘)); } }, (error) { // hideFullScreenLoading(); // 发生错误也要关闭Loading // 处理HTTP网络错误状态码不在2xx范围内 if (error.response) { // 请求已发出服务器也响应了状态码但状态码超出了2xx的范围 const { status, data } error.response; let errMessage ‘请求错误‘; switch (status) { case 400: errMessage ‘请求参数错误‘; break; case 401: errMessage ‘未授权请重新登录‘; // 同样执行登出和跳转逻辑 localStorage.removeItem(‘access_token‘); window.location.href ‘/login‘; break; case 403: errMessage ‘拒绝访问‘; break; case 404: errMessage 请求地址出错: ${error.response.config.url}; break; case 408: errMessage ‘请求超时‘; break; case 500: errMessage ‘服务器内部错误‘; break; case 501: errMessage ‘服务未实现‘; break; case 502: errMessage ‘网关错误‘; break; case 503: errMessage ‘服务不可用‘; break; case 504: errMessage ‘网关超时‘; break; case 505: errMessage ‘HTTP版本不受支持‘; break; default: errMessage 未知网络错误 ${status}; } // 如果后端在错误响应体里提供了message优先使用 if (data data.message) { errMessage data.message; } ElMessage.error(errMessage); } else if (error.request) { // 请求已经发出但没有收到响应例如网络断开、服务器无响应 // error.request 在浏览器中是 XMLHttpRequest 的实例 if (error.message.includes(‘timeout‘)) { ElMessage.error(‘网络请求超时请检查网络连接‘); } else if (error.message.includes(‘Network Error‘)) { ElMessage.error(‘网络连接异常请检查网络设置‘); } else { ElMessage.error(‘网络异常请检查您的网络‘); } } else { // 在设置请求时触发错误例如配置错误 console.error(‘Error in request setup:‘, error.message); ElMessage.error(请求配置错误: ${error.message}); } // 将错误继续抛出保证业务层的catch能捕获到 return Promise.reject(error); } ); // 最后导出配置好的service实例 export default service;至此一个具备基础认证、错误处理和业务状态码判断的axios封装就完成了。你可以在业务组件中这样使用import request from ‘/utils/request‘; // 导入我们封装的实例 export function getUserInfo(userId) { return request({ url: /api/user/${userId}, method: ‘get‘, }); } // 在Vue组件中 async function fetchData() { try { const userData await getUserInfo(123); console.log(‘成功:‘, userData); // 直接使用数据无需再解构res.data } catch (error) { // 这里捕获到的错误已经是经过拦截器处理并提示过的。 // 你可以在这里做一些组件特有的错误处理比如重置状态。 console.error(‘组件内捕获错误:‘, error); } }4. 高级特性与工程化增强基础封装满足了80%的需求但对于一个追求卓越的项目我们还需要考虑更多。下面介绍几个我实践中觉得非常有价值的高级特性。4.1 请求取消与自动取消在单页面应用SPA中用户切换页面时上一个页面发起的请求可能还在进行中。如果这些请求在组件销毁后仍然更新已被销毁组件的状态会导致内存泄漏和控制台警告。Axios提供了CancelToken旧版和AbortController新版来取消请求。方案使用AbortController推荐更现代我们可以封装一个“请求取消器”在发起请求时自动生成一个信号signal并在组件卸载时自动取消该请求。首先创建一个工具函数或ComposableVue 3// src/utils/request/requestCancel.js class RequestCanceler { constructor() { this.pendingMap new Map(); // 存储每个请求的标识和对应的AbortController } // 生成请求的唯一标识方法url getPendingKey(config) { const { method, url } config; return [method, url].join(‘‘); } // 添加请求到等待列表 addPending(config) { this.removePending(config); // 发起新请求前先取消相同的未完成请求 const key this.getPendingKey(config); const controller new AbortController(); config.signal controller.signal; // 将signal挂载到请求配置上 this.pendingMap.set(key, controller); } // 从等待列表中移除请求请求完成或取消后 removePending(config) { const key this.getPendingKey(config); if (this.pendingMap.has(key)) { const controller this.pendingMap.get(key); controller.abort(‘请求被取消‘); // 取消请求 this.pendingMap.delete(key); } } // 清空所有等待中的请求例如用户登出时 clearAllPending() { this.pendingMap.forEach((controller) { controller.abort(‘清空所有请求‘); }); this.pendingMap.clear(); } } export const requestCanceler new RequestCanceler();然后在请求拦截器中添加请求在响应拦截器中移除请求// 在 src/utils/request/index.js 的请求拦截器中 import { requestCanceler } from ‘./requestCancel‘; service.interceptors.request.use((config) { // ... 其他逻辑 requestCanceler.addPending(config); // 添加请求到取消列表 return config; }); // 在响应拦截器的成功和失败回调中 service.interceptors.response.use( (response) { requestCanceler.removePending(response.config); // 请求成功移除 // ... 其他逻辑 }, (error) { // 注意如果请求被取消error的code是 ‘ERR_CANCELED‘ if (axios.isCancel(error)) { console.log(‘请求已被取消:‘, error.message); // 被取消的请求不需要提示错误 return Promise.reject(new Error(‘请求取消‘)); // 返回一个特定的错误避免触发通用错误提示 } // 对于其他错误依然需要移除pending记录如果有的话 if (error.config) { requestCanceler.removePending(error.config); } // ... 其他错误处理逻辑 } );最后在Vue组件中你通常不需要手动操作因为拦截器已经自动管理了。但如果你有特殊场景比如一个查询框用户连续输入时只发送最后一次请求你也可以手动利用这个机制。4.2 业务方法二次封装虽然直接使用request({...})没问题但封装成语义化的函数能让业务代码更简洁。我习惯在src/api/目录下按模块组织API函数。// src/api/user.js import request from ‘/utils/request‘; // 用户相关接口 export function login(data) { return request({ url: ‘/auth/login‘, method: ‘post‘, data, // ES6简写等同于 data: data }); } export function getUserInfo(params) { return request({ url: ‘/user/info‘, method: ‘get‘, params, // GET请求参数用params }); } export function updateUserProfile(data) { return request({ url: ‘/user/profile‘, method: ‘put‘, data, }); } // 带特定配置的请求示例上传文件 export function uploadAvatar(file) { const formData new FormData(); formData.append(‘file‘, file); return request({ url: ‘/user/avatar‘, method: ‘post‘, data: formData, headers: { ‘Content-Type‘: ‘multipart/form-data‘, // 覆盖默认的json头 }, timeout: 30000, // 上传文件需要更长时间 }); }在组件中使用时代码非常清晰script setup import { getUserInfo, updateUserProfile } from ‘/api/user‘; import { ref } from ‘vue‘; const user ref(null); async function loadUser() { user.value await getUserInfo({ id: 123 }); } async function saveProfile() { await updateUserProfile({ name: ‘新名字‘ }); // 成功后提示... } /script4.3 集成TypeScript以获得类型安全如果你的项目使用TypeScript那么给请求层加上类型体验会提升一个档次。主要做两件事定义后端通用的响应体类型定义具体API的请求/响应类型。首先定义通用类型// src/types/api.d.ts // 后端统一的响应结构 export interface ApiResponseT any { code: number; data: T; message: string; [key: string]: any; // 其他可能的字段 } // 分页列表数据的通用结构如果后端统一 export interface ApiPageResponseT any { list: T[]; total: number; page: number; size: number; [key: string]: any; }然后改造我们的request函数使其支持泛型// src/utils/request/index.ts import axios, { AxiosRequestConfig, AxiosResponse } from ‘axios‘; import { ApiResponse } from ‘/types/api‘; // 导入通用类型 const service axios.create({ /* ...配置同上... */ }); // 重写axios的请求方法使其返回我们定义的PromiseApiResponseT // 这里我们覆盖AxiosInstance的类型 export interface CustomAxiosInstance extends AxiosInstance { T any(config: AxiosRequestConfig): PromiseT; // 注意这里直接返回T因为拦截器已经帮我们剥离了code/message // 你也可以保留原始结构PromiseApiResponseT } const service: CustomAxiosInstance axios.create({ /* ... */ }) as any; // ... 拦截器逻辑TypeScript需要适当调整类型 ... export default service;接着为具体的API函数定义类型// src/api/user.ts import request from ‘/utils/request‘; import type { ApiResponse, ApiPageResponse } from ‘/types/api‘; // 定义具体的业务数据类型 export interface UserInfo { id: number; username: string; email: string; avatar: string; } export interface LoginParams { username: string; password: string; } export interface LoginResult { token: string; expiresIn: number; } // 带类型的API函数 export function login(data: LoginParams): PromiseLoginResult { return request({ url: ‘/auth/login‘, method: ‘post‘, data, }); } export function getUserInfo(params: { id: number }): PromiseUserInfo { return request({ url: ‘/user/info‘, method: ‘get‘, params, }); }现在在Vue组件中使用时你将获得完美的代码提示和类型检查script setup lang“ts“ import { getUserInfo, type UserInfo } from ‘/api/user‘; import { ref } from ‘vue‘; const user refUserInfo | null(null); // user有明确的类型 async function loadUser() { user.value await getUserInfo({ id: 123 }); // 参数和返回值都有类型约束 // 访问 user.value.username 会有智能提示 } /script实操心得引入TypeScript初期会增加一些工作量但对于长期维护和团队协作来说其带来的代码智能提示、编译时错误检查和重构便利性是巨大的。强烈建议在中大型项目中采用。5. 常见问题、性能优化与踩坑记录即使有了完善的封装在实际开发中还是会遇到各种问题。下面是我总结的一些高频问题和优化点。5.1 如何避免重复提示错误在响应拦截器中我们对所有非200的业务码和HTTP错误都进行了ElMessage.error提示。但有时候在某个具体组件里我们想自己处理错误不希望弹出全局提示。我们可以通过给请求配置添加一个自定义参数来控制。解决方案利用axios的config对象传递自定义元数据。在请求拦截器中允许一个自定义字段例如noErrorTip来跳过全局错误提示。// 修改响应拦截器的错误处理部分 service.interceptors.response.use( (response) { /* ...成功逻辑不变... */ }, (error) { const { config, response } error; // 检查请求配置中是否有自定义字段决定是否显示全局错误 if (config config.noErrorTip) { // 不显示全局提示直接抛出错误由业务代码处理 return Promise.reject(error); } // ... 原有的全局错误提示逻辑 ... } );在发起请求时传入这个配置。// 在业务API调用处 export function someApi() { return request({ url: ‘/api/some‘, method: ‘get‘, noErrorTip: true, // 添加自定义字段 }); } // 在组件中你就可以自己捕获并处理错误而不会触发全局提示 try { await someApi(); } catch (e) { // 在这里使用自定义的提示方式或者静默处理 console.log(‘业务特定处理:‘, e); }5.2 多个并行请求的Loading管理如果在拦截器中直接控制一个全局Loading的显示/隐藏当多个请求并行时会出现Loading频繁闪烁一个请求结束就关闭但另一个还在进行。我们需要一个计数器来管理。// src/utils/request/loading.js let loadingCount 0; let loadingInstance null; // 存储Loading组件实例 export function showFullScreenLoading() { if (loadingCount 0) { // 只有当第一个请求开始时才显示Loading loadingInstance ElLoading.service({ lock: true, text: ‘加载中...‘, background: ‘rgba(0, 0, 0, 0.7)‘, }); } loadingCount; } export function hideFullScreenLoading() { loadingCount--; if (loadingCount 0) { // 当所有请求都结束时才关闭Loading loadingInstance?.close(); loadingCount 0; loadingInstance null; } }然后在请求和响应拦截器中分别调用showFullScreenLoading和hideFullScreenLoading即可。这样就实现了多个请求共享一个Loading状态只有当所有请求都完成时Loading才会消失。5.3 处理后端返回的非JSON数据有些接口可能返回文件流如下载或纯文本。我们的拦截器默认将响应作为JSON处理这会导致错误。我们可以通过检查响应头Content-Type来动态处理。service.interceptors.response.use( (response) { const contentType response.headers[‘content-type‘]; // 如果是文件流直接返回整个response让业务代码处理 if (contentType contentType.indexOf(‘application/octet-stream‘) ! -1) { return response; } // 如果是JSON走原有的业务逻辑处理 if (contentType contentType.indexOf(‘application/json‘) ! -1) { const res response.data; // ... 原有的code判断逻辑 ... return res.data; } // 其他类型直接返回原始数据 return response.data; }, (error) { /* ...错误处理... */ } );5.4 请求参数序列化的坑axios默认将对象参数序列化成keyvaluekey2value2的形式但对于数组默认会转换成key[]value1key[]value2。如果你的后端期望接收keyvalue1,value2或者JSON字符串就需要自定义序列化。我们在3.2节的请求拦截器中已经演示了将对象参数转为JSON字符串的方法。更复杂的场景可以使用axios的paramsSerializer配置项。import qs from ‘qs‘; // 一个常用的查询字符串库 const service axios.create({ baseURL: ‘...‘, paramsSerializer: (params) { // 使用qs库进行序列化可以处理嵌套对象和数组 return qs.stringify(params, { arrayFormat: ‘brackets‘ }); // 可选 ‘indices‘, ‘brackets‘, ‘repeat‘, ‘comma‘ }, });5.5 封装后的代码组织建议一个清晰的目录结构能让你的请求层更易维护src/ ├── api/ # 所有API模块 │ ├── index.js # 可选统一导出所有API │ ├── user.js # 用户相关接口 │ ├── order.js # 订单相关接口 │ └── product.js # 产品相关接口 ├── utils/ │ └── request/ │ ├── index.js # axios实例创建与拦截器核心 │ ├── requestCancel.js # 请求取消器 │ ├── loading.js # 全局Loading管理 │ └── status.js # 统一维护HTTP状态码和业务状态码映射 └── types/ └── api.d.ts # TypeScript类型定义最后我想说的是封装没有银弹。本文提供的方案是一个功能比较全面的起点你需要根据自己项目的实际技术栈是Vue 2还是Vue 3用Pinia还是Vuex、UI库以及后端接口规范进行适配和调整。核心是理解每一层拦截器、每一个配置项的作用然后打造出最适合自己团队的“通信基站”。在项目迭代过程中这个请求层也会不断进化记录下每一次遇到的问题和解决方案它就会成为你们项目最稳固的基石之一。
返回列表