Vue3 + Vite项目实战:手把手教你封装一个带Token自动管理的Axios请求库
Vue3 + Vite项目实战:打造企业级Axios请求库的自动化设计
在当今前端工程化实践中,一个健壮的HTTP请求库早已不是简单的请求发送工具,而是承载着Token管理、错误处理、性能监控等多项职责的基础设施。本文将带您从工程化角度,重构一个真正具备生产级质量的请求库解决方案。
1. 请求库架构设计理念
现代前端项目的HTTP请求库需要解决三个核心问题:自动化、可维护性和一致性。我们先来看一个典型的请求生命周期中需要处理的环节:
graph TD A[发起请求] --> B[Token注入] B --> C[参数处理] C --> D[实际请求] D --> E[状态码处理] E --> F[数据格式化] F --> G[错误处理]基于这个流程,我们的请求库需要实现以下关键特性:
- 智能Token管理:自动注入、刷新和失效处理
- 统一错误处理:网络异常、业务异常的分类处理
- 请求生命周期钩子:提供各个阶段的扩展点
- 类型安全:完整的TypeScript支持
- 性能监控:请求耗时统计和异常上报
2. 核心实现方案
2.1 创建增强型Axios实例
不同于基础封装,我们需要创建一个具备扩展能力的Axios实例:
// src/libs/http-client.ts import axios from 'axios' import type { AxiosInstance, AxiosRequestConfig } from 'axios' class HttpClient { private instance: AxiosInstance private interceptors: HttpInterceptors constructor(config: HttpRequestConfig) { this.instance = axios.create(config) this.interceptors = config.interceptors || {} this.setupInterceptors() } private setupInterceptors() { // 请求拦截器链 this.instance.interceptors.request.use( this.interceptors.request?.success, this.interceptors.request?.failure ) // 响应拦截器链 this.instance.interceptors.response.use( this.interceptors.response?.success, this.interceptors.response?.failure ) } public request<T>(config: HttpRequestConfig): Promise<T> { return this.instance.request(config) } // 其他HTTP方法封装... } export default HttpClient2.2 Token自动化管理
实现Token的自动注入和刷新机制需要考虑多种场景:
// Token管理策略 const tokenStrategy = { getToken(): string | null { // 从存储中获取 }, refreshToken(): Promise<string> { // 调用刷新接口 }, shouldRefresh(error): boolean { // 判断是否需要刷新 }, isExpired(token): boolean { // 检查Token过期 } } // 请求队列管理 const pendingRequests = new Map() // 拦截器配置 instance.interceptors.request.use(async (config) => { if (config.requireAuth !== false) { let token = tokenStrategy.getToken() if (token && tokenStrategy.isExpired(token)) { if (!pendingRequests.has('refreshing')) { pendingRequests.set('refreshing', true) token = await tokenStrategy.refreshToken() pendingRequests.delete('refreshing') } else { return new Promise((resolve) => { const unsubscribe = () => { pendingRequests.delete('waiting') resolve(instance(config)) } pendingRequests.set('waiting', unsubscribe) }) } } config.headers.Authorization = `Bearer ${token}` } return config })2.3 智能错误处理系统
建立分级的错误处理机制:
| 错误类型 | 处理方式 | 用户提示 |
|---|---|---|
| 网络错误 | 自动重试/跳转网络设置 | "网络连接不可用" |
| 401未授权 | 清除Token跳转登录 | "登录已过期" |
| 403禁止访问 | 提示权限不足 | "无访问权限" |
| 500服务器错误 | 上报异常/展示备用UI | "服务暂时不可用" |
| 业务逻辑错误 | 透传错误码 | 根据业务码显示对应提示 |
// 错误处理器 class ErrorHandler { static handle(error) { if (error.isAxiosError) { switch (error.response?.status) { case 401: this.handleUnauthorized() break case 403: this.handleForbidden() break // 其他状态码处理... } } // 业务错误处理 if (error.bizCode) { showToast(error.bizMessage) } // 异常上报 trackError(error) } }3. 高级功能实现
3.1 请求取消与竞态处理
实现请求防抖和取消机制:
// 请求取消令牌管理 const cancelTokenMap = new Map() function createCancelToken(key: string) { if (cancelTokenMap.has(key)) { cancelTokenMap.get(key).cancel() } const source = axios.CancelToken.source() cancelTokenMap.set(key, source) return source.token } // 使用示例 async function fetchData(params) { try { const res = await request({ url: '/api/data', cancelToken: createCancelToken('data-request') }) // 处理数据 } catch (err) { if (!axios.isCancel(err)) { // 处理真实错误 } } }3.2 性能监控集成
在拦截器中加入性能统计:
instance.interceptors.request.use((config) => { config.metadata = { startTime: Date.now() } return config }) instance.interceptors.response.use( (response) => { const duration = Date.now() - response.config.metadata.startTime trackApiPerformance({ url: response.config.url, duration, status: response.status }) return response }, (error) => { if (error.config) { const duration = Date.now() - error.config.metadata.startTime trackApiPerformance({ url: error.config.url, duration, status: error.response?.status || 0, isError: true }) } return Promise.reject(error) } )4. 工程化实践建议
4.1 配置管理最佳实践
推荐的项目配置结构:
src/ libs/ http-client/ # 请求库核心 index.ts # 主入口 interceptors/ # 拦截器实现 types/ # 类型定义 utils/ # 工具函数 config/ http.ts # HTTP相关配置 env.ts # 环境变量处理环境变量处理方案:
// config/env.ts export function getApiBaseUrl() { return import.meta.env.VITE_API_BASE ?? '/api' } export function getApiTimeout() { return Number(import.meta.env.VITE_API_TIMEOUT) || 10000 }4.2 团队协作规范
制定团队内的HTTP请求规范:
API定义规范
- 所有API必须定义在
src/api目录下 - 按业务模块组织文件结构
- 必须使用TypeScript接口定义请求/响应类型
- 所有API必须定义在
错误处理规范
- 业务错误必须包含错误码和用户友好提示
- 网络错误要有统一的重试机制
- 敏感错误不上报具体信息
性能优化建议
- 列表接口默认启用分页
- 大数据量响应启用压缩
- 频繁调用的接口考虑本地缓存
5. 测试与调试技巧
5.1 Mock方案对比
| 方案 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| Mock Service Worker | 浏览器层拦截,最接近真实 | 配置复杂 | 开发环境全量Mock |
| Vite Proxy Mock | 零配置,修改即时生效 | 功能有限 | 快速原型开发 |
| 本地JSON文件 | 简单直接 | 无法模拟动态场景 | 静态数据模拟 |
推荐使用Mock Service Worker进行API模拟:
// src/mocks/handlers.ts import { rest } from 'msw' export const handlers = [ rest.get('/api/user', (req, res, ctx) => { return res( ctx.delay(150), ctx.json({ id: 'user-1', name: 'John Doe' }) ) }) ]5.2 调试工具链配置
推荐在开发环境中添加以下调试支持:
// vite.config.js export default defineConfig({ plugins: [ { name: 'configure-response', configureServer(server) { server.middlewares.use((req, res, next) => { res.setHeader('Access-Control-Expose-Headers', 'X-Request-Id') next() }) } } ] })在Chrome开发者工具中,可以通过自定义日志标签过滤请求:
// 在请求拦截器中 console.log( '%cAPI Request%c %s %o', 'background: #4CAF50; color: white; padding: 2px 4px; border-radius: 3px;', '', config.method?.toUpperCase(), { url: config.url, params: config.params, data: config.data } )6. 性能优化策略
6.1 请求缓存实现
基于内存的缓存方案:
const cache = new Map() async function cachedRequest(config) { const cacheKey = JSON.stringify({ url: config.url, params: config.params }) if (cache.has(cacheKey)) { return cache.get(cacheKey) } const response = await instance.request(config) cache.set(cacheKey, response) // 设置过期时间 setTimeout(() => { cache.delete(cacheKey) }, config.cacheTTL || 30000) return response }6.2 压缩与性能调优
推荐配置:
const instance = axios.create({ // 启用请求体压缩 decompress: true, // 优化大JSON响应 transformResponse: [ (data) => { try { return JSONbig.parse(data) } catch { return data } } ], // DNS缓存 httpAgent: new http.Agent({ keepAlive: true }), httpsAgent: new https.Agent({ keepAlive: true }) })7. 安全加固方案
7.1 常见安全防护
- CSRF防护:自动注入XSRF-TOKEN
- CSP合规:检查响应头安全策略
- 敏感信息过滤:拦截器中的日志过滤
// 安全拦截器 const securityInterceptor = { onRequest(config) { // 过滤敏感字段 if (config.data?.password) { config.data.password = '[REDACTED]' } return config }, onResponse(response) { // 检查安全头 if (!response.headers['content-security-policy']) { reportSecurityIssue('Missing CSP header') } return response } }7.2 敏感操作保护
关键操作需要二次确认:
function withConfirmation(requestFn, message) { return async function(...args) { await confirmDialog(message) return requestFn(...args) } } // 使用示例 const deleteUser = withConfirmation( (userId) => request.delete(`/users/${userId}`), '确认删除该用户?' )8. 项目集成示例
8.1 Vue3组合式API集成
创建可复用的HTTP Hook:
// src/composables/useHttp.ts export function useHttp() { const loading = ref(false) const error = ref(null) const request = async (config) => { loading.value = true error.value = null try { const response = await httpClient.request(config) return response } catch (err) { error.value = err throw err } finally { loading.value = false } } return { loading, error, request } }8.2 Pinia状态管理集成
将请求状态纳入状态管理:
// stores/api.ts export const useApiStore = defineStore('api', { state: () => ({ pendingRequests: 0, lastError: null }), actions: { async callApi(config) { this.pendingRequests++ try { const response = await httpClient.request(config) this.lastError = null return response } catch (error) { this.lastError = error throw error } finally { this.pendingRequests-- } } } })9. 演进路线规划
9.1 架构演进方向
微前端适配
- 支持多实例共存
- 主子应用通信桥接
Serverless集成
- 云函数调用封装
- 自动凭证管理
GraphQL适配层
- 查询批处理
- 缓存策略集成
9.2 性能监控扩展
建议增加的监控维度:
- 请求成功率统计
- 慢请求分析
- 依赖服务健康度检查
- 用户端网络质量采样
// 监控插件示例 const monitoringPlugin = { install(httpClient) { httpClient.interceptors.response.use( (response) => { reportSuccess(response) return response }, (error) => { reportError(error) return Promise.reject(error) } ) } }10. 疑难问题解决方案
10.1 常见问题排查
问题1:Token刷新竞态条件
解决方案:
let refreshPromise = null async function getToken() { if (tokenExpired()) { if (!refreshPromise) { refreshPromise = refreshToken().finally(() => { refreshPromise = null }) } return refreshPromise } return currentToken }问题2:文件上传进度丢失
解决方案:
const uploadFile = (file, onProgress) => { const formData = new FormData() formData.append('file', file) return request.post('/upload', formData, { onUploadProgress: (progressEvent) => { const percent = Math.round( (progressEvent.loaded * 100) / progressEvent.total ) onProgress(percent) } }) }10.2 调试技巧
推荐使用以下Chrome开发者工具技巧:
网络请求过滤
- 使用
domain:api.example.com过滤特定域名请求 - 使用
larger-than:1M查找大体积请求
- 使用
性能分析
- 使用Performance面板记录请求瀑布流
- 检查Timing标签中的各个阶段耗时
断点调试
// 在请求拦截器中添加debugger instance.interceptors.request.use((config) => { if (config.url.includes('/sensitive')) { debugger } return config })
11. 前沿技术展望
11.1 WebSocket集成方案
实现实时数据与HTTP请求的统一管理:
class HybridClient { private http: HttpClient private socket: WebSocket constructor() { this.http = new HttpClient() this.socket = new WebSocket('wss://api.example.com') this.setupSocket() } private setupSocket() { this.socket.onmessage = (event) => { this.emit('message', JSON.parse(event.data)) } } public request(config) { if (config.realtime) { return new Promise((resolve) => { const handler = (data) => { if (data.id === config.id) { this.off('message', handler) resolve(data) } } this.on('message', handler) this.socket.send(JSON.stringify(config)) }) } return this.http.request(config) } }11.2 Server-Sent Events适配
处理服务器推送事件:
function createEventSource(url) { const eventSource = new EventSource(url) return { onMessage(handler) { eventSource.onmessage = (event) => { handler(JSON.parse(event.data)) } }, close() { eventSource.close() } } }12. 移动端适配策略
12.1 弱网处理方案
实现离线优先策略:
const offlineCache = new Map() async function offlineRequest(config) { if (navigator.onLine) { try { const response = await request(config) offlineCache.set(config.cacheKey, response) return response } catch (err) { if (offlineCache.has(config.cacheKey)) { return offlineCache.get(config.cacheKey) } throw err } } else if (offlineCache.has(config.cacheKey)) { return offlineCache.get(config.cacheKey) } throw new Error('Offline and no cached data') }12.2 请求优先级管理
基于业务重要性的优先级队列:
class PriorityQueue { private high: Array<() => Promise<any>> private normal: Array<() => Promise<any>> private low: Array<() => Promise<any>> add(task: () => Promise<any>, priority: 'high' | 'normal' | 'low') { this[priority].push(task) this.process() } private processing = false private async process() { if (this.processing) return this.processing = true while (this.high.length || this.normal.length || this.low.length) { const task = this.high.shift() || this.normal.shift() || this.low.shift() try { await task() } catch (err) { console.error('Queue task failed:', err) } } this.processing = false } }13. 微前端场景适配
13.1 主子应用通信
通过自定义事件实现跨应用通信:
// 主应用 window.mainAppBridge = { request: (config) => httpClient.request(config) } // 子应用 function proxyRequest(config) { if (window.mainAppBridge) { return window.mainAppBridge.request(config) } return localRequest(config) }13.2 共享实例方案
// 共享模块 let sharedInstance = null export function getSharedHttpClient() { if (!sharedInstance) { sharedInstance = new HttpClient({ baseURL: '/api', interceptors: { request: [authInterceptor], response: [errorInterceptor] } }) } return sharedInstance }14. 测试策略设计
14.1 单元测试重点
测试关键拦截器逻辑:
describe('Auth Interceptor', () => { it('should add token to headers', async () => { localStorage.setItem('token', 'test-token') const instance = axios.create() instance.interceptors.request.use(authInterceptor) const config = await instance.interceptors.request.handlers[0].fulfilled({ headers: {} }) expect(config.headers.Authorization).toBe('Bearer test-token') }) })14.2 E2E测试方案
使用Cypress进行API测试:
// cypress/e2e/api.cy.js describe('API Suite', () => { it('should handle auth failure', () => { cy.intercept('POST', '/auth', { statusCode: 401, body: { code: 'AUTH_FAILED' } }) cy.visit('/login') cy.get('#submit').click() cy.contains('.error', '认证失败').should('be.visible') }) })15. 部署与运维
15.1 健康检查方案
实现API健康监控端点:
// 健康检查路由 router.get('/health', (req, res) => { const checks = { database: checkDatabase(), cache: checkCache(), thirdParty: checkThirdPartyAPI() } Promise.all(Object.values(checks)) .then(() => res.status(200).json({ status: 'healthy' })) .catch(() => res.status(500).json({ status: 'unhealthy' })) })15.2 灰度发布支持
通过Header控制请求路由:
instance.interceptors.request.use((config) => { if (localStorage.getItem('beta-user') === 'true') { config.headers['X-API-Version'] = 'v2' } return config })16. 性能优化进阶
16.1 请求合并策略
实现类似GraphQL的批处理:
class BatchRequest { private batch: Array<{ config: any, resolve: Function, reject: Function }> = [] private timer: any = null add(config) { return new Promise((resolve, reject) => { this.batch.push({ config, resolve, reject }) if (!this.timer) { this.timer = setTimeout(() => this.flush(), 50) } }) } private async flush() { const batch = this.batch this.batch = [] this.timer = null try { const response = await axios.post('/batch', { requests: batch.map(item => item.config) }) batch.forEach((item, index) => { item.resolve(response.data.results[index]) }) } catch (err) { batch.forEach(item => item.reject(err)) } } }16.2 预加载方案
基于路由的API预加载:
router.beforeEach((to) => { if (to.meta.preloadApis) { to.meta.preloadApis.forEach(api => { api.load() }) } }) // 路由配置示例 { path: '/dashboard', component: Dashboard, meta: { preloadApis: [userApi, statsApi] } }17. 安全防护进阶
17.1 请求签名方案
防止请求篡改:
function createSign(config) { const timestamp = Date.now() const nonce = Math.random().toString(36).slice(2) const str = [ config.method, config.url, timestamp, nonce, JSON.stringify(config.data) ].join('|') const sign = crypto .createHmac('sha256', SECRET_KEY) .update(str) .digest('hex') config.headers['X-Timestamp'] = timestamp config.headers['X-Nonce'] = nonce config.headers['X-Sign'] = sign return config }17.2 敏感操作审计
记录关键操作日志:
instance.interceptors.request.use((config) => { if (config.sensitive) { auditLog({ action: config.url, params: config.data, user: currentUser }) } return config })18. 监控体系集成
18.1 全链路追踪
实现请求链路追踪:
instance.interceptors.request.use((config) => { const traceId = generateTraceId() config.headers['X-Trace-Id'] = traceId startTrace(traceId, config) return config }) instance.interceptors.response.use( (response) => { endTrace(response.config.headers['X-Trace-Id'], 'success') return response }, (error) => { if (error.config) { endTrace(error.config.headers['X-Trace-Id'], 'failed') } return Promise.reject(error) } )18.2 性能指标采集
采集关键性能指标:
const metrics = { requestCount: 0, successCount: 0, errorCount: 0, durationSum: 0 } instance.interceptors.response.use( (response) => { metrics.requestCount++ metrics.successCount++ metrics.durationSum += response.duration return response }, (error) => { metrics.requestCount++ metrics.errorCount++ return Promise.reject(error) } ) // 定期上报 setInterval(() => { reportMetrics({ ...metrics, avgDuration: metrics.durationSum / metrics.requestCount }) }, 60000)19. 国际化支持
19.1 多语言错误处理
根据用户语言返回错误信息:
class I18nErrorHandler { static messages = { 'en-US': { 401: 'Unauthorized', 404: 'Not Found' }, 'zh-CN': { 401: '未授权', 404: '资源不存在' } } static handle(error, lang = 'en-US') { const message = this.messages[lang]?.[error.status] || error.message showToast(message) } }19.2 区域化配置
不同区域的API配置:
function getRegionalConfig() { const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone const region = timezone.startsWith('Asia') ? 'APAC' : 'EMEA' return { baseURL: REGION_CONFIG[region].apiBase, timeout: REGION_CONFIG[region].timeout } }20. 设计模式应用
20.1 策略模式应用
可插拔的拦截器策略:
interface InterceptorStrategy { onRequest?(config): any; onResponse?(response): any; onError?(error): any; } class InterceptorManager { private strategies: InterceptorStrategy[] = [] use(strategy: InterceptorStrategy) { this.strategies.push(strategy) } apply(instance) { instance.interceptors.request.use( (config) => this.strategies.reduce((c, s) => s.onRequest?.(c) ?? c, config) ) instance.interceptors.response.use( (response) => this.strategies.reduce((r, s) => s.onResponse?.(r) ?? r, response), (error) => this.strategies.reduceRight((e, s) => s.onError?.(e) ?? e, error) ) } }20.2 装饰器模式应用
增强请求功能:
function withRetry(times = 3) { return function(target, key, descriptor) { const original = descriptor.value descriptor.value = async function(...args) { let lastError for (let i = 0; i < times; i++) { try { return await original.apply(this, args) } catch (error) { lastError = error await new Promise(resolve => setTimeout(resolve, 1000 * i)) } } throw lastError } return descriptor } } class ApiService { @withRetry(3) async fetchData() { return request.get('/data') } }