前端八股---axios封装

目录


一、什么是封装?

封装就是将重复逻辑、公共逻辑提取出来,统一处理,简化外部调用,提高代码复用性和可维护性。

复制代码
没有封装:
┌─────────┐ ┌─────────┐ ┌─────────┐
│ 组件A   │ │ 组件B   │ │ 组件C   │
│ 重复写  │ │ 重复写  │ │ 重复写  │
│ baseURL │ │ baseURL │ │ baseURL │
│ 加token │ │ 加token │ │ 加token │
│ 错误处理│ │ 错误处理│ │ 错误处理│
└─────────┘ └─────────┘ └─────────┘

有封装:
┌─────────────────────────────────┐
│          封装层                  │
│  baseURL、token、错误处理        │
└─────────────────────────────────┘
         ↑    ↑    ↑
    ┌────┴────┴────┴────┐
    │ 组件A │ 组件B │ 组件C │
    └──────┴──────┴──────┘
    调用简单,只传业务参数

二、封装的核心配置(3 大核心)

1. 创建实例:create()

统一设置基础地址、超时时间等公共配置

javascript 复制代码
// 基础配置
const service = axios.create({
  baseURL: '/api',           // 统一请求前缀
  timeout: 5000,             // 超时时间 5秒
  withCredentials: false,    // 是否携带 Cookie
  headers: {
    'Content-Type': 'application/json'
  }
})

// 不同环境不同配置
const service = axios.create({
  baseURL: import.meta.env.VITE_API_BASE_URL || '/api',
  timeout: 10000
})

2. 请求拦截器:interceptors.request

发请求前统一处理:加 token、加请求头、开启 loading

javascript 复制代码
service.interceptors.request.use(
  config => {
    // 1. 开启 loading(防止重复请求)
    // 2. 添加 token
    const token = localStorage.getItem('token')
    if (token) {
      config.headers.Authorization = `Bearer ${token}`
      // 或 config.headers.token = token
    }
    
    // 3. 添加其他公共参数
    // config.headers['X-Request-Id'] = generateRequestId()
    
    // 4. POST 请求序列化
    // if (config.method === 'post') {
    //   config.data = qs.stringify(config.data)
    // }
    
    return config
  },
  error => {
    return Promise.reject(error)
  }
)

3. 响应拦截器:interceptors.response

拿到数据后统一处理:状态码判断、错误提示、关闭 loading

javascript 复制代码
service.interceptors.response.use(
  response => {
    // 1. 关闭 loading
    // 2. 直接返回 data 层,简化调用
    const res = response.data
    
    // 根据后端约定的状态码判断
    if (res.code !== 200) {
      // 业务错误处理
      ElMessage.error(res.message || '请求失败')
      return Promise.reject(new Error(res.message))
    }
    return res
  },
  error => {
    // 1. 关闭 loading
    // 2. HTTP 状态码错误处理
    const { response } = error
    
    if (response) {
      switch (response.status) {
        case 401:
          // token 过期,跳转登录
          localStorage.removeItem('token')
          window.location.href = '/login'
          break
        case 403:
          ElMessage.error('没有权限')
          break
        case 404:
          ElMessage.error('请求资源不存在')
          break
        case 500:
          ElMessage.error('服务器错误')
          break
        default:
          ElMessage.error(response.data?.message || '请求失败')
      }
    } else if (error.code === 'ECONNABORTED') {
      ElMessage.error('请求超时')
    } else if (error.message.includes('Network Error')) {
      ElMessage.error('网络连接异常')
    } else {
      ElMessage.error(error.message || '未知错误')
    }
    
    return Promise.reject(error)
  }
)

三、封装对外暴露的方法

基础版封装

javascript 复制代码
// api/request.js
import axios from 'axios'

const service = axios.create({
  baseURL: '/api',
  timeout: 5000
})

// 请求拦截器
service.interceptors.request.use(config => {
  const token = localStorage.getItem('token')
  if (token) {
    config.headers.Authorization = `Bearer ${token}`
  }
  return config
})

// 响应拦截器
service.interceptors.response.use(
  response => response.data,
  error => Promise.reject(error)
)

// 封装 GET
export function get(url, params) {
  return service.get(url, { params })
}

// 封装 POST
export function post(url, data) {
  return service.post(url, data)
}

// 封装 PUT
export function put(url, data) {
  return service.put(url, data)
}

// 封装 DELETE
export function del(url, params) {
  return service.delete(url, { params })
}

export default service

进阶版封装(支持取消请求)

javascript 复制代码
// api/request.js
import axios from 'axios'

// 存储取消请求的 Map
const pendingRequests = new Map()

// 生成请求唯一标识
function generateRequestKey(config) {
  const { method, url, params, data } = config
  return [method, url, JSON.stringify(params), JSON.stringify(data)].join('&')
}

// 添加请求到队列
function addPendingRequest(config) {
  const requestKey = generateRequestKey(config)
  const cancelToken = axios.CancelToken
  config.cancelToken = new cancelToken(cancel => {
    if (!pendingRequests.has(requestKey)) {
      pendingRequests.set(requestKey, cancel)
    }
  })
}

// 取消重复请求
function removePendingRequest(config) {
  const requestKey = generateRequestKey(config)
  if (pendingRequests.has(requestKey)) {
    const cancel = pendingRequests.get(requestKey)
    cancel(requestKey)
    pendingRequests.delete(requestKey)
  }
}

const service = axios.create({
  baseURL: '/api',
  timeout: 5000
})

// 请求拦截器
service.interceptors.request.use(config => {
  // 取消重复请求
  removePendingRequest(config)
  addPendingRequest(config)
  
  // 加 token
  const token = localStorage.getItem('token')
  if (token) {
    config.headers.Authorization = `Bearer ${token}`
  }
  return config
})

// 响应拦截器
service.interceptors.response.use(
  response => {
    removePendingRequest(response.config)
    return response.data
  },
  error => {
    if (axios.isCancel(error)) {
      console.log('请求已取消:', error.message)
    } else {
      // 错误处理
    }
    return Promise.reject(error)
  }
)

使用示例

javascript 复制代码
// api/user.js
import { get, post } from './request'

// 获取用户信息
export function getUserInfo(id) {
  return get('/user/info', { id })
}

// 登录
export function login(data) {
  return post('/user/login', data)
}

// 页面中使用
import { getUserInfo } from '@/api/user'

async function loadUser() {
  const res = await getUserInfo(123)
  console.log(res.data)
}

四、Token 处理详解

为什么需要 token?

token 是用户登录后的身份凭证。在请求拦截器中统一添加 token,是为了让后端识别当前登录用户,验证身份和权限。

复制代码
登录流程:
用户登录 → 后端验证 → 返回 token → 前端存储 token
     ↓
后续请求 → 请求拦截器加 token → 后端验证 token → 返回数据

Token 存储位置对比

存储位置 优点 缺点
localStorage 持久化,跨标签页 有 XSS 风险
sessionStorage 会话级,相对安全 关页面丢失
Cookie (HttpOnly) 最安全,防 XSS 需要后端配合
javascript 复制代码
// 存储 token
localStorage.setItem('token', token)

// 获取 token
const token = localStorage.getItem('token')

// 删除 token(退出登录)
localStorage.removeItem('token')

五、完整封装示例

javascript 复制代码
// utils/request.js
import axios from 'axios'
import { ElMessage, ElLoading } from 'element-plus'

let loading = null
let loadingCount = 0

// 显示 loading
function showLoading() {
  if (loadingCount === 0) {
    loading = ElLoading.service({
      fullscreen: true,
      text: '加载中...'
    })
  }
  loadingCount++
}

// 隐藏 loading
function hideLoading() {
  loadingCount--
  if (loadingCount === 0 && loading) {
    loading.close()
    loading = null
  }
}

const service = axios.create({
  baseURL: import.meta.env.VITE_API_BASE_URL || '/api',
  timeout: 10000,
  withCredentials: false
})

// 请求拦截器
service.interceptors.request.use(
  config => {
    // 开启 loading(可配置是否显示)
    if (config.showLoading !== false) {
      showLoading()
    }
    
    // 添加 token
    const token = localStorage.getItem('token')
    if (token) {
      config.headers.Authorization = `Bearer ${token}`
    }
    
    // 添加时间戳防缓存(GET 请求)
    if (config.method === 'get' && config.preventCache) {
      config.params = {
        ...config.params,
        _t: Date.now()
      }
    }
    
    return config
  },
  error => {
    hideLoading()
    return Promise.reject(error)
  }
)

// 响应拦截器
service.interceptors.response.use(
  response => {
    hideLoading()
    
    const res = response.data
    
    // 根据业务状态码判断
    if (res.code !== 0 && res.code !== 200) {
      // token 过期
      if (res.code === 401) {
        ElMessage.error('登录已过期,请重新登录')
        localStorage.removeItem('token')
        window.location.href = '/login'
        return Promise.reject(new Error('登录过期'))
      }
      
      ElMessage.error(res.message || '请求失败')
      return Promise.reject(new Error(res.message))
    }
    
    return res
  },
  error => {
    hideLoading()
    
    if (error.response) {
      const { status, data } = error.response
      switch (status) {
        case 401:
          ElMessage.error('登录已过期')
          localStorage.removeItem('token')
          window.location.href = '/login'
          break
        case 403:
          ElMessage.error('没有权限访问')
          break
        case 404:
          ElMessage.error('请求的资源不存在')
          break
        case 500:
          ElMessage.error('服务器内部错误')
          break
        default:
          ElMessage.error(data?.message || '请求失败')
      }
    } else if (error.code === 'ECONNABORTED') {
      ElMessage.error('请求超时,请稍后重试')
    } else if (error.message.includes('Network Error')) {
      ElMessage.error('网络连接异常,请检查网络')
    } else {
      ElMessage.error(error.message || '未知错误')
    }
    
    return Promise.reject(error)
  }
)

// 封装请求方法
export function get(url, params, config = {}) {
  return service.get(url, { params, ...config })
}

export function post(url, data, config = {}) {
  return service.post(url, data, config)
}

export function put(url, data, config = {}) {
  return service.put(url, data, config)
}

export function del(url, params, config = {}) {
  return service.delete(url, { params, ...config })
}

export default service

六、面试高频问题

Q1:为什么要封装 axios?

答:

  1. 统一配置 baseURL、timeout

  2. 统一处理 token、请求头

  3. 统一处理错误、loading

  4. 简化调用,提高代码复用性

Q2:请求拦截器和响应拦截器分别做什么?

答:

  • 请求拦截器:发请求前加 token、加 loading、防重复请求

  • 响应拦截器:统一处理状态码、错误提示、关闭 loading、数据解构

Q3:token 过期怎么处理?

答:

  1. 响应拦截器判断 401 状态码

  2. 清除本地 token

  3. 提示用户重新登录

  4. 跳转到登录页


七、快速记忆

复制代码
axios 封装三板斧:

1. create 创建实例
   baseURL + timeout

2. 请求拦截器
   加 token + 开 loading

3. 响应拦截器
   判状态 + 关 loading + 解数据

最后封装 get/post/put/delete
页面调用传业务参数即可
相关推荐
斌味代码2 小时前
SpringBoot 实战总结:踩坑与解决方案全记录
java·spring boot·后端
im_AMBER2 小时前
Leetcode 156 旋转图像 | 矩阵置零
javascript·数据结构·算法·leetcode
Highcharts.js2 小时前
在 Next.js App Router 中使用 Highcharts Stock(完整实战指南 )
开发语言·javascript·ecmascript
摇滚侠2 小时前
Groovy 中如何定义集合
java·开发语言·python
0xDevNull2 小时前
Spring Boot 3.0动态多数据源切换实战教程
java·spring boot·后端
代码漫谈2 小时前
微服务 vs 单体架构:架构选型、实战拆解与决策指南
java·微服务·springboot·springcloud
神龙斗士2402 小时前
第一个Spring Boot程序
java·spring boot·java-ee·tomcat
gelald2 小时前
Spring Boot - 配置加载
java·spring boot·后端·spring
中国胖子风清扬2 小时前
基于GPUI框架构建现代化待办事项应用:从架构设计到业务落地
java·spring boot·macos·小程序·rust·uni-app·web app