项目中你不知道的Axios骚操作(手写核心原理、兼容性)

3,580 阅读12分钟

前言

  • 【音乐博客】上线啦!
  • 相信在面试的时候经常被问到axios是什么?有什么特性?请求和响应拦截器?
  • 下面全方面介绍Axios相关知识(包含手写核心原理、兼容性)


手写Axios核心原理

Axios是什么?

Axios是一个基于promise的HTTP库,可运行在浏览器、Node环境中。

Axios有什么特性?(面试要考的)

  • 从浏览器中创建XMLHttpRequests
  • 从Node创建HTTP请求
  • 支持Promise API
  • 拦截请求和响应
  • 转换请求数据和响应数据
  • 取消请求
  • 自动转换Json数据
  • 客户端支持防御XSRF

实际上,axios可以用在浏览器和 node.js 中是因为,它会自动判断当前环境是什么,如果是浏览器,就会基于 XMLHttpRequests 实现axios。如果是node.js环境,就会基于node 内置核心模块http 实现axios

简单来说,axios的基本原理就是

  • axios还是属于 XMLHttpRequest, 因此需要实现一个ajax。或者基于http 。
  • 还需要一个promise对象来对结果进行处理。

基本使用方式

axios基本使用方式主要有

  • axios(config)
  • axios.method(url, data, config)

myAxios.js文件

// index.html文件
<html>
<script type="text/javascript" src="./myaxios.js"></script>
<body>
<button class="btn">点我发送请求</button>
<script>
    document.querySelector('.btn').onclick = function() {
        // 分别使用以下方法调用,查看myaxios的效果
        axios.post('/postAxios', {
          name: '小美post'
        }).then(res => {
          console.log('postAxios 成功响应', res);
        })

        axios({
          method: 'post',
          url: '/getAxios'
        }).then(res => {
          console.log('getAxios 成功响应', res);
        })
    }
</script>
</body>
</html>
</html>

实现Axios和Axios.method

从axios(config)的使用上可以看出导出的axios是一个方法。从axios.method(url, data , config)的使用可以看出导出的axios上或者原型上挂有get,post等方法。

实际上导出的axios就是一个Axios类中的一个方法。

如代码所以,核心代码是request。我们把request导出,就可以使用axios(config)这种形式来调用axios了。

class Axios {
    constructor() {

    }

    request(config) {
        return new Promise(resolve => {
            const {url = '', method = 'get', data = {}} = config;
            // 发送ajax请求
            const xhr = new XMLHttpRequest();
            xhr.open(method, url, true);
            xhr.onload = function() {
                console.log(xhr.responseText)
                resolve(xhr.responseText);
            }
            xhr.send(data);
        })
    }
}

怎么导出呢?十分简单,new Axios,获得axios实例,再获得实例上的request方法就好了。

// 最终导出axios的方法,即实例的request方法
function CreateAxiosFn() {
    let axios = new Axios();
    let req = axios.request.bind(axios);
    return req;
}

// 得到最后的全局变量axios
let axios = CreateAxiosFn();

现在axios实际上就是request方法。

你可能会很疑惑,因为我当初看源码的时候也很疑惑:干嘛不直接写个request方法,然后导出呢?非得这样绕这么大的弯子。别急。后面慢慢就会讲到。

现在一个简单的axios就完成了,我们来引入myAxios.js文件并测试一下可以使用不?

//index.html
<script type="text/javascript" src="./myAxios.js"></script>

<body>
<button class="btn">点我发送请求</button>
<script>
    document.querySelector('.btn').onclick = function() {
        // 分别使用以下方法调用,查看myaxios的效果
        axios({
          method: 'get',
          url: 'https://zhengzemin.cn/dist/static/fonts/element-icons.535877f.woff'
        }).then(res => {
          console.log('getAxios 成功响应', res);
        })
    }
</script>
</body>

点击按钮,看看是否能成功获得数据。

嗯,很完美,浏览器打印了一堆信息,就是我们通过axios请求回来的

现在我们来实现下axios.method()的形式。

思路。我们可以再Axios.prototype添加这些方法。而这些方法内部调用request方法即可,如代码所示:

// 定义get,post...方法,挂在到Axios原型上
const methodsArr = ['get', 'delete', 'head', 'options', 'put', 'patch', 'post'];
methodsArr.forEach(met => {
    Axios.prototype[met] = function() {
        console.log('执行'+met+'方法');
        // 处理单个方法
        if (['get', 'delete', 'head', 'options'].includes(met)) { // 2个参数(url[, config])
            return this.request({
                method: met,
                url: arguments[0],
                ...arguments[1] || {}
            })
        } else { // 3个参数(url[,data[,config]])
            return this.request({
                method: met,
                url: arguments[0],
                data: arguments[1] || {},
                ...arguments[2] || {}
            })
        }

    }
})

我们通过遍历methodsArr数组,依次在Axios.prototype添加对应的方法,注意的是'get', 'delete', 'head', 'options'这些方法只接受两个参数。而其他的可接受三个参数,想一下也知道,get不把参数放body的。 但是,你有没有发现,我们只是在Axios的prototype上添加对应的方法,我们导出去的可是request方法啊,那怎么办? 简单,把Axios.prototype上的方法搬运到request上即可。 我们先来实现一个工具方法,实现将b的方法混入a;

const utils = {
  extend(a,b, context) {
    for(let key in b) {
      if (b.hasOwnProperty(key)) {
        if (typeof b[key] === 'function') {
          a[key] = b[key].bind(context);
        } else {
          a[key] = b[key]
        }
      }
      
    }
  }
}

然后我们就可以利用这个方法将Axios.prototype上的方法搬运到request上啦。

我们修改一下之前的 CreateAxiosFn 方法即可

function CreateAxiosFn() {
  let axios = new Axios();
  
  let req = axios.request.bind(axios);
  增加代码
  utils.extend(req, Axios.prototype, axios)
  
  return req;
}

现在来测试一下能不能使用axios.get()这种形式调用axios。

<body>
<button class="btn">点我发送请求</button>
<script>
    document.querySelector('.btn').onclick = function() {

        axios.get('https://zhengzemin.cn/dist/static/fonts/element-icons.535877f.woff')
            .then(res => {
                 console.log('getAxios 成功响应', res);
            })

    }
</script>
</body>

害,又是意料之中成功。

请求和响应拦截器

我们先看下拦截器的使用

// 添加请求拦截器
axios.interceptors.request.use(function (config) {
    // 在发送请求之前做些什么
    return config;
  }, function (error) {
    // 对请求错误做些什么
    return Promise.reject(error);
  });

// 添加响应拦截器
axios.interceptors.response.use(function (response) {
    // 对响应数据做点什么
    return response;
  }, function (error) {
    // 对响应错误做点什么
    return Promise.reject(error);
  });

拦截器是什么意思呢?其实就是在我们发送一个请求的时候会先执行请求拦截器的代码,然后再真正地执行我们发送的请求,这个过程会对config,也就是我们发送请求时传送的参数进行一些操作。

而当接收响应的时候,会先执行响应拦截器的代码,然后再把响应的数据返回来,这个过程会对response,也就是响应的数据进行一系列操作。

怎么实现呢?需要明确的是拦截器也是一个类,管理响应和请求。因此我们先实现拦截器

class InterceptorsManage {
  constructor() {
    this.handlers = [];
  }

  use(fullfield, rejected) {
    this.handlers.push({
      fullfield,
      rejected
    })
  }
}

我们是用这个语句 axios.interceptors.response.useaxios.interceptors.request.use,来触发拦截器执行use方法的。

说明axios上有一个响应拦截器和一个请求拦截器。那怎么实现Axios呢?看代码

class Axios {
    constructor() {
        新增代码
        this.interceptors = {
            request: new InterceptorsManage,
            response: new InterceptorsManage
        }
    }

    request(config) {
        return new Promise(resolve => {
            const {url = '', method = 'get', data = {}} = config;
            // 发送ajax请求
            console.log(config);
            const xhr = new XMLHttpRequest();
            xhr.open(method, url, true);
            xhr.onload = function() {
                console.log(xhr.responseText)
                resolve(xhr.responseText);
            };
            xhr.send(data);
        })
    }
}

可见,axios实例上有一个对象interceptors。这个对象有两个拦截器,一个用来处理请求,一个用来处理响应。

所以,我们执行语句 axios.interceptors.response.useaxios.interceptors.request.use 的时候,实现获取axios实例上的interceptors对象,然后再获取response或request拦截器,再执行对应的拦截器的use方法。

而执行use方法,会把我们传入的回调函数push到拦截器的handlers数组里。

到这里你有没有发现一个问题。这个interceptors对象是Axios上的啊,我们导出的是request方法啊(欸?好熟悉的问题,上面提到过哈哈哈~~~额)。处理方法跟上面处理的方式一样,都是把Axios上的方法和属性搬到request过去,也就是遍历Axios实例上的方法,得以将interceptors对象挂载到request上。

所以只要更改下 CreateAxiosFn 方法即可。

function CreateAxiosFn() {
  let axios = new Axios();
  
  let req = axios.request.bind(axios);
  // 混入方法, 处理axios的request方法,使之拥有get,post...方法
  utils.extend(req, Axios.prototype, axios)
  新增代码
  utils.extend(req, axios)
  return req;
}

好了,现在request也有了interceptors对象,那么什么时候拿interceptors对象中的handler之前保存的回调函数出来执行。

没错,就是我们发送请求的时候,会先获取request拦截器的handlers的方法来执行。再执行我们发送的请求,然后获取response拦截器的handlers的方法来执行。

因此,我们要修改之前所写的request方法

之前是这样的。

request(config) {
    return new Promise(resolve => {
        const {url = '', method = 'get', data = {}} = config;
        // 发送ajax请求
        console.log(config);
        const xhr = new XMLHttpRequest();
        xhr.open(method, url, true);
        xhr.onload = function() {
            console.log(xhr.responseText)
            resolve(xhr.responseText);
        };
        xhr.send(data);
    })
}

但是现在request里不仅要执行发送ajax请求,还要执行拦截器handlers中的回调函数。所以,最好下就是将执行ajax的请求封装成一个方法

request(config) {
    this.sendAjax(config)
}
sendAjax(config){
    return new Promise(resolve => {
        const {url = '', method = 'get', data = {}} = config;
        // 发送ajax请求
        console.log(config);
        const xhr = new XMLHttpRequest();
        xhr.open(method, url, true);
        xhr.onload = function() {
            console.log(xhr.responseText)
            resolve(xhr.responseText);
        };
        xhr.send(data);
    })
}

好了,现在我们要获得handlers中的回调

request(config) {
    // 拦截器和请求组装队列
    let chain = [this.sendAjax.bind(this), undefined] // 成对出现的,失败回调暂时不处理

    // 请求拦截
    this.interceptors.request.handlers.forEach(interceptor => {
        chain.unshift(interceptor.fullfield, interceptor.rejected)
    })

    // 响应拦截
    this.interceptors.response.handlers.forEach(interceptor => {
        chain.push(interceptor.fullfield, interceptor.rejected)
    })

    // 执行队列,每次执行一对,并给promise赋最新的值
    let promise = Promise.resolve(config);
    while(chain.length > 0) {
        promise = promise.then(chain.shift(), chain.shift())
    }
    return promise;
}

我们先看拦截器是否生效,再解释代码

<script type="text/javascript" src="./myAxios.js"></script>

<body>
<button class="btn">点我发送请求</button>
<script>
    // 添加请求拦截器
    axios.interceptors.request.use(function (config) {
        // 在发送请求之前做些什么
        config.method = "get";
        console.log("被我请求拦截器拦截了,哈哈:",config);
        return config;
    }, function (error) {
        // 对请求错误做些什么
        return Promise.reject(error);
    });

    // 添加响应拦截器
    axios.interceptors.response.use(function (response) {
        // 对响应数据做点什么
        console.log("被我响应拦截拦截了,哈哈 ");
        response = {message:"响应数据被我替换了,啊哈哈哈"}
        return response;
    }, function (error) {
        // 对响应错误做点什么
        console.log("错了吗");
        return Promise.reject(error);
    });
    document.querySelector('.btn').onclick = function() {
        // 分别使用以下方法调用,查看myaxios的效果
        axios({
          url: 'https://zhengzemin.cn/dist/static/fonts/element-icons.535877f.woff'
        }).then(res => {
          console.log('response', res);
        })
    }
</script>
</body>

拦截成功!!!

下面进行代码理解

我们先把sendAjax请求和undefined放进了chain数组里,再把请求拦截器的handlers的成对回调放到chain数组头部。再把响应拦截器的handlers的承兑回调反倒chain数组的尾部。

然后再 逐渐取数 chain数组的成对回调执行。

promise = promise.then(chain.shift(), chain.shift())

这一句,实际上就是不断将config从上一个promise传递到下一个promise,期间可能回调config做出一些修改。什么意思?我们结合一个例子来讲解一下

首先拦截器是这样使用的

// 添加请求拦截器
axios.interceptors.request.use(function (config) {
    // 在发送请求之前做些什么
    return config;
  }, function (error) {
    // 对请求错误做些什么
    return Promise.reject(error);
  });

// 添加响应拦截器
axios.interceptors.response.use(function (response) {
    // 对响应数据做点什么
    return response;
  }, function (error) {
    // 对响应错误做点什么
    return Promise.reject(error);
  });

然后执行request的时候。chain数组的数据是这样的

chain = [
  function (config) {
    // 在发送请求之前做些什么
    return config;
  }, 
  
  function (error) {
    // 对请求错误做些什么
    return Promise.reject(error);
  }
  this.sendAjax.bind(this), 
  
  undefined,
  
  function (response) {
    // 对响应数据做点什么
    return response;
  }, 
  function (error) {
    // 对响应错误做点什么
    return Promise.reject(error);
  }
]

首先 执行第一次promise.then(chain.shift(), chain.shift()),即

promise.then(
  function (config) {
    // 在发送请求之前做些什么
    return config;
  }, 
  
  function (error) {
    // 对请求错误做些什么
    return Promise.reject(error);
  }
)

一般情况,promise是resolved状态,是执行成功回调的,也就是执行

function (config) {
    // 在发送请求之前做些什么
    return config;
  }, 

而promise.then是要返回一个新的promise对象的。

为了区分,在这里,我会把这个新的promise对象叫做第一个新的promise对象

这个第一个新的promise对象会把

function (config) {
    // 在发送请求之前做些什么
    return config;
  }, 

的执行结果传入resolve函数中

resolve(config)

使得这个返回的第一个新的promise对象的状态为resovled,而且第一个新的promise对象的data为config。

接下来,再执行

promise.then(
  sendAjax(config)
  ,
  undefined
)

注意:这里的promise是 上面提到的第一个新的promise对象。

而promise.then这个的执行又会返回第二个新的promise对象。

因为这里promise.then中的promise也就是第一个新的promise对象的状态是resolved的,所以会执行sendAjax()。而且会取出第一个新的promise对象的data 作为config转入sendAjax()。

当sendAjax执行完,就会返回一个response。这个response就会保存在第二个新的promise对象的data中。

接下来,再执行

promise.then(
  function (response) {
    // 对响应数据做点什么
    return response;
  }, 
  function (error) {
    // 对响应错误做点什么
    return Promise.reject(error);
  }
)

同样,会把第二个新的promise对象的data取出来作为response参数传入

function (response) {
    // 对响应数据做点什么
    return response;
  }, 

饭后返回一个promise对象,这个promise对象的data保存了这个函数的执行结果,也就是返回值response。

然后通过return promise;

把这个promise返回了。咦?是怎么取出promise的data的。我们看看我们平常事怎么获得响应数据的

axios.get('https://zhengzemin.cn/dist/static/fonts/element-icons.535877f.woff')
    .then(res => {
         console.log('getAxios 成功响应', res);
    })

在then里接收响应数据。所以原理跟上面一样,将返回的promise的data作为res参数了。

最后,献上myAxios完整代码,好有个全面的了解

myAxios核心代码

调用myAxios代码(可搜索initHandleWrite,在initHandleWrite方法的手写ajax测试里面)

Axios的兼容性处理

在安卓4.3及以下的手机不支持axios的使用

项目中发现,在安卓4.3及以下的手机不支持axios的使用,主要就是无法使用promise。加上以下polyfill就可以了。

项目中安装es6-promise

cnpm install es6-promise --save-dev

axios.min.js开头加上

require('es6-promise').polyfill();

在ie浏览器下的兼容问题

虽然说axios是支持ie8+的,但是由于其原理是基于promise之上的,当项目中用到es6语法等新知识时,会有不兼容ie的情况出现,在控制台查看,会发现报 “promise"未定义 等错。。

解决方法

在项目中安装babel-polyfill

npm install --save babel-polyfill

在main.js中引入 import "babel-polyfill"

修改配置文件,即在webpack.base.conf.js这个文件中加入代码 require("babel-polyfill")

完成以上步骤后,重启下项目,在ie浏览器下数据便能正常获取了~~~

Axios取消上一次请求

使用场景:
1.重复点击(我们只希望请求一次)
2.分页(我们又很快的速度点击第三页,迅速点击第四页,有可能我们请求第四页的数据回来了,第三页的数据后回来,这样子就显示第三页的数据,可我们点击的是第四页的数据呀)

3.在写地图的时候,有个时间轴,上面有很多时间,疯狂点击时间,很难说哪个时间先被请求回来,所以我们要将上一次请求取消,只请求最后的请求
4.input实时查询,每输入一个值请求一次(可失去焦点最后请求)

相信大家已经知道取消接口请求的场景了,接下来介绍一下

Axios官方提供了一个取消接口请求的方法

Axios 提供了一个 CancelToken的函数,这是一个构造函数,该函数的作用就是用来取消接口请求的,至于怎么用,看代码吧,我在代码中写了注解

data(){
  return {
    cancel: this.axios.CancelToken.source()
  }
},
methods:{
  getData(){
    this.cancel.cancel();  // 取消上一次请求
    this.cancel = this.axios.CancelToken.source();  //重新添加请求
    
    this.axios.get(url,{
      params: query,
      cancelToken: this.cancel.token
    }).then(res => console.log(res))
    .catch(err => {
      // 如果手动取消请求的话,他不会走then,走的是catch
      // 这里的判断是,当我们手动取消请求才会进入if,可以额外做下判断
      if(err.toString() == 'Cancel'){
        
      }
    })
    
  }
}

这样的好处:

  • 不管我点击tab切换的时候,网络敢延迟,我就敢掐掉你的请求,保证我下一个请求不被影响
  • 用很快的速度点击分页都不会有任何影响
  • 开发的时候会遇到一个重复点击的问题,短时间内多次点击同一个按钮发送请求会加重服务器的负担,消耗浏览器的性能,多以绝大多数的时候我们需要做一个取消重复点击的操作
  • 在地图上的时间轴如何点击,只要取消上一次请求,只需要请求最后一次请求即可

原文链接

juejin.im/post/686374…

参考文档

手写axios核心原理,再也不怕面试官问我axios原理

axios的兼容性处理

在vue项目中使用axios,解决axios在ie下的兼容性问题

axios取消接口请求

axios官网介绍