谈谈axios

axios为何被广泛使用,有什么优势?如何封装,从ajax谈起

Posted by LBX on August 1, 2023

axios为何被广泛使用,与fetch及其他网络请求库的区别,有什么优势?如何封装,简单原理了解,从ajax谈起

简单了解ajax

为什么会出现?

传统的网页如果需要更新内容的话,必须重载整个页面,例如提交表单场景,然后在新页面告诉你操作是成功了还是失败了。如果碰到网速太慢或其他原因,就会得到一个404的页面了。所以ajax的出现大大改变了这种现状,可以局部刷新页面,用户体验感提升,较少网络数据的传输量,同时也节省网络带宽。

语义:Asynchronous JavaScript and XML(异步的 JavaScript 和 XML),它不是新的变成语言,而是一种使用现有标准的新方法,在不重新加载整个页面的情况下,局部更新部分页面

  • 工作原理

    在用户和服务器之间加了一个中间层(AJAX引擎),使用户操作与服务器响应异步化

  • 创建对象

    1
    2
    3
    4
    
      const xhr = new XMLHttpRequest() // xhr即为ajax实例
        
      // 旧版本的ie ie5 ie6 使用ActiveX对象
      const xhr = new ActiveXObject("Microsoft.XMLHTTP");
    
  • 请求

    向服务器发送请求,使用实例对象的open和send方法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    
      /**
       open方法的三个参数:
          method --  请求类型 GET POST PUT DELETE OPTIONS TRACE等皆可
          url -- 请求资源位置
          async -- 布尔类型 true代表异步 false代表同步
    
        对于post请求时,send可以携带body参数 例如:xhr.send('name=zs&age=18')
    
        get请求时参数可以直接写到open方法url参数用问号拼接 例如:xhr.open("GET", "xxx.txt?name=zs", true)
    
        setRequestHeader -- 设置请求头,两个参数,第一个为请求头键名 第二个为请求头的值
      */
      xhr.open("GET", "xxx.txt", true)
    
      xhr.setRequestHeader("Content-type","application/x-www-form-urlencoded");
        
      // 发送给服务器
      xhr.send()
    
  • 响应

    responseText – 获得字符串形式的响应数据 只有当 readyState 属性值变为4时,responseText 属性才可用

    responseXML – 获得XML形式的响应数据 如果来自服务器的响应是 XML,而且需要作为 XML 对象进行解析

  • onreadystatechange事件

    当发送一个请求后,客户端需要确定请求什么时候完成,此事件就是用来捕获请求状态

    每当readyState改变时,都会触发此事件

    1
    2
    3
    4
    5
    
    xhr.onreadystatechange = function() {
      if (xhr.readyState === 4 && xhr.status === 200) {
        console.log(xhr.responseText); 
      }
    }
    

    readyState的取值

    • 0 – 请求未初始化,还没有调用open方法
    • 1 – 服务器连接已建立,但没有发送,没有调用send方法
    • 2 – 请求已接收
    • 3 – 请求处理中
    • 4 – 请求已完成,且响应就绪

    status http状态码


了解axios

基于promise的请求库,作用于nodejs和浏览器中,同构应用程序(即同一套代码可以运行在浏览器和nodejs中),在服务器端使用nodejs的http模块,在客户端使用XMLHttpRequests

  • 特点

    1. 支持Promise api
    2. 转换请求和响应数据
    3. 取消请求
    4. 超时处理
    5. 自动将请求序列化
    6. 自动转换JSON数据 …
  • 基本用法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    
    // 采用ES Modules方法引入,作为举例
    
    // 安装,以下三种方式皆可
    npm install axios / bower install axios / yarn add axios
    
    // 也可CDN引入
    <script src="https://unpkg.com/axios/dist/axios.min.js"></script>
    
    // 引入
    import axios from 'axios'
    
    // 请求
    // 向给定ID的用户发起请求
    axios.get('/user?ID=12345').then(function (response) {
      // 处理成功情况
      console.log(response);
    })
    
    // 也可采用config对象的形式
    axios({
      method: 'post',
      url: '/user/12345',
      data: {
        firstName: 'Fred',
        lastName: 'Flintstone'
      }
    })
    
  • axios实例

    关于创建实例的参数, 以最新版本的axios(v1.4.0)为例

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    147
    148
    149
    150
    151
    152
    153
    154
    155
    156
    157
    158
    159
    160
    161
    162
    163
    164
    165
    166
    167
    168
    169
    170
    171
    172
    173
    174
    175
    176
    177
    178
    179
    180
    181
    182
    183
    184
    185
    186
    187
    188
    189
    190
    191
    192
    193
    
      // 请求的服务器URL
      url?: string;
        
      // 创建请求时使用的方法 get delete head options post put patch purge link unlink中的一种,大小写都可
      method?: Method | string; 
    
      // 为axios实例传递相对URL,自动拼接到请求路径前
      baseURL?: string; 
        
      // 允许在向服务器发送之前,修改请求数据,只能用于put post patch这几个请求方法
      // 数组中最后一个函数必须返回一个字符串,一个Buffer实例,ArrayBuffer,FormData或Stream
      // 可以修改请求头
      // 参数为data, headers
      transformRequest?: AxiosRequestTransformer | AxiosRequestTransformer[];
    
      // e.g.
      transformRequest: [
        function (data, headers) {
          // 对发送的数据进行任意转换处理
          return data
        }
      ]
    
    
      // 在传递给 then/catch 前,允许修改响应数据 对接收的数据进行任意处理
      transformResponse?: AxiosResponseTransformer | AxiosResponseTransformer[]; 
    
      // e.g.
      transformResponse: [
        function(data) {
    
          // 对接收的数据进行任意转换处理
          return data
        }
      ]
    
      // 请求头
      headers?: (RawAxiosRequestHeaders & MethodsHeaders) | AxiosHeaders;
        
      // 参数,params的参数会出现在url path中
      params?: any;
    
      // 用于序列化params
      paramsSerializer?: ParamsSerializerOptions | CustomParamsSerializer;
    
       // 请求body参数,该方式传的参数出现在 Request Payload中,即body中
      data?: D;
    
      // 请求时间限制
      timeout?: Milliseconds;
    
      // 请求超时提示错误信息
      timeoutErrorMessage?: string;
    
      // 跨域请求时是否需要使用凭证
      withCredentials?: boolean; 
    
      // 允许自定义处理请求,使测试更加容易
      // 返回一个promise并提供一个有效的响应 
      // 例子:https://github.com/axios/axios/blob/v1.x/lib/adapters/README.md
      adapter?: AxiosAdapterConfig | AxiosAdapterConfig[];
    
      // HTTP Basic Auth
      auth?: AxiosBasicCredentials = {
        username: string;
        password: string;
      };
    
      // 响应类型  arraybuffer blob document json text stream
      responseType?: ResponseType;
    
      // 解码响应的编码 默认值为utf8
      // ascii ansi binary base64 base64url hex latin1 ucs-2 ucs2 utf-8 utf8 utf16le 以上编码还可以转换为大写传入
      responseEncoding?: responseEncoding | string;
    
      // xsrf token 的值,被用作 cookie 的名称
      xsrfCookieName?: string;
    
      // 带有 xsrf token 值的http 请求头名称
      xsrfHeaderName?: string;
    
      // 允许为上传处理进度事件 -- 浏览器专属
      onUploadProgress?: (progressEvent: AxiosProgressEvent) => void;
    
      // 允许为下载处理进度事件 -- 浏览器专属
      onDownloadProgress?: (progressEvent: AxiosProgressEvent) => void;
    
      // AxiosProgressEvent 的定义
      export interface AxiosProgressEvent {
        loaded: number;
        total?: number;
        progress?: number;
        bytes: number;
        rate?: number;
        estimated?: number;
        upload?: boolean;
        download?: boolean;
        event?: BrowserProgressEvent;
      }
    
    
      // 定义了node.js中允许的HTTP响应内容的最大字节数
      maxContentLength?: number;
    
      // 验证响应状态 2xx 3xx 4xx 5xx等 返回一个布尔值
      validateStatus?: ((status: number) => boolean) | null;
    
      // (仅Node)定义允许的http请求内容的最大字节数
      maxBodyLength?: number;
    
      // 定义了在node.js中要遵循的最大重定向数  如果值为0 则不进行重定向
      maxRedirects?: number;
    
      // 最大上传下载速率
      maxRate?: number | [MaxUploadRate, MaxDownloadRate];
    
      // 重定向之前处理
      beforeRedirect?: (options: Record<string, any>, responseDetails: { headers: Record<string, string> }) => void;
    
      // 定义了在nodejs中使用的unix套接字
      // e.g. '/var/run/docker.sock' 发送请求到 docker 守护进程。
      // 只能指定socketPath或proxy,如果都指定,则使用socketPath
      socketPath?: string | null;
      transport?: any;
    
      // 定义执行http时要使用的自定义代理和https请求
      httpAgent?: any;
      httpsAgent?: any;
    
      // false禁用代理
      // 为对象时,host port为必填 
      proxy?: {
        host: string;
        port: number;
        auth?: {
          username: string;
          password: string;
        };
        protocol?: string;
      };
    
      // 取消请求
      cancelToken?: CancelToken;
    
      // 是否应该自动地压缩响应正文,如果设置为true还将删除 content-encoding 头部字段
      decompress?: boolean;
    
      // 属性均为非必填
      // silentJSONParsing -- 不进行json格式化 forcedJSONParsing -- 被迫进行json格式化 clarifyTimeoutError -- 声明超时错误
      transitional?: TransitionalOptions = {
        silentJSONParsing?: boolean;
        forcedJSONParsing?: boolean;
        clarifyTimeoutError?: boolean;
      };
    
      // 信号,用来配合取消请求使用
      signal?: GenericAbortSignal = {
        readonly aborted: boolean;
        onabort?: ((...args: any) => any) | null;
        addEventListener?: (...args: any) => any;
        removeEventListener?: (...args: any) => any;
      };
    
      // 不安全的http解析
      insecureHTTPParser?: boolean;
    
      // 环境
      env?: {
        FormData?: new (...args: any[]) => object;
      };
    
      // 表单序列化
      formSerializer?: FormSerializerOptions = {
        visitor?: (value, key, path, helpers) => {
          // helpers = {
          //  defaultVisitor: { visitor, dots, metaTokens, indexes }; -- 等同于当前对象
          //  convertValue: (value: any) => any;
          //  isVisitable: (value: any) => boolean;
          // }
            
          return true
        };
        dots?: boolean;
        metaTokens?: boolean;
        indexes?: boolean | null;
      };
    
      // 家族成员
      family?: 4 | 6 | undefined;
    
      // 查找
      lookup?: ((hostname: string, options: object, cb: (err: Error | null, address: string, family: number) => void) => void) |
          ((hostname: string, options: object) => Promise<[address: string, family: number] | string>);
    

    常见创建实例方法

    1
    2
    3
    4
    
      const service = axios.create({
        baseURL: 'http://localhost:8080/',
        timeout: 10000
      })
    
  • 请求配置 – 与创建实例时参数相同

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    
      // 两种写法皆可
      // 1.
      export const getData = (params) => {
        return service.get('/api/getData', params)
      } 
    
      // 2.
      export function getData(params) {
        return service({
          url: '/api/getData',
          method: 'get',
          params
        })
      }
    
      // 使用时常见的两种方式
      import { getData } from 'xxx'
    
      import * as api from 'xxx'
    
  • 响应结构

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    
      {
        // 由服务器提供的响应
        data: {
          message: '',
          code: 200,
          data: {}
        },
    
        // 服务器响应的http状态码
        status: 200,
    
        // 服务器响应的http状态信息
        statusText: 'OK',
    
        // 服务器响应头,所有的header名称都是小写,可以使用方括号形式访问
        // response.headers['content-type']
        headers: {
          // 键值对形式
          cache-control: 'no-cache',
          content-type: 'application/json',
          ... 
        },
    
        // axios请求的配置信息
        config: {
          // 配置信息
          baseURL: '',
          method: '',
          timeout: '',
          ...
        },
    
        // 生成此响应的请求,在nodejs中它是最后一个ClientRequest实例
        // 在浏览器中则是XMLHttpRequest实例
        request: {
          // 请求信息
          readyState: 4,
          status: 200,
          timeout: 300000,
          ...
        }
      }
    
  • 拦截器

在请求发送前和响应接收前的操作可以在拦截器中进行,可以给axios自身或者axios实例添加

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
    // 返回一个id,方便使用eject方法清除拦截器
    const myInterceptor = service.interceptors.request.use(onFulfilled, onRejected, options)
    
    // e.g.
    // 创建请求拦截器
    const myInterceptor = service.interceptors.request.use(
      config => { 
        // 可在此添加请求头信息
        return config
      },
      err => {
        // 在此处理错误
      },
      {
        synchronous: false,
        runWhen: (config) => {
          // config参数继承自请求配置,即与创建实例配置一致
          return false
        }
      }
    )

    // 清除拦截器
    service.interceptors.request.eject(myInterceptor);

    // 清空请求拦截器
    service.interceptors.request.clear()
  • 错误处理
1
2
3
4
5
6
7
  import { getData } from 'xxx'

  getData().then(res => {

  }).catch(e => {
    console.log(e)
  })
  • 取消请求

官网的取消请求的例子,可以了解以下

axios拦截器封装

简易拦截器封装

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
import axios from 'axios';
import { Message as ELMessage } from 'element-ui'

const service = axios.create({
  baseURL: 'http://localhost:8081', // localhost替换成本地ip
  timeout: 100000
})


service.interceptors.request.use(config => {
    // 在此可以做请求头的处理,添加你所需要的请求头
    config.headers['userId'] = 'userId'
    config.headers['userName'] = 'userName'
    config.headers.Authorization = 'tk'
    return config
  }, 
  err => {
    // 用饿了么弹窗提示
    ELMessage({
      showClose: true,
      message: error && error.data.error.message,
      type: 'error'
    });
    return Promise.reject(error.data.error.message)
  }
)

service.interceptors.response.use(
  res => {
    const { data = {}, config = {}, headers = {} } = res || {}
    const { code, message } = data
    const { url, cancelMessage, responseType } = config

    // 对响应的code message做处理
    if (code && ![200, '200'].includes(code)) {
      return Promise.reject()
    }

    // 此外还可以对文件下载做处理 responseType
    // blob arraybuffer stream等
    if (responseType === 'blob') {}
    return data
  },
  error => {
    if(axios.isCancel(error)) {
      return ELMessage.error(error.message)
    } else {
      const { response = {} } = error
      const { status, data = {} } = response
      let { message = '系统错误' } = data

      // 这里可以提示对应状态码的错误信息,也可以跳转到指定的页面,比方说404跳转到404的页面 
      const statusMap = {
        400: '请求错误(400)',
        403: '拒绝访问(403)',
        500: '服务器错误(500)'
      }

      // 对响应状态status进行判断,判断几种特殊的值 401 - 未授权 400 - 请求错误 403 - 拒绝访问 501 - 服务未实现等状态码
      if (statusMap.hasOwnProperty(status)) {
        // 对于401后直接退出
        if(status === 401) {
          // logout
        } else {
          let _message = message || statusMap[status]
          ELMessage.error(_message)
        }

        // 弹出提示信息框
      } else {
        ELMessage.error(message)
      }

      return Promise.reject(error)
    }
  }
)

export default service

接口防抖 - 即统一处理取消请求

一段时间内同样的接口(包含params和body参数及参数值)只能请求一次,如果重复相同的请求则取消后续的请求 取消的请求还是会发出该请求,但浏览器不接受该响应了,实际并未真正意义上的取消

关于取消请求,有多种方式,axios从v0.22.0开始已经支持以fetch API方式 – AbortController 取消请求,至于多种取消请求的方式,可以参考官网的例子自己进行拓展

这里我采用的方式是new axios.CancelToken的方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
// 第一个版本  axios + lodash
import axios from 'axios'
import { isEqual } from 'lodash'

class CancelToken {
  constructor({ timeout = 500 } = {}) {
    this.requestMap = [] // 请求列表
    this.timeout = timeout // 防抖时间,在timeout时间范围内的后续同样(url,method,data,params均一致)请求会被取消
  }

  // 每个请求添加唯一值,之后用作判断
  uniqueKey(config) {
    // 解构,config为请求拦截的config配置,这里data和params给了默认值
    const { url, method, data = {}, params = {} } = config || {}
    let uniqueKey = `${url}-${method}-${JSON.stringify(data)}-${JSON.stringify(params)}`
    return uniqueKey
  }

  // 添加处理
  append(config) {
    const key = this.uniqueKey(config)

    // 寻找与当前请求一致的之前的请求,用key做比较
    const index = this.requestMap.findIndex(item => isEqual(item.key, key))

    // 采用官网提供的 new axios.CancelToken 方式
    config.cancelToken = new axios.CancelToken(executor => {
      if (index === -1) {
        // 如果没有则直接添加
        this.requestMap.push({
          fun: executor,
          key,
          config,
          time: new Date().getTime()
        })
      } else {
        // 如果有的话则查看是否在时间范围内
        if (new Date().getTime() - this.requestMap[index].time < this.timeout) {
          // 直接取消,这里不需要再对data和params参数及参数值作比较,前面寻找索引的时候已经通过key对比
          // key里包含了data和params等
          executor('取消请求')
        } else {
          // 更新时间,方便下次进行时间比较,否则会一直进入该else判断
          this.requestMap[index].time = new Date().getTime()
        }
      }
    })
  }
}

export default CancelToken


// 第二种:
import axios from 'axios'
import { isEqual } from 'lodash'

class CancelToken {
  constructor({ timeout = 500 } = {}) {
    this.pending = []
    this.timeout = timeout
  }

  // 比较配置是否相等,返回布尔值
  isEqual(config1, config2) {
    const urlEqual = config1.url === config2.url
    const methodEqual = config1.method === config2.method
    const dataEqual = isEqual(config1.data, config2.data)
    const paramsEqual = isEqual(config1.params, config2.params)
    return urlEqual && methodEqual && dataEqual && paramsEqual
  }

  // 添加请求,不讲之前有无此请求,一律进行添加,判断交给remove方法
  append(config) {
    config.cancelToken = new axios.CancelToken(executor => {
      this.pending.push({
        fun: executor,
        config,
        time: new Date().getTime()
      })
    })
  }

  // 去除请求
  remove(config) {
    // 过滤掉不在时间范围内的, 就只剩下在时间范围内的了
    this.pending = this.pending.filter(item => item && new Date().getTime() - item.time < this.timeout)

    // 在时间范围内的进行循环,比较有无一致(url、method、data、params)的请求,如果有,则进行取消
    for (let i = 0; i < this.pending.length; i++) {
      // 书写采用es6可选链运算符,如果不支持,可以选择安装插件,或者改为 && 判断也可
      if (this.isEqual(this.pending[i]?.config, config) && this.pending[i]?.config !== config) {
        const index = this.pending.findIndex(i => i?.config === config)
        this.pending[index]?.fun?.('取消请求')
        this.pending[i] = null
      }
    }
  }

  prevent(config) {
    this.append(config)
    this.remove(config)
  }
}

export default CancelToken



// 使用 -- 在axios拦截器页面进行引入,将config传入
import CancelToken from './cancelToken.js'

const cancelToken = new CancelToken({}) // 可以传入配置对象,当前仅支持timeout配置,如果不提供配置对象,timeout默认500

// 创建axios实例
const service = axios.create({
  baseURL: 'http://localhost:8081', // localhost替换成本地ip
  timeout: 100000
})

service.interceptors.request.use(config => {
  // 第一种使用
  cancelToken.append(config)

  // 第二种使用
  cancelToken.prevent(config)

  return config
})


两种方法皆可实现取消请求的功能

axios取消请求,实际并未真正的取消,而是将浏览器中的接口状态 status,修改为canceled,响应还会发出,浏览器不对此接口做回应

如果项目中没有使用到lodash库的话,我觉得可以手写一个比较两个对象的方法,下面是手写比较方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
function isEqual(value, other) {

  function isObjectLike(value) {
    return value != null && typeof value == 'object'
  }

  if (value == null || other == null || (!isObjectLike(value) && !isObjectLike(other))) {
    return value !== value && other !== other
  }

  const toStr = Object.prototype.toString,
    objTag = '[object Object]',
    arrTag = '[object Array]'

  if(((toStr.call(value) !== objTag) && (toStr.call(other) !== objTag)) || (toStr.call(value) !== arrTag) && (toStr.call(other) !== arrTag)) {
    return value === other || JSON.stringify(value) === JSON.stringify(other)
  }

  let valueKeys = Object.keys(value),
    otherKeys = Object.keys(other)

  if(valueKeys.length !== otherKeys.length) {
    return false
  }

  for (let i = 0; i < valueKeys.length; i ++) {
    let key = valueKeys[i]
    let item1 = value[key],
      item2 = other[key]

    if(((toStr.call(item1) === objTag) && (toStr.call(item2) === objTag)) || (toStr.call(item1) === arrTag) && (toStr.call(item2) === arrTag)) {
      // 值是对象或数组则再次进行比较
      return isEqual(item1, item2)
    } else if(item1 === item2) {
      return true
    } else {
      return false
    }
  }
}

let obj1 = { 'b': { 'c': { 'd': 333 } } }
let obj2 = { 'b': { 'c': { 'd': 222 } } }

console.log(isEqual(obj1, obj2)); // false

let arr1 = [1,2,{'a': 12, 'b': [1,2,3]}]
let arr2 = [1,2,{'a': 12, 'b': [1,2,3]}]

console.log(isEqual(arr1, arr2)); // true

有什么不足,需要改进的地方请在评论区中指出,后继会完善,感谢!文章已同步至github.io感兴趣的可以前往查看。

参考