后台管理系统-axios网络请求的封装

此博客是针对开源项目:vue3-element-admin 的学习记录,为了帮助自己理清开发这个系统的逻辑.

安装依赖

javascript 复制代码
npm install axios , qs

Axios实例封装

javascript 复制代码
// 创建 axios 实例 ,同时给出一些预设配置,比如baseURL,超时时间等等
const service = axios.create({
  baseURL: import.meta.env.VITE_APP_BASE_API,
  timeout: 10000,
  headers: { 'Content-Type': 'application/json;charset=utf-8' },
  //序列化`params`
  paramsSerializer: (params) => qs.stringify(params),
})

其中 paramsSerializer: (params) => qs.stringify(params)是把发送网络请求时传递的params参数序列化为url查询字符串,拼接在URL之后,发送网络请求。

请求拦截器

javascript 复制代码
// 请求拦截器,如果有token,就添加
service.interceptors.request.use(
  (config: InternalAxiosRequestConfig) => {
    const accessToken = getToken() //获取访问token
    if (config.headers.Authorization !== 'no-auth' && accessToken) {
      config.headers.Authorization = accessToken
    }
    else {
      delete config.headers.Authorization
    }
    return config
  },
  (error) => {
    return Promise.reject(error)
  },
)

请求拦截器,在我们的网络请求发送之前,拦截它们,这里我们需要加上token,有些页面是必须有token才能访问的,所以我们这里统一拦截,统一添加。

其中
const accessToken = getToken()

是在获取访问token:accessToken
getToken()的代码如下:

javascript 复制代码
//获取到的token格式如  Bearer eyJhbGciOiJIUzI1NiJ9.xxx.xxx 
function getToken(): string {
  return localStorage.getItem(ACCESS_TOKEN_KEY) || ''
}

设置token

javascript 复制代码
if (config.headers.Authorization !== 'no-auth' && accessToken) {
//把获取到的token设置上
  config.headers.Authorization = accessToken
}
else {
  delete config.headers.Authorization
}

这段代码的目的是在请求发送之前判断是否需要携带 Token。如果请求头 Authorizationno-auth,或者没有设置请求头 Authorization字段,则不会添加 Token,即删除该字段。

反之则会根据 accessToken 动态地设置 Authorization 头。

在进行登录时,会调用登录方法,成功登录之后,会调用setToken的方法,保存tokenlocalStorage中:

javascript 复制代码
  // 登录操作,这是首先在登录.vue组件中调用的方法
  function login(loginData: LoginData) {
    return new Promise((resolve, reject) => {
      AuthAPI.login(loginData)
        .then((data) => {
          const { tokenType, accessToken, refreshToken } = data
          
          // 这里在成功登录以后,保存token,这样之后的网络请求都可以获取到token。
          setToken(tokenType + ' ' + accessToken) // Bearer eyJhbGciOiJIUzI1NiJ9.xxx.xxx,存储到localStorage中
          
          setRefreshToken(refreshToken)
          resolve(data)
        })
        .catch((err) => {
          reject(err)
        })
    })
  }

响应拦截器

javascript 复制代码
// 响应拦截器
service.interceptors.response.use(
  (response: AxiosResponse) => {
    // 如果响应是二进制流,则直接返回,用于下载文件、Excel 导出等
    if (response.config.responseType === "blob") {
      return response;
    }

    const { code, data, msg } = response.data;
    if (code === ResultEnum.SUCCESS) {
      console.log("response SUCCESS", response);
      return data;
    }

    ElMessage.error(msg || "系统出错");
    return Promise.reject(new Error(msg || "Error"));
  },
  async (error: any) => {
    // 非 2xx 状态码处理 401、403、500 等
    const { config, response } = error;
    if (response) {
      const { code, msg } = response.data;
      if (code === ResultEnum.ACCESS_TOKEN_INVALID) {
        // Token 过期,刷新 Token
        return handleTokenRefresh(config);
      } else if (code === ResultEnum.REFRESH_TOKEN_INVALID) {
        return Promise.reject(new Error(msg || "Error"));
      } else {
        ElMessage.error(msg || "系统出错");
      }
    }
    return Promise.reject(error.message);
  }
);

service.interceptors.response.use( onFulfilled ,onRejected ) 函数接收两个函数作为参数。

onFulfilled函数是响应成功时执行的回调:所谓响应成功是当 axios 收到一个 2xx 状态码 的响应时,它会认为响应成功,并进入 onFulfilled 函数。此时的 response 对象中包含了以下内容:

response.data:服务器返回的响应数据(通常是你需要的内容)。
response.status:响应的 HTTP状态码,如 200, 201 等。
response.statusText:状态信息文本,如 "OK","Created" 等。
response.headers:响应头信息。
response.config:原始请求配置对象。

axios收到一个 非 2xx 状态码 的响应时,或者请求失败(如网络错误、超时等),它会进入 onRejected 函数。在 onRejected 函数中,错误对象 error 会包含以下内容:

error.response: 当请求得到了响应但状态码不在 2xx 范围时(如 4xx 或 5xx 错误)才存在的属性。它包含了来自服务器的响应信息。
error.config:包含了请求时的配置信息

等等...

对于请求失败的情况,我们获取到 const { config, response } = error;

通过response.data获取到服务器返回的错误消息或错误数据,比如codemsg等信息。

如果code代表访问token失效,则需要重新获取一次访问token,这里调用handleTokenRefresh()函数

如果code代表刷新token失效,中止请求流程。

这个系统里的分为refreshTokenaccessToken,通常refreshToken的有效期是更久的,同时每次accessToken更新时也会把refreshToken一起更新,正常来说,基本不会同时出现两个Token同时过期(根据其它代码的推测)。

现在重点关注handleTokenRefresh函数,相关代码如下:

javascript 复制代码
// 刷新 Token 的锁
let isRefreshing = false;
// 因 Token 过期导致失败的请求队列
let requestsQueue: Array<() => void> = [];

// 刷新 Token 处理
async function handleTokenRefresh(config: InternalAxiosRequestConfig) {
  return new Promise((resolve) => {
    const requestCallback = () => {
      config.headers.Authorization = getToken();
      resolve(service(config));
    };

    requestsQueue.push(requestCallback);

    if (!isRefreshing) {
      isRefreshing = true;

      // 刷新 Token
      useUserStoreHook()
        .refreshToken()
        .then(() => {
          // Token 刷新成功,执行请求队列
          requestsQueue.forEach((callback) => callback());
          requestsQueue = [];
        })
        .catch((error) => {
          console.log("handleTokenRefresh error", error);
          // Token 刷新失败,清除用户数据并跳转到登录
          ElNotification({
            title: "提示",
            message: "您的会话已过期,请重新登录",
            type: "info",
          });
          useUserStoreHook()
            .clearUserData()
            .then(() => {
              router.push("/login");
            });
        })
        .finally(() => {
          isRefreshing = false;
        });
    }
  });
}

首先有一个刷新 Token 的锁isRefreshing ,因为可能多个网络请求再发送时,token过期了,这时候需要更新token,但是我们只需要更新一次就可以了,所有有一个锁。对于后续的网络请求,发现当前正在更新token时,就不用再次更新token了。

首先定义了一个网络请求的回调函数:

javascript 复制代码
const requestCallback = () => {
      config.headers.Authorization = getToken();
      resolve(service(config));
    };
//推入数组中
requestsQueue.push(requestCallback);

这段代码是为了保存那些再token失效时发送的网络请求,等到后面刷新token以后,在统一调用。
service(config):是使用之前创建的axios实例对象,config则是本次请求的配置信息,因为token失效,axios请求失败,通过error对象获取到本次的configresponse

javascript 复制代码
 const { config, response } = error;

之后:

javascript 复制代码
//如果当前没有进行更新token
    if (!isRefreshing) {
    //则进行更新token,同时上锁
      isRefreshing = true;

      // 刷新 Token,调用相关的API更新
      useUserStoreHook()
        .refreshToken()
        .then(() => {
          // Token 刷新成功,执行请求队列,即重新发送之前因为token失效而失败的网络请求
          requestsQueue.forEach((callback) => callback());
          requestsQueue = [];
        })
        //如果更新token出错,直接清除数据,跳转到登录页.
        .catch((error) => {
          console.log("handleTokenRefresh error", error);
          // Token 刷新失败,清除用户数据并跳转到登录
          ElNotification({
            title: "提示",
            message: "您的会话已过期,请重新登录",
            type: "info",
          });
          useUserStoreHook()
            .clearUserData()
            .then(() => {
              router.push("/login");
            });
        })
        .finally(() => {
          isRefreshing = false;
        });
    }

完整代码

javascript 复制代码
import axios, { type InternalAxiosRequestConfig, type AxiosResponse } from 'axios'
import qs from 'qs'
import { ElNotification } from 'element-plus'
import { useUserStore } from '@/stores/modules/user' //用户信息的store
import { getToken } from '@/utils/auth'
import { ResultEnum } from '@/enums/ResultEnum'
import router from '@/router'

// 创建 axios 实例 ,针对网络请求的封装
const service = axios.create({
  baseURL: import.meta.env.VITE_APP_BASE_API,
  timeout: 10000,
  headers: { 'Content-Type': 'application/json;charset=utf-8' },
  paramsSerializer: (params) => qs.stringify(params),
})

// 请求拦截器,如果有token,就添加
service.interceptors.request.use(
  (config: InternalAxiosRequestConfig) => {
    const accessToken = getToken() //获取访问token
    if (config.headers.Authorization !== 'no-auth' && accessToken) {
      config.headers.Authorization = accessToken
    }
    else {
      delete config.headers.Authorization
    }
    return config
  },
  (error) => {
    return Promise.reject(error)
  },
)

// 响应拦截器
service.interceptors.response.use(
  (response: AxiosResponse) => {
    // 如果响应是二进制流,则直接返回,用于下载文件、Excel 导出等
    // 这种类型不能return response.data
    if (response.config.responseType === 'blob') {
      return response
    }
    // json数据,成功状态
    const { code, data, msg } = response.data
    if (code === ResultEnum.SUCCESS) {
      return data
    }

    // @ts-ignore
    ElMessage.error(msg || '系统出错')
    return Promise.reject(new Error(msg || 'Error'))
  },
  // 响应错误拦截器,处理不同的错误状态
  (error: any) => {
    // 非 2xx 状态码处理 401、403、500 等
    const { config, response } = error
    if (response) {
      const { code, msg } = response.data

      // 访问令牌无效或过期,重新获取一次,但是可能刷新令牌也过期
      if (code === ResultEnum.ACCESS_TOKEN_INVALID) {
        console.log('token过期,重新获取')
        return handleTokenRefresh(config)
        // return 'AccessTokenInvalid'
      }
      // 刷新令牌无效或过期,应该是跳转登录页面
      else if (code === ResultEnum.REFRESH_TOKEN_INVALID) {
        return Promise.reject(new Error(msg || 'Error'))
      } else {
        // @ts-ignore
        ElMessage.error(msg || '系统出错')
      }
    }
    return Promise.reject(error.message)
  },
)

// 导出 axios 实例
export default service

// 刷新 Token 的锁,表示是否正在刷新token
let isRefreshing = false
// 因 Token 过期导致失败的请求队列
let requestsQueue: Array<() => void> = []

async function handleTokenRefresh(config: InternalAxiosRequestConfig) {
  return new Promise((resolve) => {
    const requestCallback = () => {
      config.headers.Authorization = getToken()
      resolve(service(config))
    }
    requestsQueue.push(requestCallback)

    if (!isRefreshing) {
      isRefreshing = true

      // 刷新 Token
      useUserStore()
        .refreshToken()
        .then(() => {
          // Token 刷新成功,执行请求队列
          requestsQueue.forEach((callback) => callback())
          requestsQueue = []
        })
        .catch((error: any) => {
          console.log('handleTokenRefresh error', error)
          // Token 刷新失败,清除用户数据并跳转到登录
          ElNotification({
            title: '提示',
            message: '您的会话已过期,请重新登录',
            type: 'info',
          })
          useUserStore()
            .clearUserData()
            .then(() => {
              router.push('/login')
            })
        })
        .finally(() => {
          isRefreshing = false
        })
    }
  })
}
相关推荐
肖老师xy3 分钟前
h5使用better scroll实现左右列表联动
前端·javascript·html
一路向北North7 分钟前
关于easyui select多选下拉框重置后多余显示了逗号
前端·javascript·easyui
Libby博仙10 分钟前
.net core 为什么使用 null!
javascript·c#·asp.net·.netcore
一水鉴天10 分钟前
为AI聊天工具添加一个知识系统 之26 资源存储库和资源管理器
前端·javascript·easyui
浩浩测试一下14 分钟前
Web渗透测试之XSS跨站脚本 防御[WAF]绕过手法
前端·web安全·网络安全·系统安全·xss·安全架构
hvinsion15 分钟前
HTML 迷宫游戏
前端·游戏·html
m0_6724496019 分钟前
springmvc前端传参,后端接收
java·前端·spring
万物得其道者成29 分钟前
在高德地图上加载3DTilesLayer图层模型/天地瓦片
前端·javascript·3d
跳跳的向阳花36 分钟前
06、Docker学习,常用安装:Zookeeper、ES、Minio
学习·docker·zookeeper
码农君莫笑1 小时前
Blazor用户身份验证状态详解
服务器·前端·microsoft·c#·asp.net