一、二次封装axios

通常用vue脚手架搭建的项目包含路由层和视图层,向后端发送请求获取数据的方法会写在视图层(即.vue文件中),但这造成了数据与页面的高耦合,后期维护不便的问题。因此我们将axios进行二次封装,写在request.js中,使每个请求都能直接使用这个封装后的方法,并创建api文件夹,将不同的请求方法按业务进行分模块管理。
首先我们来看一下二次封装需要实现哪些功能。
最基础的,先创建axios实例,配置请求的baseURL和请求超时的时间。根据接口文档配置请求头中的Content-Type。
request.js

import axios from 'axios'
axios.defaults.headers['Content-Type'] = 'application/json;charset=utf-8'
const service = axios.create({
    // axios中请求配置有baseURL选项,表示请求URL公共部分
    baseURL: 'http://localhost:8080',
    // 超时
    timeout: 10000
})

二、请求拦截器

然后,要分别配置请求拦截器响应拦截器,请求拦截器配置的内容会在发送请求前执行,响应拦截器会在请求后执行,这两个拦截器分别接受两个参数,即成功的拦截和失败的拦截。
在请求拦截器中,首先要给每个请求的请求头携带token,传给后端让后端进行身份验证。

service.interceptors.request.use(config => {
  // 是否需要设置 token
  const isToken = (config.headers || {}).isToken === false
  if (getToken() && !isToken) {
    config.headers['Authorization'] = 'Bearer ' + getToken() // 让每个请求携带自定义token
  }
  //注意一定要return config
  return config
},error => {
  console.log(error)
  Promise.reject(error)
})

在用户提交表单等操作时,若用户点击提交了两次,那么就会向后端发两个请求,因此我们要在拦截器里拦截重复请求。一个常用的方法是利用axios的cancelToken,具体如下:
1、定义pending数组,收集请求信息。
2.用axios生成cancel函数和cancelToken。
3.每次axios请求之前判断pending中是否有该请求信息,如果有,则利用axios生成的cancel函数和
cancelToken取消之前请求,再把本次请求信息添加pending。如果没有,则直接添加。
4. 接口返回后,移除pending数组中该请求信息。
但是,这个方法不能解决上述问题,因为取消借口时,有可能是服务器已经响应但还没返回,没法保证在服务器响应前取消。因此,我们将请求的地址、数据和时间存在sessionStorage中,并判断与上次存的内容是否一致且时间间隔小于设定值。
cache.js

const sessionCache = {
  setJSON (key, jsonValue) {
    if (jsonValue != null) {
      this.set(key, JSON.stringify(jsonValue))
    }
  },
  getJSON (key) {
    const value = this.get(key)
    if (value != null) {
      return JSON.parse(value)
    }
  }
 }
export default {
  /**
   * 会话级缓存
   */
  session: sessionCache,

}
import cache from '@/plugins/cache'
if (!isRepeatSubmit && (config.method === 'post' || config.method === 'put')) {
    const requestObj = {
      url: config.url,
      data: typeof config.data === 'object' ? JSON.stringify(config.data) : config.data,
      time: new Date().getTime()
    }
    const sessionObj = cache.session.getJSON('sessionObj')
    if (sessionObj === undefined || sessionObj === null || sessionObj === '') {
      cache.session.setJSON('sessionObj', requestObj)
    } else {
      const s_url = sessionObj.url;                // 请求地址
      const s_data = sessionObj.data;              // 请求数据
      const s_time = sessionObj.time;              // 请求时间
      const interval = 1000;                       // 间隔时间(ms),小于此时间视为重复提交
      if (s_data === requestObj.data && requestObj.time - s_time < interval && s_url === requestObj.url) {
        const message = '数据正在处理,请勿重复提交';
        console.warn(`[${s_url}]: ` + message)
        return Promise.reject(new Error(message))
      } else {
        cache.session.setJSON('sessionObj', requestObj)
      }
    }
  }

三、响应拦截器

响应拦截器中如果响应成功返回,那就可以拿到响应状态码与返回的数据,根据状态码要做一个判断token是否过期的处理。如果token过期了,就弹窗显示是否重新登录,重新登录则跳转回登陆页面。

service.interceptors.response.use(res => {
    // 未设置状态码则默认成功状态
    const code = res.data.code || 200;
    // 获取错误信息
    const msg = errorCode[code] || res.data.msg || errorCode['default']
    // 二进制数据则直接返回
    if(res.request.responseType ===  'blob' || res.request.responseType ===  'arraybuffer'){
      return res.data
    }
    //token过期
    if (code === 401) {
      if (!isRelogin.show) {
        isRelogin.show = true;
        ElMessageBox.confirm('登录状态已过期,您可以继续留在该页面,或者重新登录', '系统提示', {
          confirmButtonText: '重新登录',
          cancelButtonText: '取消',
          type: 'warning'
        }
      ).then(() => {
        isRelogin.show = false;
        store.dispatch('LogOut').then(() => {
          location.href = '/index';
        })
      }).catch(() => {
        isRelogin.show = false;
      });
    }
      return Promise.reject('无效的会话,或者会话已过期,请重新登录。')
    } else if (code === 500) {
      ElMessage({
        message: msg,
        type: 'error'
      })
      return Promise.reject(new Error(msg))
    } else if (code !== 200) {
      ElNotification.error({
        title: msg
      })
      return Promise.reject('error')
    } else {
      return  Promise.resolve(res.data)
    }
  },
  error => {
    console.log('err' + error)
    let { message } = error;
    if (message == "Network Error") {
      message = "后端接口连接异常";
    }
    else if (message.includes("timeout")) {
      message = "系统接口请求超时";
    }
    else if (message.includes("Request failed with status code")) {
      message = "系统接口" + message.substr(message.length - 3) + "异常";
    }
    ElMessage({
      message: message,
      type: 'error',
      duration: 5 * 1000
    })
    return Promise.reject(error)
  }
)

四、登陆登出实现

在api文件夹下新建login.js,用上述封装的request.js实现登陆登出方法

// 登录方法
export function login(username, password, code, uuid) {
  const data = {
    username,
    password,
    code,
    uuid
  }
  return request({
    url: '/login',
    headers: {
      isToken: false
    },
    method: 'post',
    data: data
  })
}
// 退出方法
export function logout() {
  return request({
    url: '/logout',
    method: 'post'
  })
}

然后,我们在store文件夹下新建user.js,用vuex来统一管理token

import { login, logout } from '@/api/login'
import { getToken, setToken, removeToken } from '@/utils/auth'

const user = {
  state: {
    token: getToken()
  },

  mutations: {
    SET_TOKEN: (state, token) => {
      state.token = token
    }
  },

  actions: {
    // 登录
    Login({ commit }, userInfo) {
      const username = userInfo.username.trim()
      const password = userInfo.password
      const code = userInfo.code
      const uuid = userInfo.uuid
      return new Promise((resolve, reject) => {
        login(username, password, code, uuid).then(res => {
          setToken(res.token)
          commit('SET_TOKEN', res.token)
          resolve()
        }).catch(error => {
          reject(error)
        })
      })
    },

    // 退出系统
    LogOut({ commit, state }) {
      return new Promise((resolve, reject) => {
        logout(state.token).then(() => {
          commit('SET_TOKEN', '')
          commit('SET_ROLES', [])
          commit('SET_PERMISSIONS', [])
          removeToken()
          resolve()
        }).catch(error => {
          reject(error)
        })
      })
    }
  }
}

export default user

@/utils/auth如下,即将token存在cookie中

import Cookies from 'js-cookie'

const TokenKey = 'Admin-Token'

export function getToken() {
  return Cookies.get(TokenKey)
}

export function setToken(token) {
  return Cookies.set(TokenKey, token)
}

export function removeToken() {
  return Cookies.remove(TokenKey)
}

这样我们就可以在登陆页面login.vue中调用action的登录方法,并在成功的回调中跳转页面到主页。登出同理。

五、登陆鉴权实现

首先在项目文件夹下定义permission.js,通过路由守卫判断要跳转的页面。如果要跳转到主页,则要获取用户的权限,然后根据权限生成动态路由表。
permission.js

router.beforeEach((to, from, next) => {
  NProgress.start()
  if (getToken()) {
    to.meta.title && store.dispatch('settings/setTitle', to.meta.title)
    /* has token*/
    if (to.path === '/login') {
      next({ path: '/' })
      NProgress.done()
    } else {
      if (store.getters.roles.length === 0) {
        isRelogin.show = true
        // 判断当前用户是否已拉取完user_info信息
        store.dispatch('GetInfo').then(() => {
          isRelogin.show = false
          store.dispatch('GenerateRoutes').then(accessRoutes => {
            // 根据roles权限生成可访问的路由表
            accessRoutes.forEach(route => {
              if (!isHttp(route.path)) {
                router.addRoute(route) // 动态添加可访问路由表
              }
            })
            next({ ...to, replace: true }) // hack方法 确保addRoutes已完成
          })
        }).catch(err => {
          store.dispatch('LogOut').then(() => {
            ElMessage.error(err)
            next({ path: '/' })
          })
        })
      } else {
        next()
      }
    }
  } else {
    // 没有token
    if (whiteList.indexOf(to.path) !== -1) {
      // 在免登录白名单,直接进入
      next()
    } else {
      next(`/login?redirect=${to.fullPath}`) // 否则全部重定向到登录页
      NProgress.done()
    }
  }
})

生成动态路由的方法主要在store/modules/permission.js中的GenerateRoutes方法。主要逻辑就是向后端请求路由,后端根据token会直接返回该用户权限对应的路由。我们要做的是首先对路由进行过滤,通过懒加载的形式根据路由导入模块。由于存在多级子路由的情况,就要递归调用这个过滤函数。处理完后返回处理后的路由,再一一通过router.addRoute动态添加到可访问路由表中。

Logo

前往低代码交流专区

更多推荐