配置项目请求地址和axios以及实现token过期无痛刷新

时间:2023-02-24 12:52:35

配置请求地址:config->index.js

一个项目里通常有一个config->index.js,该文件包含了当前项目的请求地址,以及项目的版本信息。

// 请求地址
const API_URL_DEV = 'http://xxx.xxx.xxx.net:81/xxx' // 测试接口
const API_URL_TRIAL = 'http://xxx.xxx.xxx.net:81/xxx' // 体验版接口
const API_URL_PROD = 'http://xxx.xxx.xxx.net:81/xxx' // 线上接口

上面就是api请求的baseUrl,在后续配置axios的时候会用到,在项目开发中,有时候会切换开发版和体验版,就是更改此处的地址。

微信小程序里需要区分一件事,那就是当前的运行环境,是否是开发环境。
微信小程序官方提供了 wx.getAccountInfoSync()可以获取小程序版本的appid以及小程序的版本信息,根据envVersion来判断当前小程序是线下环境还是线上环境。
配置项目请求地址和axios以及实现token过期无痛刷新

  let {
    miniProgram: {
      envVersion,
      version
    }
  } = wx.getAccountInfoSync()  // 获取小程序版本信息

将获取版本信息的操作封装成一个函数,根据运行环境配置小程序版本信息和apiUrl

function getparamsByEnv() {
  /**
   * 官方提供:wx.getAccountInfoSync()获取小程序版本信息,以及appid
   * 模拟当前环境
   * 线下的是 'develop' 线上是 'prod'
   */

  let {
    miniProgram: {
      envVersion,
      version
    }
  } = wx.getAccountInfoSync()  // 获取小程序版本信息
  
  // 返回的参数
  let params = {
    apiUrl: '',
    version: version,
    tester: true
  }
  
  switch (envVersion) {
    case 'develop':
      params.apiUrl = API_URL_DEV  // 此处对应上面配置的线下baseUrl接口
      params.version = '1.0.2'
      break
    case 'trial':
      params.apiUrl = API_URL_TRIAL // 此处对应上面配置的体验版baseUrl接口
      params.version = '1.0.2'
      break
    default:
      params.apiUrl = API_URL_PROD  // 此处对应上面配置的线上baseUrl接口
      params.tester = false
      break
  }
  return params   // 配置完成后返回params
}

完整的config.js配置

// 请求地址
const API_URL_DEV = 'http://xxx.xxx.xxx.net:81/xxx' // 测试接口
const API_URL_TRIAL = 'http://xxx.xxx.xxx.net:81/xxx' // 体验版接口
const API_URL_PROD = 'http://xxx.xxx.xxx.net:81/xxx' // 线上接口


const envParams = getparamsByEnv()

export default envParams.apiUrl  // 导出apiUrl

export const version = envParams.version  // 导出版本信息

export const tester = envParams.tester  // 导出tester

/**
 * 区分小程序当前运行环境
 * @return {Boolean} 是否是开发环境
 */
function getparamsByEnv() {
  /**
   * 官方提供:wx.getAccountInfoSync()获取小程序版本信息,以及appid
   * 模拟当前环境
   * 线下的是 'develop' 线上是 'prod'
   */

  let {
    miniProgram: {
      envVersion,
      version
    }
  } = wx.getAccountInfoSync()  // 获取小程序版本信息
  let params = {
    apiUrl: '',
    version: version,
    tester: true
  }
  switch (envVersion) {
    case 'develop':
      params.apiUrl = API_URL_DEV
      params.version = '1.0.2'
      break
    case 'trial':
      params.apiUrl = API_URL_TRIAL
      params.version = '1.0.2'
      break
    default:
      params.apiUrl = API_URL_PROD
      params.tester = false
      break
  }
  return params
}

配置request.js

在这个文件其实就是配置axios,需要引入axios以及上面配置的config.js文件

import axios from 'axios'
import mpAdapter from 'axios-miniprogram-adapter'
axios.defaults.adapter = mpAdapter
import API_URL from '../config/index'   // apiUrl

axios有三个配置项,分别是:baseUrl请求拦截器响应拦截器,下面分别就这些配置项进行配置。

配置请求baseUrl
引入config.jsapiUrl,在axiosbaseUrl中配置

const service = axios.create({
  baseURL: API_URL, // url = base url + request url
})

配置请求拦截器service.interceptors.request.use
在请求拦截器内通常会做一些配置,比如:在请求头中携带token,在请求的时候弹出“加载中”告诉用户正在发生什么等等,通常只要发送请求,都会经过请求拦截器。

let ajaxTimes = 0; // 状态
// 请求拦截器
service.interceptors.request.use(function (config) {
    // config就是请求的配置信息,里面包含baseUrl地址,headers请求头,url请求接口等信息
    // 在发送请求前,可以对config做一些配置操作(加盐)
    
    // 获取token
    if(wx.getStorageSync('token')){
        // 如果token存在,那么在请求头上带上token
        config.headers['access_token'] = wx.getStorageSync('token')
    }
    
    //  请求的时候,添加一个弹框,告诉用户正在发生什么
    // config.url != 'xxx/xxx' 请求某个接口不弹出提示
    if (!ajaxTimes++ && config.url != '/lottery/activity/get-prize'){
        wx.showLoading({ title: '加载中···' })  // 其实把wx.showLoading写在请求拦截器里都会生效,这里只是做了一个ajaxTimes的判断,优化了提示弹框
    }
    
    return config;  // 返回配置信息-也就是请求头之类的
    
},function (error){
    // 处理错误请求
    return Promise.reject(error)
})

配置响应拦截器service.interceptors.response.use
只要请求接口,接口响应并返回,都会走响应拦截器,可以在响应拦截器内做一些配置,比如请求日志、token过期处理、账户封禁处理、服务器错误处理、响应成功处理等等。

// 添加响应拦截器
service.interceptors.response.use(function (response) {
  // response返回上方请求拦截器的config配置信息,data后台接口返回的数据,headers服务器响应头等信息
    
  // 由于微信小程序的 toast 和 loading 相关接口可以相互混用,所以需要取消混用提示,也是关联上方的“加载中”优化
  if (ajaxTimes > 0 && --ajaxTimes === 0) {
    wx.hideLoading({noConflict:true})  // // 取消混用提示
  }
  
  // 这里返回的response.data就是后台接口返回的数据
  const res = response.data
  // 如果返回的code不是200或201,则判断为错误
  if(res.code && res.code !== 200 && res.code !== 201){
      // 分别对这些错误进行对应的处理
      
      if(res.code === 1004){
          // 1004状态码表示token过期,需要重新获取token,并在获取token后,重新请求接口,清除token
          return reSetToken(response.config)  // 这里调用获取token的方法,后面会进行配置
          
      } else if(res.code === 9001){
          // 返回9001状态码处理,通过wx.login获取code
          if (res.message.indexOf('code been used') !== -1) {
            getApp().globalData.userInfo._getLoginCode()
          } else if (res.message.indexOf('invalid code') !== -1) {
            console.log('invalid code')
          }
          return Promise.reject(res)
          
      } else if (res.code === 400 || res.code === 500){
          // 返回400或500状态码,表示服务器错误或者接口请求异常
          wx.showToast({title: `${res.message}`,icon: 'none'})
          return Promise.reject(res)
          
      } else if(res.code == 30011){  // 账户封禁
          // 返回30011状态码,表示该账号已封禁,如果想对封禁的账户做一些操作可以将返回的信息携带并跳转到封禁页,封禁页展示封禁的原有和信息,用户可以在封禁页做封禁申诉等操作
          const info = res.message;
          wx.reLaunch({
            url: `/pages/account/index?info=${info}`,
          })
      }
      
      return Promise.reject(res) // 其他的错误状态码就直接reject
  } else {
      return res
  }   
}, function (error) {
   // 处理响应错误
   if (ajaxTimes > 0 && --ajaxTimes === 0) {
        wx.hideLoading({noConflict:true})
   }
   return Promise.reject(error);
}

token过期后重新获取token,在响应拦截器里说过,返回状态1004表示token过期,在这里封装重新获取token的方法,在响应拦截器状态1004处调用。

let tokenLoad = false
// 重新获取token
function reSetToken(params) {
    // 这里的params就是response.config的信息
    return new Promise(async (resolve,reject) => {
        // 用函数形式将resolve存入,等待刷新后再执行
        const app = getApp()  // 获取小程序全局唯一的 App 实例
        if(!app){
            return console.log("app undefined")
        }
        
        wx.queue.asyncWait("login_back", () => {
          service(params).then(res => {
            resolve(res)
          }, err => {
            reject(err)
          })
        },params.url)
        
        // tokenLoad = true什么都不做,false走重新发起登录的流程
        if(tokenLoad) {
            console.log("中断重新获取token")
        } else {
            // 将tokenLoad状态更改为true
            tokenLoad = true
            try {
                // 重新发起登录,调用发起登录的方法
                app.globalData.userInfo.Login().then(res => {
                    if(res === 'notBindPhone') {
                        app.awaitLoginDialog('request')
                    }
                    tokenLoad = false // 登录成功后将tokenLoad改为false,等待下次token过期重新发起登录
                }).catch(()=>{
                    tokenLoad = false
                })
                
            } catch (error) {
                // 重新获取token报错,说明用户没注册,需要唤醒注册弹窗,如果在tabbar页面,就不自动唤醒
                app.awaitLoginDialog('request')
                tokenLoad = false
                reject(error)
            }
        }
        
    })
}

完整的request.js配置

import axios from 'axios'
import mpAdapter from 'axios-miniprogram-adapter'
axios.defaults.adapter = mpAdapter
import API_URL from '../config/index'
const service = axios.create({
  baseURL: API_URL, // url = base url + request url
})

let ajaxTimes = 0  
// 请求拦截器
service.interceptors.request.use(function (config) {
  // config是请求的配置信息,内部有baseUrl地址,headers请求头,url请求接口等信息
  // console.log('请求拦截器',config); 

  // 发送请求之前你可以在这里对config做一些配置
  // 获取token
  if (wx.getStorageSync('token')) {
    // 如果token存在,那么在请求头上带上token
    config.headers['access_token'] = wx.getStorageSync('token')
  }

  // 请求的时候,添加一个弹框wx.showLoading告诉用户正在加载中
  // !ajaxTimes++是因为请求可能不止一次,请求的时候这种"加载中"的提醒优化,其实只需要显示一次即可,所以需要做一个判断,ajaxTimes == 0的时候提示一次,后续ajaxTimes++都不会提示,每次进新页面或刷新都会重置ajaxTimes == 0 ,这里做的操作其实就是为了优化,减少提示的次数。
  if (!ajaxTimes++ && config.url != '/lottery/activity/get-prize' && config.url != '/lottery/activity/time'){
    wx.showLoading({ title: '加载中···' })  // 其实把wx.showLoading写在请求拦截器里都会生效,这里只是做了一个ajaxTimes的判断,优化了提示弹框
  }

  return config;
}, function (error) {
  // 处理请求错误
  return Promise.reject(error);
});

// 添加一个响应拦截器
service.interceptors.response.use(function (response) {
  // response返回请求拦截器的config配置信息,data后台接口返回的数据,headers服务器响应头等信息
  // console.log('响应拦截器',response);
  if (ajaxTimes > 0 && --ajaxTimes === 0) {
    wx.hideLoading({noConflict:true})
  }

  // 这里返回的response.data就是后台接口返回的数据
  const res = response.data
  console.log('请求日志:',response);  // 请求日志
  // 如果自定义代码不是200或201,则判断为错误.
  if (res.code && res.code !== 200 && res.code !== 201 && res.code !== 2001 && res.code !== 2002) {
    if (res.code === 1004) {
      // 1004状态码表示token过期,这是需要重新获取token,并且在获取完token之后,重新请求接口,清除token
      return reSetToken(response.config)
    } else if (res.code === 9001) {
      if (res.message.indexOf('code been used') !== -1) {
        getApp().globalData.userInfo._getLoginCode()
      } else if (res.message.indexOf('invalid code') !== -1) {
        console.log('invalid code')
      }
      return Promise.reject(res)
    } else if (res.code === 400 || res.code === 500) {
      wx.showToast({title: `${res.message}`,icon: 'none'})
      return Promise.reject(res)
    } else if (res.code === 30011) { // 账户封禁
      const info = res.message;
      // 根据code判断该账户是否封禁,如果封禁,则携带信息跳转到封禁页
      wx.reLaunch({
        url: `/pages/account/index?info=${info}`,
      })
    }
    return Promise.reject(res)
  } else {
    return res
  }
}, function (error) {
  // 处理响应错误
  if (ajaxTimes > 0 && --ajaxTimes === 0) {
    wx.hideLoading({noConflict:true})
  }
  return Promise.reject(error);
});


let tokenLoad = false
// 重新获取token
function reSetToken(params) {
  // console.log('重新获取token:',params);
  return new Promise(async (resolve, reject) => {
    // 用函数形式将 resolve 存入,等待刷新后再执行
    const app = getApp()
    if (!app) {
      console.error("app undefind");
      return
    }
    wx.queue.asyncWait("login_back", () => {
      service(params).then(res => {
        resolve(res)
      }, err => {
        reject(err)
      })
    },params.url)

    // true什么都不做,false走重新发起登录流程
    if (tokenLoad) {
      console.log("中断重新获取token");
    } else {
      // 将tokenLoad状态更改为true
      tokenLoad = true
      try {
        // 重新发起登录
        app.globalData.userInfo.Login().then(res => {
          // console.log('发起登录',res);
          if (res === 'notBindPhone') {
            app.awaitLoginDialog('request')
          }
          tokenLoad = false  // 登录成功后将tokenLoad改为false,等待下次token过期重新发起登录
        }).catch(() => {
          tokenLoad = false
        })
      } catch (error) {
        // 重新获取token报错,说明用户没有注册。需要唤起注册弹窗,如果是在tabbar页面,就不自动唤起
        // console.log(error, '请求token失败报错')
        app.awaitLoginDialog('request')
        tokenLoad = false
        reject(error)
      }
    }
  })


}
export default service

getApp()为微信小程序的全局唯一App实例,用户的登录方法可以挂载到App实例当中,在需要唤醒登录处获取getApp()中挂载的登录方法即可唤醒登录。