实现一个axios(二)

789 阅读6分钟

回顾

上一篇文章我们实现了axios发送请求和错误处理的功能,今天来实现一下axios用的比较多的2个功能,拦截器和取消请求。

拦截器

拦截器的用途:

axios的拦截器是一个很有用的功能,通常我们在真正发送请求之前可能会执行一些逻辑,比如:

  • 在用JWT做登录的时候,前端通常会在请求中添加一个Authorization的的Header,Authorization值为一个 Beaere token,有些请求需要带上token,有些请求可能不需要token(一些公开的接口),这种场景下拦截器就派上用场了。
  • 在用JWT做登录的时候,通常token我们会设置一个过期时间(比如:2个小时过期),过期了以后会重新获取一个新的token实现无感知登录,这个逻辑也可以放到拦截器里做。

API回顾

拦截器分为请求拦截器和响应拦截器,请求拦截器就是在发送请求前做一些处理,响应拦截器就是接收到请求后做一些处理。

请求拦截器的用法如下:

axios.interceptors.request.use(function(config){},function(error){})

响应拦截器的用法如下:

axios.interceptors.response.use(function(response){},function(err){})

use方法接收2个回调函数,类似于promise的then中的2个参数,请求拦截器中,use方法的第一个参数是config对象,返回值是一个config对象。而在响应拦截器中,use方法的第一个参数是响应的对象,返回值是一个response的对象(在上一篇文章中已经分析过这个对象了)

实现原理

如果对promise的比较熟悉,那么拦截器的实现也不算难。我们先新建个src/core/InsterceptorMananger.js文件:

export default class InterceptorManager {
  interceptors = []
  use(resolve, reject) {
    this.interceptors.push({ resolve, reject })

    return this.interceptors.length - 1
  }
  eject(id) {
    if (this.interceptors[id]) {
      this.interceptors[id] = null
    }
  }
  forEach(fn) {
    this.interceptors.forEach(interceptor => {
      if (interceptor) {
        fn(interceptor)
      }
    })
  }
}

需要注意:

  • 调用use方法,我们就是把传进来的2个回调函数push到一个数组里,use返回一个数组的下标,用这个下标可以删除拦截器。
  • eject方法就是删除一个拦截器。
  • forEach对外提供一个接口,外面可以调用这个forEach实现遍历数组的功能,这也是迭代器设计模式的一个应用。

然后修改/src/core/Axios.js:

constructor() {
  this.interceptors = {
    request: new IneterceptorManager(),
    response: new IneterceptorManager()
  }
}
request(config) {
  const promiseChain = [
    {
      resolve: dispatchRequest,
      reject: undefined
    }
  ]
  this.interceptors.request.forEach(interceptor => {
    promiseChain.unshift(interceptor)
  })
  this.interceptors.response.forEach(interceptor => {
    promiseChain.push(interceptor)
  })
  let promise = Promise.resolve(config)
  while (promiseChain.length) {
    const { resolve, reject } = promiseChain.shift()
    promise = promise.then(resolve, reject)
  }

  return promise
}

对于请求拦截器,先添加的最后被执行,后添加的先被执行。

对于响应拦截器,先添加的先被执行,后添加的后被执行。

在Axios类的构造器我们初始化了interceptors对象的request和response。在我们的核心方法reqeust中:我们定义了一个promiseChain,默认情况下只有dispatchRequest方法,但是当用户一但调用了axios.interceptors.request.use添加了多个请求拦截器后,我们把它放到promiseChain数组的头部,用户一旦调用过了axios.interceptors.response.use添加了多个响应拦截器后,我们把它放到promiseChain数组的尾部,然后定义个初始的config,不断的经过每个请求拦截器的处理(每个拦截器会修改config),最终给了dispatchRequest去发送请求。请求回来后,又会把reponse作为参数经过每个响应拦截器的处理,从而完成一次请求。

取消请求

取消请求的适用场景:

  • 我们有个按钮,点击的时候会发送ajax请求,为了避免重复的请求,我们可以在每次发送请求前把上次的请求取消掉
  • 当我们在做模糊搜索的时候,输入都会向后端发起请求,比如发送了请求1和请求2,但是由于网络原因,请求2先响应,请求1后响应,那么请求2的响应数据会覆盖请求1的的,导致的结果就是我们模糊搜索框里输入的是请求2的关键字,而实际响应的结果是请求1的。

API回顾

axios取消请求有2种用法,分别用代码来说一下:

第一种:

const CancelToken = axios.CancelToken
let cancel

axios
  .get("/xxx", {
    cancelToken: new CancelToken(c => {
      cancel = c
    })
  })
cancel("取消原因")

第二种:

const CancelToken = axios.CancelToken
const source = CancelToken.source()
axios
  .get("/xxx", {
    cancelToken: source.token
  })
source.cancel('取消原因')

二种用法其实本质都一样,第一种用法是使用者自己用new构造了一个CancelToken对象,然后定义一个变量接收了取消函数,然后调用取消函数。第二种方法是CancelToken.token()生成了CancelToken实例和取消函数,使用这直接用就可以了。

原理分析

由于是使用者 调用取消函数 来手动取消请求,一旦响应结束用户再调用取消函数其实就不应该起作用(状态只能改变一次),所以想到用promise来做,新建src/cancel/Cancel.js 和 src/cancel/CancelToken.js :

export default class Cancel {
  message
  constructor(message) {
    this.message = message
  }
}

export function isCancel(val) {
  return val instanceof Cancel
}

对于取消的原因这里新建Cancel类 而不是 直接使用一个字符串表示错误原因是为了后续异常的判断,让用户知道发生异常是取消导致的还是其他异常原因(超时、网络异常还是4xx、5xx),类实例的话可以用instanceof来判断。上面的isCancel函数就是用于判断是不是cancel类型的异常。

import Cancel from "./Cancel"

export default class CancelToken {
  promise
  reason
  constructor(executor) {
    let resolve
    this.promise = new Promise(r => {
      resolve = r
    })
    executor(message => {
      if (this.reason) return
      this.reason = new Cancel(message)
      resolve(this.reason)
    })
  }
  static source() {
    let cancel
    const token = new CancelToken(r => {
      cancel = r
    })

    return {
      token,
      cancel
    }
  }
  throwIfRequested() {
    if (this.reason) {
      throw this.reason
    }
  }
}

上面的代码并不难理解,需要提醒一点是throwIfRequested这个方法,产生的CancelToken实例一旦被使用过,也就是调用过了取消函数,那么就不能再次用。修改src/core/dispathRequest.js:

function throwIfCancellationRequested(config) {
  if (config.cancelToken) {
    config.cancelToken.throwIfRequested()
  }
}

export default function dispatchRequest(config) {
  // 增加这个代码
  throwIfCancellationRequested(config)
  processConfig(config)
  return xhr(config).then(res => {
    // json字符串转为json对象
    res.data = transformResponse(res.data)
    return res
  })
}

最后修改src/core/xhr.js,添加错误处理的判断:

const { cancelToken } = config
if (cancelToken) {
  cancelToken.promise.then(() => {
    // request是一个XMLHttpRequest对象
    request.abort()
    reject(cancelToken.reason)
  })
}

当使用者调用取消函数,then里的回调被执行,调用原生的abort方法就能取消请求,同时调用reject,使用者可以在代码中用catch来捕获,配合着axios.isCancel方法就能知道是一个cancel类型的异常。

总结

今天的文章实现了axios的拦截器和取消请求,下一篇文章将实现axios的默认配置和根据默认配置产生一个全新的axios实例以及其他一些axios的功能。