手写简单实现axios

什么是 axios

关于 axios 是什么,使用 Vue 开发的同学应该不会陌生,具体的 API 也可以参考axios 中文文档

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

(这里只针对浏览器的原理进行阐述)

手写实现

创建本地文件

demo.html

<button id="btnId">请求数据</button>

node 服务模拟数据

// httpServer.js
let express = require('express')
let app = express()

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

app.get('/', (req, res) => {
  res.send('首页')
})

app.get('/get/info', (req, res) => {
  res.send({
    code: 200,
    data: {
      sunny: '阳光'
    }
  })
})

app.listen(3000, () => {
  console.log('服务器启动成功...')
})

axios实现

axios({})

axios 的原理还是创建 XMLHttpRequest

// axios.js
class Axios { 
  constructor() { 

  } 
  request(config) { 
    return new Promise(resolve => { 
      const {url = '', method = 'get', data = {}} = config
      const xhr = new XMLHttpRequest()
      xhr.open(method, url, true)
      xhr.onload = function() { 
        resolve(xhr.responseText)
      } 
      xhr.send()
    }) 
  } 
} 

function createInstance() { 
  let axios = new Axios()
  let req = axios.request.bind(axios)
  return req
} 

let axios = CreateAxiosFn() // demo.html中调用的就是这个axios 全局变量

说明

  • 这里创建一个 axios 实例,原型上有一个 request 方法,使用 axios({}) 请求接口,我们需要调用的其实是 request,所以需要返回 request 这个方法
  • bind 方法返回的是一个函数,fun.bind(context) 返回的其实还是这个 fun,只是改变了它内部的作用到 context上而已
  • 有个疑问:为什么不直接返回实例的方法 axios.request ,而非要用bind来?

现在我们点击按钮可以返回数据了。

// demo.html
<button id="btnId">请求数据</button>

<script src="axios.js"></script>
<script>
  document.querySelector('#btnId').onclick = () => {
    axios({
      method: 'get',
      url: 'http://localhost:3000/get/info'
    }).then(res => {
      console.log('响应数据,', res)
    })
  }
</script>

到此完成了 axios({}) 形式调用。

axios.get()

const methodsArr = ['get', 'delete', 'head', 'options', 'put', 'patch', 'post']
methodsArr.forEach(met => {
  Axios.prototype[met] = function() {
    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] || {} 
      }) 
    } 
  }
})

现在在 Axios 的 prototype 上添加了对应的方法,但是 axios 实例返回出去的是 request 方法啊,也就是说这个返回出去的 request 方法需要有 Axios.prototype上 这些方法,如何做呢?
Axios.prototype 的方法搬运到 request 上去
下面的工具方法就可以做到

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] 
        } 
      } 
    } 
  } 
}

然后修改 createInstance 方法

function createInstance() { 
  let axios = new Axios()
  let req = axios.request.bind(axios)
  utils.extend(req, Axios.prototype, axios) // 混入,使得axios的request拥有get,post...等方法
  return req; 
} 

此时我们可以使用 axios.get(‘xxx’) 来调用了

document.querySelector('#btnId').onclick = () => {
  axios.get('http://localhost:3000/get/info').then(res => {
    console.log('响应数据,', res)
  })
}

请求和响应拦截器

我们发送一个请求时,会先执行请求拦截器的代码,然后执行我们的发送代码。
返回时会先执行响应拦截器的代码对数据进行一个过滤筛选,然后再把数据返回。

使用过拦截器的知道,我们需要通过 axios.interceptors.request.useaxios.interceptors.response.use 来调用,也就是说 axios 有个 interceptors 对象,这个对象有两个拦截器,一个处理请求,一个处理响应,所有 Axios 的构造函数可以写成

class Axios { 
  constructor() {
    this.interceptors = {
      request: new InterceptorsManage,
      response: new InterceptorsManage
    }
  } 
  request(config) { 
    ... 
  } 
} 

而且两个拦截器都会调用use 方法,当执行 use 的时候会将我们传入的回调函数 push 到拦截器中处理。
拦截器也是一个构造函数

class InterceptorsManage{
  constructor() {
    this.handlers = []
  }
  use(fillfield, rejected) {
    this.handlers.push({fillfield, rejected})
  }
}

上面我们知道,我们返回的 axios 实例其实是 request 方法,所以还是需要将拦截器搬到 request 上去

function createInstance() { 
  let axios = new Axios(); 
  let req = axios.request.bind(axios); 
  utils.extend(req, Axios.prototype, axios)
  utils.extend(req, axios) // 混入,使axios的request拥有interceptor拦截器
  return req; 
} 

发送请求时,会先获取 request 拦截器的的方法来执行,再执行我们发送的请求,再获取 response 拦截器的方法来执行
因为需要重写我们的请求方法 request
先将 ajax 单独提取出来,用来插入执行的队列中

class Axios { 
  constructor() {
    this.interceptors = {
      request: new InterceptorsManage,
      response: new InterceptorsManage
    }
  } 
  request(config) { 
    let chain = [this.sendAjax.bind(this), undefined]
    // 请求拦截
    this.interceptors.request.handlers.forEach(interceptor => {
      chain.unshift(interceptor.fillfield, interceptor.rejected)
    })
    // 响应拦截
    this.interceptors.response.handlers.forEach(interceptor => {
      chain.push(interceptor.fillfield, interceptor.rejected)
    })
    // 执行队列
    let resolvePromise = Promise.resolve(config)
    while(chain.length > 0){
      // 不断对 config 从上一个promise传递到下一个 promise
      // Promise.resolve() 返回是一个新的 promise 对象
      resolvePromise = resolvePromise.then(chain.shift(), chain.shift()) 
    }
    return resolvePromise
  } 
  sendAjax(config) {
    return new Promise(resolve => { 
      const {url = '', method = 'get', data = {}} = config; 
      const xhr = new XMLHttpRequest(); 
      xhr.open(method, url, true); 
      xhr.onload = function() { 
        resolve(xhr.responseText); 
      } 
      
      xhr.send(); 
    }) 
  }
} 

此时拦截器已经起作用了

axios.interceptors.request.use(function(config) {
  console.log('请求拦截器先走,', config)
  return config
})
axios.interceptors.response.use(function(data) {
  console.log('拦截到响应数据了,', data)
  return data
})

document.querySelector('#btnId').onclick = () => {
  axios.get('http://localhost:3000/get/info').then(res => {
    console.log('响应数据,', res)
  })
}

取消请求

先看使用

const axios_cancel = axios.CancelToken
const source = axios_cancel.source()

axios.interceptors.request.use(function(config) {
  console.log('请求拦截器先走,', config)
  return config
})
axios.interceptors.response.use(function(data) {
  console.log('拦截到响应数据了,', data)
  return data
})

document.querySelector('#btnId').onclick = () => {
  axios.get('http://localhost:3000/get/info', {
    cancelToken: source.token
  }).then(res => {
    console.log('响应数据,', res)
  })
}
// 取消
source.cancel()

取消请求也是在 request 阶段,因为我们的请求之后是需要传给下一阶段拦截器 response 的,所有还是需要返回一个 promise

sendAjax(config) {
  return new Promise((resolve, reject) => { 
    const {url = '', method = 'get'} = config; 
    const xhr = new XMLHttpRequest(); 
    xhr.open(method, url, true); 
    xhr.onload = function() { 
      resolve(xhr.responseText); 
    } 
    xhr.send();
    // 取消请求
    if(config.cancelToken) {
      config.cancelToken.promise.then(() => {
        xhr.abort()
        resolve('请求取消了')
      })
    }
  }) 
}

cancelToken需要返回一个promise的resolve 方法

function CancelToken(executor) {
  let resolvePromise
  this.promise = new Promise(resolve => {
    resolvePromise = resolve
  })
  executor(resolvePromise)
}
CancelToken.source = function() {
  let cancel;
  let token = new CancelToken(c => {
    cancel = c
  })
  return {token, cancel}
}

var axios = createInstance(); 
axios.CancelToken = CancelToken

这里 CancelToken 构造函数中需要传一个函数,作用是将控制内部 Promise 的 resolve 暴露出去给 source.cancel 函数,这样内部的 Promise 状态就可以通过source.cancel方法来控制,所以说如果接口处不调用 source.cancel 请求是不会 abort

  • 3
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值