封装axios(包含CancelToken源码解析)

2,122 阅读6分钟

目标

封装的意义在于使用的时候尽量方便。

此次封装的目标,是使用尽量简单的配置完成接口的请求。

思来想去,决定使用以下配置:

// 配置介绍
[调用方法名]:{
    type: '', // 请求方式
    url: '', // 请求url
    autoCancel: Boolean, // 重复频繁请求时是否取消上次请求
    headers: {}, // 请求头配置
    ... // axios支持的其他配置
}
// 例如
const api = {
  getUserInfo: {
    type: 'get',
    url: '/common/xxx'
  },
  saveSomething: {
    type: 'post',
    url: '/common/xxx',
    autoCancel: true
    headers: {
      'Content-Type': 'application/x-www-form-urlencoded'
    }
  }
}

使用的时候,直接这样调用即可:

import api from 'api'

api.getUserInfo(data).then(() => {
    // ...
})

开始封装

其中 1. 请求及返回拦截 2. 处理异常 这两块内容基本都大同小异,简单快速浏览即可

api/axios.js里部分代码借鉴自掘友,由于时间较长找不见原文章,如有知道的可以联系添加原文链接

请求及返回拦截

// api/axios.js

// 创建axios实例
let instance = axios.create({timeout: 1000 * 20})
// 设置post请求头
instance.defaults.headers.post['Content-Type'] = 'application/x-www-form-urlencoded'
/**
 * 请求拦截器
 * 每次请求前,如果存在token则在请求头中携带token
 */
instance.interceptors.request.use(
  config => {
    // 登录流程控制中,根据本地是否存在token判断用户的登录情况
    // 但是即使token存在,也有可能token是过期的,所以在每次的请求头中携带token
    // 后台根据携带的token判断用户的登录情况,并返回给我们对应的状态码
    // 而后我们可以在响应拦截器中,根据状态码进行一些统一的操作。
    // const token = store.state.token
    // token && (config.headers.Authorization = token)
    if (config.data && config.headers && config.headers['Content-Type'] === 'application/x-www-form-urlencoded') {
      config.data = qs.stringify(config.data, { allowDots: true })
    }
    return config
  },
  error => Promise.error(error)
)

// 响应拦截器
instance.interceptors.response.use(
  // 请求成功
  res => res.status === 200 ? Promise.resolve(res) : Promise.reject(res),
  // 请求失败
  error => {
    const { response } = error
    if (response) {
      // 请求已发出,但是不在2xx的范围
      errorHandle(response.status, response.data.message)
      return Promise.reject(response)
    } else {
      // 处理断网的情况
      // eg:请求超时或断网时,更新state的network状态
      // network状态在app.vue中控制着一个全局的断网提示组件的显示隐藏
      // 关于断网组件中的刷新重新获取数据,会在断网组件中说明
      if (!window.navigator.onLine) {
        store.commit('changeNetwork', false)
      } else {
        return Promise.reject(error)
      }
    }
  }
)

处理异常

// api/axios.js

/**
 * 请求失败后的错误统一处理
 * @param {Number} status 请求失败的状态码
 */
const errorHandle = (status, other) => {
  // 状态码判断
  switch (status) {
    // 401: 未登录状态,跳转登录页
    case 401:
      toLogin()
      break
    // 403 token过期
    // 清除token并跳转登录页
    case 403:
      tip('登录过期,请重新登录')
      localStorage.removeItem('token')
      store.commit('loginSuccess', null)
      setTimeout(() => {
        toLogin()
      }, 1000)
      break
    // 404请求不存在
    case 404:
      tip('请求的资源不存在')
      break
    default:
      console.log(other)
  }
}

其中的tip()是全局提示的方法,如下:

// api/axios.js

import { Toast } from 'vant'

/**
 * 提示函数
 * 禁止点击蒙层、显示一秒后关闭
 */
const tip = msg => {
  Toast({
    message: msg,
    duration: 1000,
    forbidClick: true
  })
}

对外暴露方法

// api/axios.js

/**
 * 请求数据的方法,所有请求都调用此方法
 * @param {string} type 请求方法,默认为get
 * @param {string} url 请求url
 * @param {object} data post时的数据
 * @param {object} params get时的数据
 * @param {object} headers 请求头配置
 * @param {object} config 其他axios配置
 * @param {boolean} autoCancel 是否自动取消请求(取消重复请求)
 * @returns {Promise<any>}
 */
function request ({type = 'get', url, data, params, headers, config = {}, autoCancel}) {
  type = type.toLowerCase()
  // get请求时,如果params没值,则取data(防止用户使用参数错误)
  if (type === 'get') {
    params = params || data
    data = undefined
  }
  if (autoCancel) {
    let cc = window.cancelApiMap[url]
    cc && cc.cancel('cancel repeat request') // 如果已有则取消
    let newcc = axios.CancelToken.source()
    window.cancelApiMap[url] = newcc
    config.cancelToken = newcc.token // 重新添加cancelToken标志
  }
  return new Promise((resolve, reject) => {
    instance({
      method: type,
      url,
      data,
      params,
      headers,
      ...config
    }).then(({data}) => {
      // 请求成功,删除cancel标记
      window.cancelApiMap[url] && (delete window.cancelApiMap[url]) 
      
      resolve(data)
      // data.success ? resolve(data) : reject(data)
      // if (data.success) {
      //   resolve(data)
      // } else {
      //   reject(data)
      // }
    }).catch(error => {
      reject(error)
    })
  })
}

export default request

支持取消页面内全部请求

有时候,切换页面时我们需要清空当前页面内还未完成的请求,添加以下代码即可

// api/axios.js

// 设一个开关,开启或关闭取消页面内全部请求的功能
const API_CANCEL_PAGE = true

// 在请求拦截器里添加
if (API_CANCEL_PAGE) {
  let newcc = axios.CancelToken.source()
  window.cancelApiMap[config.url] = newcc
  config.cancelToken = newcc.token
}

// 导出一个方法
// 遍历我们上边存储cancelToken信息的对象,分别调用cancel方法,再将对象清空
export function cancelAllApi () {
  Object.values(window.cancelApiMap).forEach(api => {
    api.cancel ? api.cancel('cancel all api') : api('cancel all api')
  })
  window.cancelApiMap = {}
}

引入接口配置文件

我们项目中一般都是分模块进行编写接口的,这里对模块接口配置文件进行规范化, 统一使用[模块名].api.js的写法,方便后续的配置。如:common.api.jshome.api.js等, 下边给出一个例子。

// api/home.api,js
export default {
  getTime: {
    type: 'get',
    url: '/common/get_time'
  },
  getXkMode: {
    type: 'post',
    url: '/common/get_xk_mode.do',
    autoCancel: true
    // headers: {
    //   'Content-Type': 'application/x-www-form-urlencoded'
    // }
  }
}

引入的时候,使用webpack提供的require.context()方法直接引入, 避免每次增删接口配置文件时手动增删引入。

// api/index.js

let apiObj = {}

function dealFileName (str, replaceStr) {
  return str.slice(2).replace(replaceStr, '')
}

/**
 * 导入定义的接口配置
 * @param files
 */
function importFile (files) {
  files.keys().forEach(name => {
    apiObj[dealFileName(name, '.api.js')] = files(name).default
  })
}

//require.context的使用方法此处不做详细介绍
importFile(require.context('.', true, /\.api\.js$/))

此时apiObj的内容如下:

apiObj: {
    common: {
        getUserInfo: {
            type: 'get',
            url: 'xxx'
        }
    },
    home: {
        getCommonInfo: {
            type: 'get',
            utl: 'xxx'
        }
    }
}

将配置转化为方法

现在我们已经拿到了所有的接口配置文件,现在需要做的就是将配置转化为对应的接口请求方法。 处理方法如下:

// api/index.js

import instance from './axios'

let apiMap = {}

Object.keys(apiObj).forEach(key => {
  apiMap[key] = {}
  Object.keys(apiObj[key]).forEach(api => {
    apiMap[key][api] = function (data) {
      let query = {
        [apiObj[key][api].type === 'post' ? 'data' : 'params']: data
      }
      return instance({...apiObj[key][api], ...query})
    }
  })
})

此时apiMap结构如下:

apiMap: {
    common: {
        getUserInfo: function
    },
    home: {
        getCommonInfo: function
    }
}

最后将apiMap导出即可使用

使用

// src/xxx.vue

import api from '@/api'

api.common.getUserInfo(data).then(() => {
    // ...
})

以上为axios的封装,接下来我们看看CancelToken内部是怎么实现的。

CancelToken源码解析

cancelToken基本使用方法

 axios.get('/api/app/course',{
   params: params,
   cancelToken: new CancelToken(function executor(c) {
        // cancel函数赋值给cancelRequest属性
        // 从而可以通过cancelRequest执行取消请求的操作
        that.cancelRequest = c
    }) 
  })
  
  // 当需要取消请求的时候,调用cancelRequest即可
  that.cancelRequest()

接下来我们一起看看源码

// axios/lib/cancel/CancelToken.js 源码

function CancelToken(executor) {
  if (typeof executor !== 'function') {
    throw new TypeError('executor must be a function.');
  }

  var resolvePromise;
  // 1. 关键代码,创建Promise对象,将resolve权限交给外部的resolvePromise
  this.promise = new Promise(function promiseExecutor(resolve) {
    resolvePromise = resolve;
  });

  var token = this;
  // 2. 将cancel方法当做参数传给回调函数,回调函数接收cancel方法
  // 假设回调函数用cancelRequest字段接收cancel,cancelRequest()即执行了cancel方法
  // cancel接收一个message参数,作为描述信息
  executor(function cancel(message) {
    if (token.reason) {
      // Cancellation has already been requested
      return;
    }

    token.reason = new Cancel(message);
    // 3. 执行resolvePromise 将1处的promise状态置为resolve
    // 触发promise.then  转到下方的4处
    resolvePromise(token.reason);
  });
}
// axios/lib/adapters/xhr.js 源码

// ...
if (config.cancelToken) {
  // Handle cancellation
  // 4. 调用cancel会触发这里的onCanceled方法
  config.cancelToken.promise.then(function onCanceled(cancel) {
    if (!request) {
      return;
    }

    // 调用xmlhttprequest的abort方法取消请求
    request.abort();
    // 将axios请求的状态置为reject
    // cancel是上边3处的token.reason
    reject(cancel);
    // Clean up request
    request = null;
  });
}
// ...

关键部分在代码里做了注释,我们再看看下边的代码,可以帮助理解第1处的代码。

let resolveHandle;
new Promise((resolve)=>{
    resolveHandle=resolve;
}).then((val)=>{
    console.log('resolve',val);
});

resolve交给外部变量resolveHandle,当执行resolveHandle()即将promise对象的状态变为resolve,然后触发then的回调

// 执行
resolveHandle('ok');

// 打印
// resolve ok

以上即为CancelToken的基本原理,多看几遍理解更清晰

CancelToken还给我们提供了一个source方法

// axios/lib/cancel/CancelToken.js 源码

CancelToken.source = function source() {
  var cancel;
  var token = new CancelToken(function executor(c) {
    cancel = c;
  });
  return {
    token: token,
    cancel: cancel
  };
};

将cancel方法和token放在一个对象里返给我们

let cancelObj = axios.CancelToken.token.source()

// 这么使用
axios.get('/api/app/course',{
   params: params,
   cancelToken: cancelObj.token
})
cancelObj.cancel() //取消请求

上边封装里用的就是此方法。

以上。