请求模块配置与axios拦截器实现原理

946 阅读19分钟

概要

axios是我们开发webapp中常用的请求工具之一,它使我们能够更容易方便的进行前端与后端联调工作,在前端项目中封装好一个请求模块能够让效率有所提高。我们可以学习axios的实现原理与配置方法从而理解并更好的使用其它同类型的几种请求工具,下面的内容使用场景都基于使用vue技术栈开发的项目。

配置优化

1.1 配置一

一般我们在项目中我们都会对axios进行一定的封装以满足我们快速开发,比如以下方式:

config.js

import axios from 'axios'
export  default axios

http.js

import axios from 'config'

export function post (url, param) {
  return axios.post('url',{
    params:param
  } )
}

userInfo.vue

import {post} from 'http.js'
  created() {
    post('/v1/getPatientDetail').then((res) => {
      // do sth...
    })
}

这样的封装方式,主要对不同的请求方式进行了统一,但是如果我们还有比如put、delete等等其它请求方法的话,那我们就都要进行声明定义,而且这种方式传递参数也不自由。

另外大家可能遇到过后端可能会对某些特定业务接口名做统一的修改,像上面这种调用方式,我们如果要统一修改接口名称的话,只能是去到组件中一个个进行单独修改,这样非常不方便。

1.2 配置二

根据上面产生的问题,我们优化一下封装的方式:

config.js

import axios from 'axios'
export  default axios

user.js

import axios from 'config.js'

export const getUserDetail = (params) => {
  retrun axios.post('/v1/getUserDetail', params)
}
...
...

userInfo.vue

import {getUserDetail } from 'user.js'

created() {
  getUserDetail({id: '132456'}).then((res) => {
    // do sth...
  })
}

针对业务的不同将接口分模块分装,将具体的请求方法直接定义在文件中,我们就可以在组件中直接调用来使用,这很符合我们的需要,这样既方便后期维护也方便我们的查看接口文档。但是这种方式还并不完美,当我们每次需要请求接口时都需要在组件写大量的import语句,并且一个接口重复在多个组件中使用的话每次都需要import。

2.1 优化(结构、抽离、统一)

如何避免我们上面说到的种种问题,让我们的请求优雅且易维护?

综合以上的问题我们来配置一个请求模块,大致分为以下三个步骤:

  • 我们将接口文档统一放在一个文件夹中以配置的方式进行封装。
  • 封装一个统一调用接口的方法,该方法用传入的参数对不同的接口文件进行获取并调用。
  • 将函数以插件的方式注册到VUE原型中,方便全局调用。

./request/config.js

import axios from 'axios'
export  default axios

./request/apiModules/user.js

export default {
  getPatientDetail : {
      url: 'v1/getDetail',
      method: 'get'
  }
  // ...
  // ...
}
  • 这是我们请求接口的具体url与调用该接口对应的method,具体的请求行为还在下面。

./request/fetch.js

import axios from './config'
// 加载配置文件
const fetchConfig = {}
const requireContext = require.context('./apiModules', false, /\.js$/)
requireContext.keys().forEach(path => {
  let module = path.replace('.js', '').replace('./', '')
  fetchConfig[module] = requireContext(path).default
})
/**
 * 大致结构
 * let fetchConfig = {
 *  user: {
 *    getPatientDetail : {
 *      url: 'v1/getDetail',
 *      method: 'get'
 *    }
 *  }
 * }
 * /
  • 在webpack打包环境下,我们使用require.context动态读取apiModules目录下的配置文件,将所有配置读取后进行存储,这样避免我们每次创建新的模块后都要import一次。同样的方法也可使用在vuex配置多个模块的场景下,动态读取多个配置的配置。
/**
 * 解析参数
 * 这个函数主要负责解析传入fetch的 module 和 apiName
 * @param {String} param
 */
const fetchParam = param => {
    var valid = /[a-z]+(\.[a-z])+/.test(param)
    if (!valid) {
      throw new Error('[Error in fetch]: fetch 参数格式为 moduleName.apiName')
    } else {
      return {
        moduleName: param.split('.')[0],
        apiName: param.split('.')[1]
      }
    }
  }
  
  /**
   * 请求函数
   * 这个函数主要通过解析出来的module 和 apiName找到对应配置发起请求
   * @param {String} moduleInfo
   * @param {any} payload
   */
  export function fetch(moduleInfo, payload) {
    let prefix = ''
    let moduleName = fetchParam(moduleInfo)['moduleName']
    let apiName = fetchParam(moduleInfo)['apiName']
    // 判断没有找到传入模块
    if (!fetchConfig.hasOwnProperty(moduleName)) {
      throw new Error(
        `[Error in fetch]: 在api配置文件中未找到模块 -> ${moduleName}`
      )
    }
    // 判断没有找到对应接口
    if (!fetchConfig[moduleName].hasOwnProperty(apiName)) {
      throw new Error(
        `[Error in fetch]: 在模块${moduleName}中未找到接口 -> ${apiName}`
      )
    }
    let fetchInfo = fetchConfig[moduleName][apiName]
    let method = fetchInfo['method']
    let url = `${prefix}/${fetchInfo['url']}`
    let mode = fetchInfo['mode'] // 此处在headers中添加一个needCancel属性。
    mode
      ? (axios.defaults.headers['needCancel'] = true)
      : (axios.defaults.headers['needCancel'] = false)
    if (method === 'get') {
    // url:'v1/special/template/assess/{id}/{code}'   用于解决get接口中有多个参数需要传递的场景。
      if (url.indexOf('{') !== -1) {
        let temp = url.match(/\{[^\}]+\}/)[0]
        let param = temp.substring(1, temp.length - 1)
        let newUrl = url.replace(/\{[^\)]*\}/g, payload[param])
        return axios[method](newUrl)
      }
      return axios[method](url, {
        params: payload
      })
    } else {
      return axios[method](url, payload)
    }
  }
  • 通过解析传入的参数,我们去获取对应接口的url、method、mode等属性(mode属性主要实现对接口的自定义需求,下面小节会进行说明),最终再使用axios发起请求。
  • 当我们将请求方法单独抽离封装后,我们在开发中如果新增一个接口,只需要修改或新增apiModules中的配置文件,无需再关注其他操作,让我们更好的开发业务。

./request/index.js

import {fetch} from './request/fetch.js'

export default {
    install(Vue) {
      Vue.prototype.$fetch = fetch
    }
  }
  • 导出一个包含install函数的对象,函数中会将fetch方法放入到vue原型链中,使我们在项目中可以以this.$fetch 的方式进行调用,无需引入具体请求方法。

./main.js

import Vue from 'vue'
import request from './request/index'

Vue.use(request)
  • 我们在项目入口文件中对request进行初始化安装,最终实现全局可用。

在main.js中我们看到使用Vue.use方法就可以对插件进行安装,我们大致看下use函数的源码,了解下use是如何操作将一些vue插件安装到VUE上的。

下面截取的代码是vue"version": "2.6.12"use详细路径

vue/src/core/global-api/use.js

export function initUse (Vue: GlobalAPI) {
    Vue.use = function (plugin: Function | Object) {
      const installedPlugins = (this._installedPlugins || (this._installedPlugins = []))
      if (installedPlugins.indexOf(plugin) > -1) {
        return this
      }
  
      // additional parameters
      const args = toArray(arguments, 1)
      args.unshift(this)
      if (typeof plugin.install === 'function') {
        plugin.install.apply(plugin, args)
      } else if (typeof plugin === 'function') {
        plugin.apply(null, args)
      }
      installedPlugins.push(plugin)
      return this
    }
  }

每次调用都会查看installedPlugins数组中是否存在该插件,如已存在就直接返回。args.unshift(this)这里的this指向VUE类,将其放到参数数组中,然后apply调用插件的install方法,并且将args参数传给install方法,这样做在开发插件的时候就不用引入vue导致文件过大。最后installedPlugins.push(plugin)将已经初始化的插件存入到缓存数组中用于避免重复初始化。

从源码我们得知,开发基于vue的插件都需要实现install方法。

3.1 优化(cancel篇)

3.1.1 取消同一接口重复的请求

axios有很丰富的配置,利用其配置可以帮助我们在开发时节省工作量和优化我们的项目。我们上面将接口文档和请求方法进行了分离解耦,在请求方法中,我们可以按照我们的需要定制开发适合我们项目的方法来快速使用。

我们在开发中经常遇到这种场景,尤其是移动端。一个搜索的业务需要根据用户输入的内容实时查出查询的结果,我们捕获用户的输入事件每次输入新内容时都重新发起请求。这样的方式会对我们请求资源造成浪费,我们不希望每次输入都会发起请求,我们添加200ms的"防抖"debounce来包装输入事件,从而避免触发过多的请求。

但是当我们的搜索接口比较复杂,或者是使用类似查找地名、查找商品这种第三方提供的接口时,响应往往都会很慢,有时候1-2s的延迟也是正常的。那在这种情况下,用户在输入框中输入了“北京”,发起请求后,用户发现实际想搜的是“南京”,又重新输入了“南京”,这时即使有200ms的防抖,用户还是发起了多个请求。因为接口的响应时间是不确定,先发出的请求可能要比后发出的请求要快,这就会导致“南京”返回的结果被“北京”的结果给覆盖了这么尴尬的情况。

所以我们可以针对这种搜索的接口在添加了防抖的基础上,统一再加上cancel的机制。用于在一个接口已经发起请求但还没有响应的时候又发起了相同的接口,这时候我们将上一个接口手动取消的操作。

具体操作如下:

./request/apiModules/useInfo.js

export default {
  // 校验当前手机号码是否有注册过
  checkPatientPhone: {
    url: 'v1/patient/verification/phone/{condition}',
    method: 'get',
    mode: 'search'
  }
}

./request/fetch.js

import axios from './config'
export function fetch() {
          ...
  let mode = fetchInfo['mode']
  mode === "search" ? (axios.defaults.headers['needCancel'] = true) : (axios.defaults.headers['needCancel'] = false)
          ...
}
  • fetch实现方法我们在2.1中已经看到,在方法内部获取到对应mode的值,我们进行判断 mode === "search" 如果为true,则对headers中添加一个自定义属性用于判断,具体操作我们在拦截器中实现。

./request/config.js

import axios from 'axios'

// 截取一段url字符串拼接method作为该接口唯一标识
function fmtUrl({url, method}) {
  return `${url.substr(0, url.lastIndexOf('/'))}&${method}`
}

let pending = [] //声明一个数组用于存储每个ajax请求的取消函数和ajax标识
let cancelToken = axios.CancelToken
let removePending = config => {
  if (!config.headers.needCancel) return
  const requestName = fmtUrl(config)
  for (let p in pending) {
    if (pending[p].name === requestName) {
      //当当前请求在数组中存在时执行函数体
      pending[p].fn() //执行取消操作
      pending.splice(p, 1) //把这条记录从数组中移除
    }
  }
}

// 请求拦截器
axios.interceptors.request.use(
  config => {
    removePending(config) //在一个ajax发送前执行一下取消操作
    config.cancelToken = new cancelToken(cancelFn => {
      // 这里的ajax标识我是用请求地址&请求方式拼接的字符串,当然你可以选择其他的一些方式
      if (config.headers.needCancel) {
        const requestName = fmtUrl(config)
        pending.push({ name: requestName, fn: cancelFn })
      }
    })
    return config
  },
  err => {
    return Promise.reject(err)
  }
)

// 响应拦截器
axios.interceptors.response.use(
  response => {
    removePending(response.config) //在一个ajax响应后再执行一下取消操作,把已经完成的请求从pending中移除
    return response
  },
  error => {
    console.log(error)
    return Promise.reject(error)
  }
)

export default axios

我们先来看下这里的核心是axios.CancelToken,拿到这个暴露出来的方法对其进行实例化,实例化传入一个匿名函数作为参数,匿名函数内部可以拿到CancelToken传给我们的取消接口的方法cancelFn

在请求拦截器中首次遇到needCancel === true的接口我们将实例化CancelToken后获取的取消方法和接口唯一名称进行存储在pending数组中。当该接口再次发起请求时进入到请求拦截器中执行removePending将数组中相同的接口执行取消方法直接中断该接口。为避免一个接口已经响应成功在下次请求时还执行取消操作,我们在响应拦截器中也执行removePending函数,对已经响应成功的接口删除pending中对应的数据,避免重复操作。

至此我们这一小节针对反复发起同一请求避免结果覆盖的功能开发完毕,后面我们如果还有其他业务也需要用到这个功能可直接在配置中添加 mode:"search" 进行开启。

3.1.2 同一接口的相同响应内容进行取消

通过上面的案例我们发现cancel的机制可以帮助我们优化网页的请求,居然能优化请求那肯定是要多扩展一些思路了,毕竟能减少一个无用的请求接口省下来的资源就可以在更短的时间发起更多的请求了,这也是实实在在的性能优化呀。

利用上面cancel函数的思路,我们对一些项目中频繁使用的返回枚举数据的接口进行优化。我们对一些使用率很高的枚举接口进行标记,当其发起请求并响应成功后我们将其数据存储到sessionStorage中,下次同一接口再进行请求时取消当前接口,直接从sessionStorage中获取结果返回,毕竟从本地获取还是要比从服务器上获取要快很多的。

具体实现:

./request/apiModules/useInfo.js

export default {
  // 获取时间枚举
  getLibraryDateList: {
    url: 'v1/medical/library/date/type/list',
    method: 'get',
    mode: 'cache'
  }
}

./request/fetch.js

import axios from './config'
export function fetch() {
          ...
  let mode = fetchInfo['mode']
  mode === "cache" ? (axios.defaults.headers['needCache'] = true) : (axios.defaults.headers['needCache'] = false)
          ...
}

接口配置与fetch内部实现都是一样的套路。

./request/config.js

import axios from 'axios'

// 截取一段url字符串拼接method作为该接口唯一标识
function fmtUrl({url, method}) {
  return `${url.substr(0, url.lastIndexOf('/'))}&${method}`
}

let pendingCache = [] //声明一个数组用于存储每个ajax请求的取消函数和ajax标识
let cancelCachePending = (config, cacheData) => {
  if (!config.headers.needCache) return
  let requestName = fmtUrl(config)
  if (cacheData) {
    window.sessionStorage.setItem(`${requestName}`, JSON.stringify(cacheData))
  }
  let requestResult = window.sessionStorage.getItem(`${requestName}`)
  if (requestResult) {
    for (let p = pendingCache.length - 1; p >= 0; p--) {
      if (pendingCache[p].name === requestName) {
        // sessionStorage已有数据,则直接触发取消函数,并且将获取到的数据进行返回
        pendingCache[p].fn({
          hasLoad: true,
          result: JSON.parse(requestResult)
        })
        pendingCache.splice(p, 1) //把这条记录从数组中移除
      }
    }
  }
}

// 请求拦截器
axios.interceptors.request.use(
  config => {
    config.cancelToken = new cancelToken(cancelFn => {
      if (config.headers.needCache) {
        const requestName = fmtUrl(config)
        pendingCache.push({ name: requestName, fn: cancelFn })
        cancelCachePending(config)
      }
    })
    return config
  },
  err => {
    return Promise.reject(err)
  }
)

// 响应拦截器
axios.interceptors.response.use(
  response => {
    // 成功响应则触发该函数
    cancelCachePending(response.config, response.data)
    return response
  },
  error => {
    console.log(error)
    return Promise.reject(error)
  }
)

export default axios

./components/user.vue

created() {
  this.$fetch('userInfo.getLibraryDateList').then((res) => {
    let data = res.data.result
    this.dateOptions = Object.freeze(data.map((item) => ({ label: item.desc, value: item.status })))
  }).catch((e) => {
    let { message } = e
    if (message.hasLoad) {
      let res = message.result
      let data = res.result
      this.dateOptions = Object.freeze(data.map((item) => ({ label: item.desc, value: item.status })))
    }
  })
}

实现大致思路基本一致,在请求拦截器中我们对needCache === true的接口获取到取消函数后添加到 pendingCache 数组中。然后执行cancelCachePending函数,该函数中获取该接口存储在sessionStorage中的数据,如果有数据说明接口则直接出发取消函数并且利用cancel的返回消息机制将数据返回,如果没有数据则表明该接口是第一次请求,则不作任何取消操作。

响应拦截器中当接口响应成功时,将数据传递给cancelCachePending函数,函数中会对数据进行存储,为下次同一接口请求时获取到该数据。 这时我们需要在请求接口处做兼容处理,当cancel取消接口返回结果时,内部会调用promise.reject将信息返回,所以我们需要在catch中接收数据来进行操作。

到这里我们利用cancel机制实现的针对重复数据取消接口实现接口优化的功能已经实现。我们发现仅仅利用axios中提供的cacel机制就能帮助我们对项目的开发做不少优化工作,当然我相信不仅仅这两种方案,童鞋们可以根据自己的业务实现符合自己需要的其他功能。

原理分析

我们上面从请求模块的配置到在拦截器中配置cancel功能,都依赖axios插件的应用,这一章我们来看下上面提到的这些功能源码都是怎么实现的,多的我们就不看了,主要了解下它的拦截器和cacelToken是如何实现的,所以会隐藏一些无关的代码逻辑。从源码层面去了解插件的实现,能够大幅度提高我们对它的认识与掌握,让我们在使用中更加熟练易拓展,而且定义请求中遇到的问题也更块。

以下源码的截取均来自axios

底层请求的实现

首先我们来看下在axios内部是如何发起请求。

axios/lib/defaults.js

function getDefaultAdapter() {
  var adapter;
  if (typeof XMLHttpRequest !== 'undefined') {
    // For browsers use XHR adapter
    adapter = require('./adapters/xhr');
  } else if (typeof process !== 'undefined' && Object.prototype.toString.call(process) === '[object process]') {
    // For node use HTTP adapter
    adapter = require('./adapters/http');
  }
  return adapter;
}

var defaults = {
  adapter: getDefaultAdapter()
  ...
}

module.exports = defaults;

defaults文件主要是给axios添加一些默认的配置,当我们请求时即使没有添加任何accept、headers、timeout等配置,在这里面会帮助我们默认添加,这样的操作让我们可以直接来使用简化使用成本。

我们看getDefaultAdapter函数中有为当前环境下使用不同的请求对象的兼容语句,如果当前环境typeof XMLHttpRequest !== 'undefined'说明是在window环境下浏览器端,如果是typeof process !== 'undefined'的话,因为process对象是node环境中发起请求的内置对象,这说明是在当前axios运行的环境是node环境,针对不同环境,axios会使用不同的方式来发起请求。我们当前是在浏览器端进行开发,我们进到./adapters/xhr文件中看下在浏览器端是如何发起请求的。

axios/lib/adapters/xhr.js

module.exports = function xhrAdapter(config) {
  return new Promise(function dispatchXhrRequest(resolve, reject) {
    var request = new XMLHttpRequest();

    var fullPath = buildFullPath(config.baseURL, config.url);
    request.open(config.method.toUpperCase(), buildURL(fullPath, config.params, config.paramsSerializer), true);

    request.timeout = config.timeout;

    // Listen for ready state
    request.onreadystatechange = function handleLoad() {
      if (!request || request.readyState !== 4) {
        return;
      }

      if (request.status === 0 && !(request.responseURL && request.responseURL.indexOf('file:') === 0)) {
        return;
      }

      var response = {
        data: responseData,
        status: request.status,
        statusText: request.statusText,
        headers: responseHeaders,
        config: config,
        request: request
      };

      settle(resolve, reject, response);

      request = null;
    };

    request.onabort = function handleAbort() {
      if (!request) {
        return;
      }

      reject(createError('Request aborted', config, 'ECONNABORTED', request));
      request = null;
    };

    request.onerror = function handleError() {
      reject(createError('Network Error', config, null, request));

      request = null;
    };

    request.ontimeout = function handleTimeout() {
      var timeoutErrorMessage = 'timeout of ' + config.timeout + 'ms exceeded';
      if (config.timeoutErrorMessage) {
        timeoutErrorMessage = config.timeoutErrorMessage;
      }
      reject(createError(timeoutErrorMessage, config, 'ECONNABORTED',
        request));

      request = null;
    };

    // Handle progress if needed
    if (typeof config.onDownloadProgress === 'function') {
      request.addEventListener('progress', config.onDownloadProgress);
    }

    // Not all browsers support upload events
    if (typeof config.onUploadProgress === 'function' && request.upload) {
      request.upload.addEventListener('progress', config.onUploadProgress);
    }

    if (config.cancelToken) {
      // Handle cancellation
      config.cancelToken.promise.then(function onCanceled(cancel) {
        if (!request) {
          return;
        }

        request.abort();
        reject(cancel);
        // Clean up request
        request = null;
      });
    }

    if (!requestData) {
      requestData = null;
    }

    // Send the request
    request.send(requestData);
  }
}

在这里我删除了一些具体实现,只留下了一些主线流程。函数传入的参数config可以看成使我们传入的参数和defaults默认参数进行和合并后的值。

  • 首先我们看到,整个函数体都是被Promise对象进行包裹的,返回的也是一个promise实例,这也就得知了为什么我们在项目中使用axios发起一个请求成功后可以用then、catch的方式进行获取的原因了,源自它内部返回了一个promise类型的对象,这要比我们以前使用回调的方式接收结果方式来的更加符合异步接口理解。 函数内部利用promise的两个参数resolve返回正确结果,reject返回错误原因。

  • 让我们继续往下看,发起请求的核心代码就是new XMLHttpRequest(),不管是现在的axios还是我们以前的jquery,到现在为止我们前端的请求方式底层都没有变化,都是利用了XMLHttpRequest不刷新页面的情况下请求特定URL的特性来跟服务器端进行交互。XMLHttpRequest

    • 内部在XHR的实例request上定义了个多钩子函数,例如onabort、onerror、ontimeout等都会在请求发起错误的时候reject将具体错误封装抛出。
    • 主要发起请求的方法有request.open,它拿method与拼接参数后的url初始化了请求。然后request.send(requestData)会携带body参数将请求发出。
    • 接下来我们看下onreadystatechange钩子函数,在它内部对readyState和status的值进行了拦截,监听到请求正常返回后调用settle(函数内部主要调用resolve)将封装好的response进行返回。
  • 我们还看到内部通过request的上传与下载的progress钩子来调用对应的函数,如果你想在调用接口时显性的看到进度条不妨配置这两个函数进行实现。

总的来看,xhr内部主要帮我们完成了对配置合并后将请求发出的一些列操作,将其封装后,我们只要考虑参数的传递不再需要考虑接口是如何请求的,axios内部对其封装后简化了调用逻辑和可维护性,这种开发方式也可以运用到我们实际的开发工作中,将核心代码与业务之间进行解耦,提高代码的复用性。

拦截器实现

我们在看拦截器之前,先看下在项目中我们是怎么去使用的。

./request/config.js

// 请求拦截器
axios.interceptors.request.use(
  config => {
    return config
  },
  err => {
    return err
  }
)

// 响应拦截器
axios.interceptors.response.use(
  response => {
    return response
  },
  error => {
    return error
  }
)

通过配置文件我们大概可以看出来,在axios对象属性中暴露出了一个interceptors这个属性对象,该对象有两个属性分别是requestresponse,这两个属性存分别对应请求前与请求后我们想做的一些操作,再调用use函数配置了两个函数进去。

看了配置方式,我们来看源码中的具体实现: axios/lib/core/Axios.js

function Axios(instanceConfig) {
  this.defaults = instanceConfig;
  this.interceptors = {
    request: new InterceptorManager(),
    response: new InterceptorManager()
  };
}

我们看到在Axios构造函数中有this.interceptors这个对象,当我们使用Axios的实例去获取interceptors对象时会往上层找到Axios原型上从而找到这个对象。我们继续看下new InterceptorManager()中有什么玄机,调用其use函数它会做什么。

axios/lib/core/InterceptorManager.js

var utils = require('./../utils');

function InterceptorManager() {
  this.handlers = [];
}

InterceptorManager.prototype.use = function use(fulfilled, rejected) {
  this.handlers.push({
    fulfilled: fulfilled,
    rejected: rejected
  });
  return this.handlers.length - 1;
};

InterceptorManager.prototype.eject = function eject(id) {
  if (this.handlers[id]) {
    this.handlers[id] = null;
  }
};

InterceptorManager.prototype.forEach = function forEach(fn) {
  utils.forEach(this.handlers, function forEachHandler(h) {
    if (h !== null) {
      fn(h);
    }
  });
};

module.exports = InterceptorManager;

InterceptorManager原型上定义了三个方法和一个数组。

  • use函数将我们配置时传入的两个函数分别对应fulfilled和rejected这两种状态以对象的方式存储到handlers当中,push的顺序按照我们配置时的调用顺序依次加入,所以我们在配置请求与响应拦截器时先后顺序不能改变。
  • eject函数将指定元素置位null,删除定义的拦截器。借此我们可以根据不能的业务执行不同的拦截器。
  • forEach会将拦截器传递给fn(h)函数中,后面会用到。

在看完拦截器内部的存储、删除、遍历等操作后,接下来我们看下在Axios中是如何对拦截器中的配置的函数进行调用的。

先看下拦截器整体执行方式方法,再对应源码中的实现进行对照。我们看到请求拦截器遵循先进后执行原则,响应拦截器遵循先进先执行原则,整个过程是一个链式调用的过程,中间穿插的我们的调用接口的dispatchRequest方法,从而实现了我们对配置拦截,对响应体拦截这么一个逻辑。

axios/lib/core/Axios.js

Axios.prototype.request = function request(config) {
  /*eslint no-param-reassign:0*/
  // Allow for axios('example/url'[, config]) a la fetch API
  if (typeof config === 'string') {
    config = arguments[1] || {};
    config.url = arguments[0];
  } else {
    config = config || {};
  }

  config = mergeConfig(this.defaults, config);

  // Set config.method
  if (config.method) {
    config.method = config.method.toLowerCase();
  } else if (this.defaults.method) {
    config.method = this.defaults.method.toLowerCase();
  } else {
    config.method = 'get';
  }

  // Hook up interceptors middleware
  var chain = [dispatchRequest, undefined];
  var promise = Promise.resolve(config);

  this.interceptors.request.forEach(function unshiftRequestInterceptors(interceptor) {
    chain.unshift(interceptor.fulfilled, interceptor.rejected);
  });

  this.interceptors.response.forEach(function pushResponseInterceptors(interceptor) {
    chain.push(interceptor.fulfilled, interceptor.rejected);
  });

  while (chain.length) {
    promise = promise.then(chain.shift(), chain.shift());
  }

  return promise;
};

上面是我们再请求接口时axios内部执行的核心代码,mergeConfig将我们传递进来的config配置参数按照合并规则进行合并,我们看到之前的分析的this.defaults内部默认配合和我们的自定义配置config进行的merge,这也是我们无需配置任何参数就可以发起请求的原因。

axios使用的是数组的方式完成了链式调用,通过对request与response的forEach方法,将请求拦截器插入到chain的前面,将响应拦截器插入到chain的后面,这样整体的链式结构就出来了。

这里的核心思想就是很巧妙的借助promise的链式调用方式,实现了拦截器一层层的链式调用的效果。使用while语句,依次将chain中fulfilled与rejected方法依次放入promise.then()的参数中,因为第一个promise对象是手动Promise.resolve(config)返回了配置文件,所以触发了then函数中的fulfilled函数,函数内部执行完毕后会再次返回一个Promise对象赋值给promise,继而重复操作将所有的拦截器均执行完毕,最终返回给我们一个处理过的promise,我们只要对返回出来的promise做接收就能拿到我们想要的结果。

总结

整片章节到此已经结束,我们从对axios的配置使用到分析其底层实现的原理大致了解了我们每次在发起请求时插件都干了些什么,通过看源码的方式我们能学习到插件的设计封装与实现思路,帮助我们提高自身的能力,也帮助了我们对插件更深层次的理解,当插件本身满足不了我们的需求时我们也可以去自己进行扩展。在前端技术不断创新的今天,我们在提高自己对各种技术的应用能力的同时也应该沉下来看看源码,毕竟面试的时候聊一聊源码还是挺加分的[狗头]。

其次在本文配置章节涉及到的axios.cancel机制在此就不进行分析了,大家可以直接进github看看如何实现的。