导语

Axios 是一个基于 promise 的 HTTP 库,可以用在浏览器和 node.js 中。

  • 支持 Promise API
  • 拦截请求和响应
  • 转换请求数据和响应数据

说白了,axios就是使用Promise封装的XHR。

想要复刻我们首先要知道使用方法,如果你还不会使用axios,具体使用方法可以看我之前发的Axios

axios和axios.method

首先实现axios是一个方法,参数形式有三种,url、url+option、option,这里需要通过arguments来获取参数,然后判断参数的长度和类型进行不同的处理,这里就写一个大概的逻辑

然后,axios的返回值是一个Promise对象,所以函数的返回结果需要使用new Promise包装一下

Axios.prototype.request = function () {
  // 处理参数(粗略)
  const args = Array.from(arguments);
  let config = {}
  if (args.length === 0) {
    throw new Error('[axios]参数不能为空')
  } else if (args.length === 1) {
    if (typeof args[0] === 'string') {
      config.url = args[0];
      config.method = 'get'
    } else if (Object.prototype.toString.call(args[0]) === '[object Object]') {
      config = args[0]
    } else {
      throw new Error('[axios]参数格式错误')
    }
  } else {
    if (typeof args[0] === 'string' && Object.prototype.toString.call(args[1]) === '[object Object]') {
      config = args[1]
      config.url = args[0]
    }
  }

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

axios的核心方法request已经完成了,接下来进行包装处理,在使用时axios是一个方法,而不是一个实例,也就是说我们需要将request方法导出

function createAxiosInstance() {
  const axios = new Axios();
  let request = axios.request.bind(axios);
  return request;
}

const axios = createAxiosInstance();

至于为什么要用这么绕的方法来处理,到最后你就懂了。

现在我们先来测试一下我们刚才的杰作,首先起一个server,为了方便就用express吧

const express = require('express')
const app = express()

app.all('*', function (req, res, next) {
  res.header('Access-Control-Allow-Origin', '*');
  res.header('Access-Control-Allow-Headers', 'Content-Type');
  res.header('Access-Control-Allow-Methods', '*');
  res.header('Content-Type', 'application/json;charset=utf-8');
  next();
});

app.get('/data', function (request, response) {
  data = {
    name: 'king',
    age: 36,
    sex: 'man',
    hobby: 'code'
  };
  response.json(data);
});

app.listen(5005, () => {
  console.log('server start and listen port 5005');
})

然后编写html页面,引入编写的axios文件,然后编写函数进行请求

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <script src="./axios.js"></script>
  <title>axios</title>
</head>

<body>
  <button type="button" onclick="getData()">axios</button>
  <script>
    // function getData() {
    //   axios({
    //     url: 'http://localhost:5005/data',
    //     method: 'GET'
    //   }).then(res => {
    //     console.log(res);
    //   })
    // }
    function getData() {
      axios('http://localhost:5005/data').then(res => {
        console.log(res);
      })
    }
  </script>
</body>

</html>

打开浏览器,测试一下结果,perfect

image-20210804170710802

然后是axios.method,源码对于axios.method的处理还是挺有意思的

通过遍历方法名来对Axios原型上添加方法

// 只需要url
const single = ['delete', 'get', 'head', 'options'];
// 可以发送请求体
const double = ['post', 'put', 'patch'];

single.forEach(method => {
  Axios.prototype[method] = function () {
    return this.request({
      method,
      url: arguments[0],
      ...arguments[1] || {}
    })
  }
})

double.forEach(method => {
  Axios.prototype[method] = function () {
    return this.request({
      method,
      url: arguments[0],
      data: arguments[1] || {},
      ...arguments[2] || {}
    })
  }
})

这样就在原型上添加个get、post等方法,但是注意!!!我们导出的axios并不是Axios的实例,而是一个request方法,所以我们需要将Axios原型上的方法添加到导出的request上

写一个工具函数,负责转移方法

/**
 * @description: 将第二个对象的自有属性添加到第一个属性
 * @param {object} a 第一个对象
 * @param {object} b 第二个对象
 * @param {object} thisArg this指向
 * @return {void}
 */
function extendUtil(a, b, thisArg) {
  for (let key in b) {
    if (b.hasOwnProperty(key)) {
      if (typeof b[key] === 'function') {
        a[key] = b[key].bind(thisArg);
      } else {
        a[key] = b[key]
      }
    }
  }
}

再修改一下createInstance函数

function createAxiosInstance() {
  const axios = new Axios();
  let request = axios.request.bind(axios);
  extendUtil(request, Axios.prototype, axios)
  return request;
}

这样我们就将axios.method实现出来,再来测试一下

<script>
  function getData() {
    axios.get('http://localhost:5005/data').then(res => {
      console.log(res);
    })
  }
</script>

再次成功

image-20210804205226522

拦截器

拦截器也是axios的特色之一,可以在请求发出之前和接收到响应之后进行某些操作来处理数据。

拦截器的使用方式如下

// 添加请求拦截器
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);
  });

通过使用方式我们可以看出,axios上还挂载着interceptors,其拥有request和response两个属性,通过use方法来添加拦截器

我们可以这样实现:创建一个InterceptorsManage构造函数,原型上添加use方法,axios的interceptors上挂载着两个这样的实例

function InterceptorsManage() {
  this.handlers = [];
}
InterceptorsManage.prototype.use = function (fulfilled, rejected) {
  this.handlers.push({
    fulfilled,
    rejected
  })
}

function Axios(options) {
  this.interceptors = {
    request: new InterceptorsManage(),
    response: new InterceptorsManage()
  }
}


function createAxiosInstance() {
  const axios = new Axios();
  const request = axios.request.bind(axios);
  extendUtil(request, Axios.prototype, axios);
  extendUtil(request, axios);
  return request;
}

现在我们已经实现了axios.interceptors.response.use这种结构,然后我们再来完善他的内部功能,首先我们把request的核心方法分离出来,为了方便拦截器的顺序调用

Axios.prototype.request = function (...args) {
  let chain = [sendAjax.bind(this), undefined]
  // 请求拦截
  this.interceptors.request.handlers.forEach(interceptor => {
    chain.unshift(interceptor.fulfilled, interceptor.rejected)
  })

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

  let promise = Promise.resolve(...args);
  while (chain.length > 0) {
    promise = promise.then(chain.shift(), chain.shift())
  }
  return promise;
}

拦截器两两一对,分别是操作和异常处理,将ajax请和undefined作为一对,请求拦截器的处理添加到ajax请求前面,响应拦截添加到后面,然后顺序执行,直到调用链中没有函数

let promise = Promise.resolve(...args);用来传递参数,可以将处理结果一直传递到ajax请求,然后把ajax的响应作为参数继续传递给响应拦截,然后才把最终结果返回

我们再来测试一下,在html中添加拦截器

<script>
  axios.interceptors.request.use(function (config) {
    // 在发送请求之前做些什么
    config.url += '?a=1'
    return config;
  }, function (error) {
    // 对请求错误做些什么
    return Promise.reject(error);
  });

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

  function getData() {
    axios.get('http://localhost:5005/data').then(res => {
      console.log(res);
    })
  }
</script>

在浏览器中看一下结果

image-20210804231455021

大功告成


前端小白