谈谈axios

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

简单了解ajax

为什么会出现?

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

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

  • 工作原理

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

  • 创建对象

    js 复制代码
      const xhr = new XMLHttpRequest() // xhr即为ajax实例
      
      // 旧版本的ie ie5 ie6 使用ActiveX对象
      const xhr = new ActiveXObject("Microsoft.XMLHTTP");
  • 请求

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

    js 复制代码
      /**
       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改变时,都会触发此事件

    js 复制代码
    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数据 ...
  • 基本用法

    js 复制代码
    // 采用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)为例

    ts 复制代码
      // 请求的服务器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>);

    常见创建实例方法

    js 复制代码
      const service = axios.create({
        baseURL: 'http://localhost:8080/',
        timeout: 10000
      })
  • 请求配置 -- 与创建实例时参数相同

    js 复制代码
      // 两种写法皆可
      // 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'
  • 响应结构

    js 复制代码
      {
        // 由服务器提供的响应
        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实例添加

js 复制代码
  // 返回一个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()
  • 错误处理
js 复制代码
 import { getData } from 'xxx'

 getData().then(res => {

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

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

axios拦截器封装

简易拦截器封装

js 复制代码
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的方式

js 复制代码
// 第一个版本  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库的话,我觉得可以手写一个比较两个对象的方法,下面是手写比较方法

js 复制代码
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

有什么不足,需要改进的地方请在评论区中指出,或者大家详看关于axios的什么内容,后继会完善补充上,感谢!文章已同步至github博客首页感兴趣的可以前往查看。

参考

相关推荐
前端小小王24 分钟前
React Hooks
前端·javascript·react.js
迷途小码农零零发34 分钟前
react中使用ResizeObserver来观察元素的size变化
前端·javascript·react.js
娃哈哈哈哈呀1 小时前
vue中的css深度选择器v-deep 配合!important
前端·css·vue.js
旭东怪1 小时前
EasyPoi 使用$fe:模板语法生成Word动态行
java·前端·word
ekskef_sef3 小时前
32岁前端干了8年,是继续做前端开发,还是转其它工作
前端
sunshine6413 小时前
【CSS】实现tag选中对钩样式
前端·css·css3
真滴book理喻4 小时前
Vue(四)
前端·javascript·vue.js
蜜獾云4 小时前
npm淘宝镜像
前端·npm·node.js
dz88i84 小时前
修改npm镜像源
前端·npm·node.js
Jiaberrr4 小时前
解锁 GitBook 的奥秘:从入门到精通之旅
前端·gitbook