Skip to content

HTTP 拦截器

移动端应用的HTTP请求和响应拦截器,用于统一处理认证、错误处理、数据转换等逻辑。

📋 拦截器概览

拦截器类型

  • 请求拦截器: 在请求发送前进行处理
  • 响应拦截器: 在响应返回后进行处理
  • 错误拦截器: 专门处理请求和响应错误
  • 数据拦截器: 处理数据加密解密

🔧 请求拦截器

基础请求拦截

typescript
// src/utils/request.ts
import { useUserStore } from '@/stores/user'

// 请求拦截器
uni.addInterceptor('request', {
  invoke(args) {
    // 添加基础URL
    if (!args.url.startsWith('http')) {
      args.url = import.meta.env.VITE_APP_BASE_API + args.url
    }

    // 添加请求头
    args.header = {
      'Content-Type': 'application/json',
      ...args.header
    }

    // 添加认证token
    const userStore = useUserStore()
    if (userStore.token) {
      args.header.Authorization = `Bearer ${userStore.token}`
    }

    // 添加设备信息
    const systemInfo = uni.getSystemInfoSync()
    args.header['X-Device-Type'] = systemInfo.platform
    args.header['X-App-Version'] = systemInfo.version
    args.header['X-Client-Type'] = 'mobile'

    // 请求日志
    console.log(`[HTTP Request] ${args.method || 'GET'} ${args.url}`, {
      header: args.header,
      data: args.data
    })

    return args
  },
  fail(err) {
    console.error('[HTTP Request Failed]', err)
    uni.showToast({
      title: '请求失败',
      icon: 'error'
    })
  }
})

高级请求处理

typescript
// 请求ID生成器
let requestId = 0
const generateRequestId = () => `req_${Date.now()}_${++requestId}`

// 请求缓存
const requestCache = new Map<string, Promise<any>>()

// 请求去重
function deduplicateRequest(config: any) {
  const key = `${config.method}_${config.url}_${JSON.stringify(config.data)}`

  if (requestCache.has(key)) {
    return requestCache.get(key)
  }

  const request = new Promise((resolve, reject) => {
    uni.request({
      ...config,
      success: resolve,
      fail: reject,
      complete: () => {
        requestCache.delete(key)
      }
    })
  })

  requestCache.set(key, request)
  return request
}

// 请求重试机制
function retryRequest(config: any, maxRetries = 3, delay = 1000) {
  return new Promise((resolve, reject) => {
    let retryCount = 0

    const makeRequest = () => {
      uni.request({
        ...config,
        success: resolve,
        fail: (err: any) => {
          if (retryCount < maxRetries && shouldRetry(err)) {
            retryCount++
            setTimeout(makeRequest, delay * retryCount)
          } else {
            reject(err)
          }
        }
      })
    }

    makeRequest()
  })
}

// 判断是否应该重试
function shouldRetry(error: any): boolean {
  // 网络错误或服务器错误重试
  return error.errMsg?.includes('request:fail') ||
         (error.statusCode >= 500 && error.statusCode < 600)
}

数据加密处理

typescript
import { encrypt, decrypt } from '@/utils/crypto'

// 敏感数据加密
function encryptSensitiveData(data: any, config: any) {
  const sensitiveFields = ['password', 'idCard', 'phone', 'bankCard']

  if (config.encrypt && data) {
    const encryptedData = { ...data }

    sensitiveFields.forEach(field => {
      if (encryptedData[field]) {
        encryptedData[field] = encrypt(encryptedData[field])
      }
    })

    return encryptedData
  }

  return data
}

// 请求签名
function signRequest(config: any) {
  if (config.sign) {
    const timestamp = Date.now()
    const nonce = Math.random().toString(36).substr(2)
    const signature = generateSignature(config.data, timestamp, nonce)

    config.header['X-Timestamp'] = timestamp
    config.header['X-Nonce'] = nonce
    config.header['X-Signature'] = signature
  }

  return config
}

function generateSignature(data: any, timestamp: number, nonce: string): string {
  const params = { ...data, timestamp, nonce }
  const sortedKeys = Object.keys(params).sort()
  const queryString = sortedKeys.map(key => `${key}=${params[key]}`).join('&')
  return encrypt(queryString) // 使用加密函数生成签名
}

📨 响应拦截器

基础响应拦截

typescript
// 响应拦截器
uni.addInterceptor('request', {
  returnValue(args) {
    return new Promise((resolve, reject) => {
      // 原始请求
      const originalSuccess = args.success
      const originalFail = args.fail

      args.success = (res: any) => {
        // 响应日志
        console.log(`[HTTP Response] ${res.statusCode} ${args.url}`, res.data)

        // 统一错误处理
        if (res.statusCode >= 200 && res.statusCode < 300) {
          // 业务逻辑处理
          const result = handleBusinessLogic(res.data)
          if (originalSuccess) originalSuccess(result)
          resolve(result)
        } else {
          // HTTP错误处理
          const error = handleHttpError(res)
          if (originalFail) originalFail(error)
          reject(error)
        }
      }

      args.fail = (err: any) => {
        // 网络错误处理
        const error = handleNetworkError(err)
        if (originalFail) originalFail(error)
        reject(error)
      }

      // 执行原始请求
      uni.request(args)
    })
  }
})

业务逻辑处理

typescript
// 统一业务逻辑处理
function handleBusinessLogic(data: any) {
  // 标准API响应格式
  if (data && typeof data === 'object' && 'code' in data) {
    const { code, msg, data: responseData } = data

    switch (code) {
      case 200:
        // 成功
        return responseData
      case 401:
        // 未授权,跳转登录
        handleUnauthorized()
        throw new Error(msg || '未授权访问')
      case 403:
        // 权限不足
        uni.showToast({
          title: msg || '权限不足',
          icon: 'error'
        })
        throw new Error(msg || '权限不足')
      case 500:
        // 服务器错误
        uni.showToast({
          title: '服务器繁忙,请稍后重试',
          icon: 'error'
        })
        throw new Error(msg || '服务器错误')
      default:
        // 其他业务错误
        uni.showToast({
          title: msg || '操作失败',
          icon: 'error'
        })
        throw new Error(msg || '操作失败')
    }
  }

  // 非标准格式直接返回
  return data
}

// 处理未授权
function handleUnauthorized() {
  const userStore = useUserStore()
  userStore.logout()

  // 跳转到登录页
  uni.reLaunch({
    url: '/pages/login/login'
  })
}

数据解密处理

typescript
// 响应数据解密
function decryptResponseData(data: any, config: any) {
  if (config.decrypt && data) {
    try {
      // 解密敏感字段
      const sensitiveFields = ['phone', 'idCard', 'bankCard']
      const decryptedData = { ...data }

      sensitiveFields.forEach(field => {
        if (decryptedData[field] && typeof decryptedData[field] === 'string') {
          try {
            decryptedData[field] = decrypt(decryptedData[field])
          } catch (error) {
            console.warn(`解密字段 ${field} 失败:`, error)
          }
        }
      })

      return decryptedData
    } catch (error) {
      console.error('响应数据解密失败:', error)
      return data
    }
  }

  return data
}

// 数据格式转换
function transformResponseData(data: any, config: any) {
  if (config.transform && typeof config.transform === 'function') {
    return config.transform(data)
  }

  // 默认转换
  if (Array.isArray(data)) {
    return data.map(item => transformItem(item))
  } else if (data && typeof data === 'object') {
    return transformItem(data)
  }

  return data
}

function transformItem(item: any) {
  // 日期字段转换
  const dateFields = ['createTime', 'updateTime', 'startTime', 'endTime']
  dateFields.forEach(field => {
    if (item[field] && typeof item[field] === 'string') {
      item[field] = new Date(item[field])
    }
  })

  // 数字字段转换
  const numberFields = ['sort', 'status', 'id']
  numberFields.forEach(field => {
    if (item[field] && typeof item[field] === 'string' && !isNaN(Number(item[field]))) {
      item[field] = Number(item[field])
    }
  })

  return item
}

❌ 错误处理拦截器

HTTP错误处理

typescript
function handleHttpError(response: any) {
  const { statusCode, data } = response
  let message = '请求失败'

  switch (statusCode) {
    case 400:
      message = '请求参数错误'
      break
    case 401:
      message = '未授权访问'
      handleUnauthorized()
      break
    case 403:
      message = '权限不足'
      break
    case 404:
      message = '请求的资源不存在'
      break
    case 405:
      message = '请求方法不允许'
      break
    case 408:
      message = '请求超时'
      break
    case 429:
      message = '请求过于频繁'
      break
    case 500:
      message = '服务器内部错误'
      break
    case 502:
      message = '网关错误'
      break
    case 503:
      message = '服务不可用'
      break
    case 504:
      message = '网关超时'
      break
    default:
      message = data?.msg || `请求失败 (${statusCode})`
  }

  const error = new Error(message)
  Object.assign(error, { statusCode, data, type: 'HTTP_ERROR' })

  // 显示错误提示
  if (statusCode !== 401) { // 401错误已在handleUnauthorized中处理
    uni.showToast({
      title: message,
      icon: 'error'
    })
  }

  return error
}

网络错误处理

typescript
function handleNetworkError(error: any) {
  let message = '网络连接失败'
  let type = 'NETWORK_ERROR'

  if (error.errMsg) {
    if (error.errMsg.includes('timeout')) {
      message = '请求超时,请检查网络连接'
      type = 'TIMEOUT_ERROR'
    } else if (error.errMsg.includes('fail')) {
      message = '网络连接失败,请检查网络设置'
      type = 'CONNECTION_ERROR'
    } else if (error.errMsg.includes('abort')) {
      message = '请求已取消'
      type = 'ABORT_ERROR'
    }
  }

  const networkError = new Error(message)
  Object.assign(networkError, { ...error, type })

  // 显示错误提示
  uni.showToast({
    title: message,
    icon: 'error'
  })

  return networkError
}

// 网络状态监听
function setupNetworkMonitor() {
  uni.onNetworkStatusChange((res) => {
    if (!res.isConnected) {
      uni.showToast({
        title: '网络连接已断开',
        icon: 'error'
      })
    } else {
      // 网络恢复时重试失败的请求
      retryFailedRequests()
    }
  })
}

// 重试失败的请求
const failedRequests: any[] = []

function retryFailedRequests() {
  failedRequests.forEach(request => {
    uni.request(request)
  })
  failedRequests.length = 0
}

🔄 请求取消机制

请求取消实现

typescript
// 请求取消控制器
class RequestCancelController {
  private requestTasks = new Map<string, any>()

  // 创建可取消的请求
  createCancelableRequest(config: any) {
    const requestId = generateRequestId()

    const requestTask = uni.request({
      ...config,
      success: (res) => {
        this.requestTasks.delete(requestId)
        config.success?.(res)
      },
      fail: (err) => {
        this.requestTasks.delete(requestId)
        config.fail?.(err)
      }
    })

    this.requestTasks.set(requestId, requestTask)
    return { requestId, requestTask }
  }

  // 取消指定请求
  cancelRequest(requestId: string) {
    const requestTask = this.requestTasks.get(requestId)
    if (requestTask) {
      requestTask.abort()
      this.requestTasks.delete(requestId)
    }
  }

  // 取消所有请求
  cancelAllRequests() {
    this.requestTasks.forEach(requestTask => {
      requestTask.abort()
    })
    this.requestTasks.clear()
  }

  // 取消指定URL的请求
  cancelRequestsByUrl(url: string) {
    this.requestTasks.forEach((requestTask, requestId) => {
      if (requestTask.url === url) {
        requestTask.abort()
        this.requestTasks.delete(requestId)
      }
    })
  }
}

// 全局请求取消控制器
export const requestCancelController = new RequestCancelController()

页面级请求管理

typescript
// 页面组合函数:自动管理页面请求
export function usePageRequests() {
  const controller = new RequestCancelController()

  // 页面卸载时取消所有请求
  onUnmounted(() => {
    controller.cancelAllRequests()
  })

  // 创建页面级请求
  const request = (config: any) => {
    return controller.createCancelableRequest(config)
  }

  // 手动取消请求
  const cancelRequest = (requestId: string) => {
    controller.cancelRequest(requestId)
  }

  // 取消所有请求
  const cancelAllRequests = () => {
    controller.cancelAllRequests()
  }

  return {
    request,
    cancelRequest,
    cancelAllRequests
  }
}

// 使用示例
export default defineComponent({
  setup() {
    const { request, cancelAllRequests } = usePageRequests()

    const fetchData = async () => {
      const { requestId } = request({
        url: '/api/data',
        success: (res) => {
          console.log('数据获取成功', res.data)
        }
      })
      return requestId
    }

    return {
      fetchData,
      cancelAllRequests
    }
  }
})

📊 请求监控和统计

请求性能监控

typescript
// 请求性能监控
class RequestMonitor {
  private metrics = new Map<string, any>()

  startRequest(requestId: string, config: any) {
    this.metrics.set(requestId, {
      url: config.url,
      method: config.method || 'GET',
      startTime: Date.now(),
      size: this.calculateRequestSize(config.data)
    })
  }

  endRequest(requestId: string, response?: any, error?: any) {
    const metric = this.metrics.get(requestId)
    if (metric) {
      metric.endTime = Date.now()
      metric.duration = metric.endTime - metric.startTime
      metric.success = !error
      metric.statusCode = response?.statusCode
      metric.responseSize = this.calculateResponseSize(response?.data)

      // 记录到本地存储或发送到监控服务
      this.recordMetric(metric)
      this.metrics.delete(requestId)
    }
  }

  private calculateRequestSize(data: any): number {
    if (!data) return 0
    return new Blob([JSON.stringify(data)]).size
  }

  private calculateResponseSize(data: any): number {
    if (!data) return 0
    return new Blob([JSON.stringify(data)]).size
  }

  private recordMetric(metric: any) {
    // 保存到本地存储
    const metrics = uni.getStorageSync('request_metrics') || []
    metrics.push(metric)

    // 只保留最近100条记录
    if (metrics.length > 100) {
      metrics.splice(0, metrics.length - 100)
    }

    uni.setStorageSync('request_metrics', metrics)

    // 性能警告
    if (metric.duration > 5000) {
      console.warn(`慢请求警告: ${metric.url} 耗时 ${metric.duration}ms`)
    }
  }

  // 获取性能统计
  getPerformanceStats() {
    const metrics = uni.getStorageSync('request_metrics') || []

    return {
      totalRequests: metrics.length,
      successRate: metrics.filter((m: any) => m.success).length / metrics.length,
      averageDuration: metrics.reduce((sum: number, m: any) => sum + m.duration, 0) / metrics.length,
      slowRequests: metrics.filter((m: any) => m.duration > 3000),
      errorRequests: metrics.filter((m: any) => !m.success)
    }
  }
}

export const requestMonitor = new RequestMonitor()

使用示例

typescript
// 在拦截器中集成监控
uni.addInterceptor('request', {
  invoke(args) {
    const requestId = generateRequestId()
    args.requestId = requestId

    // 开始监控
    requestMonitor.startRequest(requestId, args)

    return args
  },
  success(res, args) {
    // 结束监控
    requestMonitor.endRequest(args.requestId, res)
    return res
  },
  fail(err, args) {
    // 记录错误
    requestMonitor.endRequest(args.requestId, undefined, err)
    return err
  }
})

// 获取性能统计
const stats = requestMonitor.getPerformanceStats()
console.log('请求性能统计:', stats)

HTTP拦截器为移动端应用提供了统一的请求处理机制,确保了数据安全、错误处理和性能监控的一致性。