Axios实现源码解析

204 阅读10分钟

一、XHR对象的理解和使用

1. XMLHttpRequest(XML)对象

XMLHttpRequest(XHR)对象用于与服务器交互。通过 XMLHttpRequest 可以在不刷新页面的情况下请求特定 URL,获取数据。这允许网页在不影响用户操作的情况下,更新页面的局部内容。XMLHttpRequest 在 AJAX 编程中被大量使用。

  • 使用XMLHttpRequest(XHR)对象可以与服务器交互,也就是发送ajax请求
  • 前端页面可以获取到数据,而无需刷新整个浏览器页面
  • 使得web页面可以只更新页面的局部,而不影响用户的操作

2. XMLHttpRequest创建请求的API说明

  1. XMLHttpRequest(): 创建XHR对象的构造函数
  2. status: 响应状态码值,比如200、404等
  3. statusText: 响应状态文本说明
  4. readyState: 标识请求状态的只读属性
    • 0: 初始
    • 1: open()之后
    • 2: send()之后
    • 3: 请求中
    • 4: 请求完成
  5. onreadytatechange: 绑定readyState改变的监听函数
  6. responseType: 指定响应数据类型,如果是json,得到响应后自动解析响应体数据
  7. response: 响应体数据,类型取决于responseType的制定
  8. timeout: 指定请求的超时时间,默认为0代表没有限制
  9. ontimeout: 绑定超时的监听函数
  10. onerror: 绑定请求网络错误的监听函数
  11. open(): 初始化一个请求,参数为: (method, url[,async])(默认asynctrue,表示异步请求)
  12. send(data): 发送请求
  13. abort(): 中断请求
  14. getResponseHeader(name): 获取指定名称的相应头值
  15. getAllResponseHeaders(): 获取所有的相应头组成的字符串
  16. getResponseHeader(name): 绑定超时的监听函数
  17. setRequestHeader(name, value): 设置请求头

3. ajax请求和一般的http请求

  1. ajax请求是一种特别的http请求
  2. 对服务器来说,ajax请求和一般的http请求没有任何区别,区别在与浏览器端
  3. 浏览器端发送请求,只有XHR或者fetch发出的才是ajax请求,其他所有的请求都是非ajax请求
  4. 浏览器端收到响应:
    • 一般请求:浏览器一般会直接显示响应体数据,也就是我们常说的刷新/跳转页面,例如,请求的css样式文件、js文件、图片文件等
    • ajax请求:浏览器不会对界面进行任何更新操作,只是调用监视的回调函数并传入响应的相关数据 注:我们编写发送ajax的代码,用ajax引擎发起ajax请求,并执行对应的回调函数

二、XHR封装原生ajax请求函数

1. axios基本使用说明:

  • axios函数返回值为promise。成功的结果为response,异常的结果为error
  • 能处理多种类型的请求:GET/POST/PUT/DELETE
  • 函数的参数为一个配置对象:{url: '请求地址', method: '请求方式', params: 'GET/DELETE请求的query参数', data: 'POST/PUT请求的请求提参数'}
  • 响应json数据自动解析为json

2. 仿照axios封装简单的ajax请求函数:

    function axios({
      url,
      method = 'GET',
      params = {},
      data = {}
    }){
      // 返回一个promise对象
      return new Promise((resolve, reject) => {
        // 修正url请求路径
        let queryString = ''
        Object.keys(params).forEach(key => {
          queryString  += `${key}=${params[key]}&`
        })
        if(queryString){
          url += url.includes('?') ? '&' : '?' + queryString.substring(0, queryString.length-1)
        }
        // 修正请求方式
        method = method.toUpperCase()
        // 1. 执行异步的ajax请求
        //  创建xhr对象
        const xhr = new XMLHttpRequest()
        // 打开连接(初始化请求,没有发送请求)
        xhr.open(method, url, true)
        // 发送请求
        if(method === 'GET' || method === 'DELETE'){
          xhr.send(null)
        }else if(method === 'POST' || method === 'PUT'){
          // Content-Type请求头告诉服务器,请求数据为json数据
          xhr.setRequestHeader('Content-Type', 'application/json;charset=utf-8')
          xhr.send(JSON.stringify(data))
        }
        // 监听异步请求成功的回调函数
        xhr.onreadystatechange = () => {
          if (xhr.readyState === 4){
            const {status, statusText} = xhr
            if(xhr.status >= 200 && xhr.status < 300){
              // 2.1 请求成功了,调用resolve()
              const response = {
                data: JSON.parse(xhr.response),
                status,
                statusText
              }
              resolve(response)
            } else {
              // 2.2 请求失败了,调用reject()
              reject(new Error('request error status is ' + status))
            }
          }
        }
      })
    }

三、axios的理解和使用

1. axios的特点

  1. 是基于promise的异步ajax请求库
  2. 浏览器和node端都可以使用
  3. 支持请求/响应拦截器
  4. 支持请求取消
  5. 请求/响应数据转换
  6. 支持批量发送多个请求(实际都是用Promise.all代替这个功能)

2. axios常用API语法

  1. axios(config): 统一/基本的发送任意类型请求的方式
  2. axios(url[, config]): 只指定url发送get请求
  3. axios.request(config): 等同于axios(config)
  4. axios.get(url[, config]): 发送get请求
  5. axios.post(url[, config]): 发送post请求
  6. axios.put(url[, config]): 发送put请求
  7. axios.delete(url[, config]): 发送delete请求
  8. =======================================
  9. axios.default.xxx: 请求的默认全局配置
  10. axios.interceptors.request.use(): 添加请求拦截器
  11. axios.interceptors.response.use(): 添加请求拦截器
  12. =======================================
  13. axios.create([config]): 根据配置创建一个新的axios(是拥有对应配置的axios函数,不是axios的实例,只是相对于axios没有下面的功能)
  14. =======================================
  15. axios.Cancel(): 用于创建取消请求的错误对象
  16. axios.CancelToken(): 用于创建取消请求的token对象
  17. axios.isCancel(): 是否是一个取消请求的错误
  18. axios.all(promises): 用于批量执行多个异步请求,所有请求都成功了才算成功,类似Promise.all()
  19. axios.spread(): 获取指定接受所有成功数据的回调函数的方法

3. axios难点语法的理解和使用

(1) axios.create(config)

  1. 根据制定配置创建一个新的axios,也就是每个新的axios都有自己的配置
  2. 新的axios只是没有取消请求和批量发送请求的方法,其他所有的语法都是一致的
  3. 为什么设计这个语法?
    • 需求:项目中有部分接口需要的配置与另一部分接口需要的配置不太一样,如何处理?
    • 解决:创建2个新的axios,每个都有自己特有的配置,分别应用到不同要求的接口请求中
  4. axios.create()返回的对象与axios的区别

(2) 拦截器函数/ajax请求/请求的回调函数的执行顺序

  1. 拦截器函数
    • 请求拦截器: 在真正发请求前, 可以对请求进行检查或配置进行特定处理的函数, 包括成功/失败的函数, 传递的必须是config
    • 响应拦截器: 在请求返回后, 可以对响应数据进行特定处理的函数,包括成功/失败的函数, 传递的默认是response
  2. 使用拦截器调用接口测试
    axios.defaults.baseURL = 'http://localhost:3001';
    // 添加请求拦截器1
    axios.interceptors.request.use((config) => {
      console.log('add request interceptors1')
      return config
    })
    // 添加请求拦截器2
    axios.interceptors.request.use((config) => {
      console.log('add request interceptors2')
      return config
    })
    // 添加响应拦截器1
    axios.interceptors.response.use((response) => {
      console.log('add response interceptors1')
      return response
    })
    // 添加响应拦截器2
    axios.interceptors.response.use((response) => {
      console.log('add response interceptors2')
      return response
    })
    // 执行发送请求操作
    function getDate(){
      axios.get('/user').then((result) => {
        console.log('Get date:', result.data)
      }).catch((err) => {
        console.log('Get err:', err)
      });
    }

==============响应数据如下图============== image.png

从上面的答应结果可以看出:

  1. 调用axios()并不是立即发送ajax请求, 而是需要经历一个较长的流程
  2. 请求拦截器2 => 请求拦截器1 => 发ajax请求 => 响应拦截器1 => 响应拦截器2 => 请求的回调
  3. 此流程是通过promise串连起来的, 请求拦截器传递的是config, 响应拦截器传递的是response错误流程控制与错误处理
  • 拦截器首先被执行的是请求拦截器,在执行响应的拦截器,请求拦截器按照添加的顺序从后往前执行,响应拦截器按照添加的顺序从前往后执行;
  • 请求拦截器是在真正发请求前,对请求头和请求体的数据进行特定的处理,用请求拦截器返回的配置参数,调用axios.request()方法发送ajax请求;
  • 响应拦截器是在接口真正返回数据前,对响应数据进行特定处理,再将处理后的数据传给执行请求结束后的成功或者失败回调函数。

四、axios源码分析

1. 源码目录组成结构

├── /dist/                     # 项目输出目录
├── /lib/                      # 项目源码目录
│ ├── /adapters/               # 定义请求的适配器 xhr、http
│ │ ├── http.js                # 实现http适配器(包装http包)
│ │ └── xhr.js                 # 实现xhr适配器(包装xhr对象)
│ ├── /cancel/                 # 定义取消功能
│ ├── /core/                   # 一些核心功能
│ │ ├── Axios.js               # axios的核心主类
│ │ ├── dispatchRequest.js     # 用来调用http请求适配器方法发送请求的函数
│ │ ├── InterceptorManager.js  # 拦截器的管理器
│ │ └── settle.js              # 根据http响应状态,改变Promise的状态
│ ├── /helpers/                # 一些辅助方法
│ ├── axios.js                 # 对外暴露接口
│ ├── defaults.js              # axios的默认配置 
│ └── utils.js                 # 公用工具
├── package.json               # 项目信息
├── index.d.ts                 # 配置TypeScript的声明文件
└── index.js                   # 入口文件

2. axios与Axios的关系

  • axios函数对应的是Axios.prototype.request方法通过bind(Axiox的实例)产生的函数
  • axiosAxios原型上的所有发特定类型请求的方法: get()/post()/put()/delete()
  • axiosAxios的实例上的所有属性: defaults/interceptors
  • 后面又添加了create()/CancelToken()/all()

3. axios.create()返回的对象与axios的区别

  • 相同:
    • 都是一个能发任意请求的函数: request(config)
    • 都有发特定请求的各种方法: get()/post()/put()/delete()
    • 都有默认配置和拦截器的属性: defaults/interceptors
  • 不同:
    • 默认匹配的值不一样
    • instance没有axios后面添加的一引起方法: create()/CancelToken()/all()

4. axios发请求的流程

  • 整体流程: request(config) ===> dispatchRequest(config) ===> xhrAdapter(config)
  • request(config): 将请求拦截器 / dispatchRequest() / 响应拦截器, 通过promise链串连起来, 返回promise
  • dispatchRequest(config): 转换请求数据 ===> 调用xhrAdapter()发请求 ===> 请求返回后转换响应数据, 返回promise
  • xhrAdapter(config): 创建XHR对象, 根据config进行相应设置, 发送特定请求, 并接收响应数据, 返回promise

image.png

5. axios的请求/响应拦截器

  • 请求拦截器: 在真正发请求前, 可以对请求进行检查或配置进行特定处理的函数, 包括成功/失败的函数, 传递的必须是config。
  • 响应拦截器: 在请求返回后, 可以对响应数据进行特定处理的函数,包括成功/失败的函数, 传递的默认是response。 image.png
    // 添加请求拦截器1
    axios.interceptors.request.use(
      config => {
        console.log('request interceptor1 onResolved()')
        return config
      },
      error => {
        console.log('request interceptor1 onRejected()')
        return Promise.reject(error);
      }
    )
    // 添加请求拦截器2
    axios.interceptors.request.use(
      config => {
        console.log('request interceptor2 onResolved()')
        return config
      },
      error => {
        console.log('request interceptor2 onRejected()')
        return Promise.reject(error);
      }
    )
    // 添加响应拦截器1
    axios.interceptors.response.use(
      response => {
        console.log('response interceptor1 onResolved()')
        return response
      },
      function (error) {
        console.log('response interceptor1 onRejected()')
        return Promise.reject(error);
      }
    )
    // 添加响应拦截器2
    axios.interceptors.response.use(
      response => {
        console.log('response interceptor2 onResolved()')
        return response
      },
      function (error) {
        console.log('response interceptor2 onRejected()')
        return Promise.reject(error);
      }
    )
    // 发送请求
    axios.get('http://localhost:3000/posts')
      .then(response => {
        console.log('data', response.data)
      })
      .catch(error => {
        console.log('error', error.message)
      })
  1. InterceptorManager.js文件中可以看到,在执行axios.interceptors.request.use()axios.interceptors.response.use()时会讲添加的请求/响应拦截器的成功和失败回调函数以对象的形式一对一对的存起来;

  2. 然后在Axios.js文件中,首先拿合并后的config配置,Promise.resolve()创建一个成功的promise对象,再将请求/响应拦截器的回调函数对象存储在Axios实例的interceptors属性上,再去执行Axios.prototype.request函数:

    • 首先声明了一个chain的回调函数数组,构建promise的回调链,默认为[dispatchRequest, undefined], 其中dispatchRequest为正常发送请求的函数,区分Node端(http请求模块实现)和浏览器端(xhr对象实现),undefined为后面一对一对的取成功和失败回调函数时的占位符
    • 遍历实例对象的拦截器存储属性interceptorsrequest数组,依次添加到chain队列的前面,这也是为什么后添加的请求拦截器先执行的原因
    • 遍历实例对象的拦截器存储属性interceptorsresponse数组,依次添加到chain队列的后面
  3. 再循环遍历chain回调函数数组,从前往后每次取两个回调函数,作为上面promise对象的成功和失败回调函数循环执行,全部回调函数执行完成之后在返回promise 上面案例中添加两个请求响应的拦截器,并发送请求,组成的链式队列解析如下:

// 默认初始化的promise回调链chain:
chain: [dispatchRequest, undefined]
// 添加请求拦截器的回调函数队列:
requestInterceptors: [{
    fulfilled: fulfilledA1(){},
    rejected: rejectedB1(){}
}, {
    fulfilled: fulfilledA2(){},
    rejected: rejectedB2(){}
}]
// 添加响应拦截器的回调函数队列:
responseInterceptors: [{
    fulfilled: fulfilledC1(){},
    rejected: rejectedC2(){}
}, {
    fulfilled: fulfilledD1(){},
    rejected: rejectedD2(){}
}]
// 遍历请求/响应拦截器后组合出来的promise回调链chain:
chain: [
  fulfilledA2, rejectedB2, fulfilledA1, rejectedB1, 
  dispatchReqeust, undefined, 
  fulfilledC1, rejectedC2, fulfilledD1, rejectedD2
]
// 拿合并好的config配置生产的初始promise对象:
var promise = Promise.resolve(config)
// 通过promise的then()串连起所有的请求拦截器/请求方法/响应拦截器
while (chain.length) {
    promise = promise.then(chain.shift(), chain.shift());
}
// 所有promise回调链chain执行完成之后最终返回promise
返回的promise对象最后会在请求结束之后,执行用户自定义的成功和失败回调

6. axios的请求/响应数据转换器

  • 请求拦截器: 对请求头和请求体数据进行特定处理的函数。
    setContentTypeIfUnset:setRequestHeader(headers, 'application/json;charset=utf-8');
    request-data:JSON.stringify(data)
  • 响应拦截器: 将响应体json字符串解析为js对象或数组的函数。
    return response.data = JSON.parse(response.data)
    // 响应成功返回的数据体结构
    {
        data,
        status,
        statusText,
        headers,
        config,
        request
    }
    // 响应失败返回的数据体结构
    {
        message,
        request,
        response
    }

7. 取消未完成的请求

  1. 基本流程:
    • 配置cancelToken对象
    • 缓存用于取消请求的cancel函数
    • 在后面特定时机调用cancel函数取消请求
    • 在错误回调中用axios.isCancel()判断errorcancel的错误,做对应的逻辑处理
  2. 实现功能:
    • 执行cancel函数, 传入错误信息message
    • 内部会让cancelPromise变为成功, 且成功的值为一个Cancel对象
    • cancelPromise的成功回调中执行中断请求的处理逻辑, 并让发请求的proimse失败, 失败的reasonCancel对象
    axios.defaults.baseURL = 'http://localhost:3001';
    let cancel; // 用于保存取消请求的函数
    function getDate(){
      axios({
        url: '/user',
        cancelToken: new axios.CancelToken((c) => {
          // 参数c是用于取消当前请求的函数,保存到全局后面调用即可取消请求
          cancel = c
        })
      }).then((result) => {
        console.log('Get date:', result.data)
      }).catch((err) => {
        // err报错信息,如果err为cancel导致的错误,可以用axios.isCancel()函数检测
        if(axios.isCancel(err)){
          console.log('Get request canceled:', err.message)
        }else{
          console.log('Get err:', err)
        }
      }).finally(() => {
        // 请求结束之后将cancel重置为空
        cancel = null
      })
      setTimeout(() => {
        // 执行取消请求,如果请求已经结束,cancel已经重置为空,则什么也不做,
        // 求未结束,可以执行cancel函数,取消请求。
        cancel && cancel('请求被取消了')
      },7)
    }