axios核心原理分析

1,847 阅读9分钟

一、概述

1、什么是axios?

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

2、axios有什么特性?

  • 从浏览器中创建XMLHTTPRequests
  • 从node.js中创建http请求
  • 支持Promise API
  • 拦截请求和响应
  • 转换请求和响应数据
  • 自动转换JSON数据
  • 客户端支持防御XSRF

二、准备工作

  1. 首先新建一个mini-axios目录
  2. 在该根目录下新建一个server.js文件,内容如下:
var express = require('express')
var app = express()
var path = require('path')

// 读取静态资源路径
app.use(express.static(path.join(__dirname, 'src')))

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('/getTest', function (request, response) {
  data = {
    'fontEnd': '前端',
    'suuny': 'zbq'
  }
  setTimeout(() => {
     response.json(data);
  }, 4000)
})

var server = app.listen(5000, function () {
  console.log('*********server start*********')
})

  1. 新建一个src/interceptors.js、src/myAxios.js文件
  2. 新建src/index.html文件,内容如下:
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>

<body>
<button class="btn">点我发送请求</button>
<script type="text/javascript" src="./interceptors.js"></script>
<script type="text/javascript" src="./myAxios.js"></script>
<script>
    document.querySelector('.btn').onclick = function() {
    }
</script>
</html>
  1. 在当前目录下打开终端,执行npm init -y 初始化package.json文件
  2. 安装 express(npm i express)
  3. 执行npm start(启动server.js)

在浏览器中输入:http://localhost:5000/index.html 能正常访问

准备工作完成

三、实现一个简单的axios

1、实现axios与axios.method方法

1) 新建一个Axios类,并实现核心方法request方法(也就是ajax封装)

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 () {
        console.log('a:', xhr.responseText)
        resolve(xhr.responseText);
      }
      xhr.send(data)
    })
  }
}

2) 导出一个axios方法,通过查看源码,它实际上导出的是axios的request方法

function createInstance () {
  var axios = new Axios()
  var req = axios.request.bind(axios)
  return req
}
var axios = createInstance()
export default axios

3) 在Axios原型上添加get、post等方法,这些方法内部实际上调用的还是Axios上的request方法

var methodsArr = ['get','delete', 'head', 'options', 'put', 'patch', 'post']
methodsArr.forEach(met => {
  Axios.prototype[met] = function () {
    if(['get','delete', 'head', 'options'].includes(met)) {
      return this.request({
        method: met,
        url: arguments[0],
        ...arguments[1] || {}
      })
    } else {
      return this.request({
        method: met,
        url: arguments[0],
        data: arguments[1],
        ...arguments[2] || {}
      })
    }
  }
})

4) 由于此时只有Axios.prototype才有get、post这些方法,但是导出的request方法没有怎么办呢?源码中是通过util.extend方法,将Axios.prototype中的方法直接copy到request上

var util = {
  extend (a, b, context) {
    for(let key in b){
      if (b.hasOwnProperty(key)) {
        if (typeof b[key] === 'function') {
          a[key] = b[key].bind(context) // 运行request中copy过来的方法,方法中的this指向的还是axios实例
        } else {
          a[key] = b[key]
        }
      }
    }
  }
}

function createInstance () {
  var axios = new Axios()
  var req = axios.request.bind(axios)
  // 新增
  util.extend(req, Axios.prototype, axios) // 将Axios原型上的方法搬运到request中
  return req
}

至此,axios(实际上是request),axios.method方法已经实现(不得不说这里用的实在巧妙了)

2、实现请求与响应拦截器

首先看一个拦截器的使用

// 添加请求拦截器
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做些处理。

响应拦截也是如此,在请求响应返回data数据后,会先直接响应拦截函数,我们可以处理放回的data数据

具体实现如下:

1) 首先新建一个拦截器Interceptors类

class Interceptors {
  constructor() {
    this.handlers = []
  }
  use (onResolved, onRejected) {
    this.handlers.push({
      onResolved,
      onRejected
    })
  }
}

2) 将axios新增interceptors对象属性,并为它新增request、response属性,属性值均是Interceptors的实例。新增sendAjax方法,将request内容剪切过去

class Axios {
  constructor() {
    // 新增
    this.interceptors = {
      request: new Interceptors(),
      response: new Interceptors()
    }
  }
  request (config) {
  
  }
  sendAjax (config) {
    return new Promise(resolve => {
      const {url = '', method = 'get', data={}} = config
      const xhr = new XMLHttpRequest()
      xhr.open(method, url, true)
      xhr.onload = function () {
        console.log('a:', xhr.responseText)
        resolve(xhr.responseText);
      }
      xhr.send(data)
    })
  }
  ....
}

3) 修改request方法,组装chain执行列表,实现如下:

  • 新建一个chain,值为sendAjax函数,undefined
  • 将请求拦截的onResolved, onRejected添加至chain最前边
  • 将响应拦截的onResolved, onRejected添加至chain的最后 新建promise,遍历chain列表,组装promise执行串,保证执行他们的执行顺序
request(config) {
    var chain = [this.sendAjax.bind(this), undefined]

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

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

    var promise = Promise.resolve(config)
    while (chain.length > 0) {
      promise = promise.then(chain.shift(), chain.shift())
    }
    return promise   
  }

4) 由于只有Axiox构造函数才有interceptors拦截器的属性,因此需要将Axios构造函数属性搬运至request方法上

function createInstance () {
  var axios = new Axios()
  var req = axios.request.bind(axios)
  util.extend(req, Axios.prototype, axios) // 将Axios原型上的方法搬运到request中
  // 新增
  util.extend(req, axios)                  // 将axios实例上的属性搬运至request中
  return req
}

3、实现CancelToken

在实现之前,先看一下CancelToken的使用

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

axios.get('/user/12345', {
  cancelToken: source.token // 表示CancelToken实例
}).catch(function(thrown) {
  if (axios.isCancel(thrown)) {
    console.log('Request canceled', thrown.message);
  } else {
     // 处理错误
  }
})

source.cancel('不想请求了')

具体实现如下:

实现CancelToken类

实现步骤:

  1. 实例化一个Promise实例,并作为CanelToken实例的一个属性
  2. 将promise与resolve进行分离
  3. 将wrap函数作为参数放入executor中,当wrap被执行时,resove就会被调用
class CancelToken {
  constructor (executor) {
    this.message = ''

    var resolvePromise
    this.promise = new Promise ((resolve, reject) => {
      resolvePromise = resolve
    })
    var token = this  
    executor(function wrap(message) {
      if (token.message) {
        console.log('已请求完毕,cancel不了')
        return
      }
      token.message = message
      resolvePromise(message)
    })
  }
}
  1. 实现CancelToken.source方法

实现步骤:

  1. 实现一个CancelToken实例,并赋值给token
  2. 为cancel赋值,cancel其实就是上文中wrap函数
  3. 将token、cancel作为对象的属性,并返回该对象
CancelToken.source = function () {
  var cancel
  var token = new CancelToken(function executor (c) {
    cancel = c
  })
  return {
    cancel, 
    token
  }
}

其实它的设计思路就是采用promise和resolve分离的方式来实现的,然后将resove的执行权交给用户来控制,当resolve被执行,promise后的then回调(回调中有xhr.abort)就会被执行,然后就能终止接口请求

实现思路:首先将cancelToken传入至axios中的sendAjax方法中,当cancel函数被执行后(resolve就被执行了),就执行token.promise中的then回调,then回调就是为了执行XMLHttpRequest方法中的abort方法,用与终止接口请求。因此还要修改一下Axios中的sendAjax方法

sendAjax (config) {
   ......
  // 新增
  if (config.cancelToken) {
    config.cancelToken.promise.then(function (cancelMessage) {
      if (!xhr) {
        return;
      }
      xhr.abort(); // 取消request请求
      reject(cancelMessage)
      xhr = null
    })
  }

四、完整代码

Interceptors类

// src/interceptors.js
class Interceptors {
  constructor() {
    this.handlers = []
  }
  use (onResolved, onRejected) {
    this.handlers.push({
      onResolved,
      onRejected
    })
  }
}

CancelToken类

// src/cancelToken.js
class CancelToken {
  constructor (executor) {
    this.message = ''

    var resolvePromise
    this.promise = new Promise ((resolve, reject) => {
      resolvePromise = resolve
    })
    var token = this  
    executor(function wrap(message) {
      if (token.message) {
        console.log('已请求完毕,cancel不了')
        return
      }
      token.message = message
      resolvePromise(message)
    })
  }
}
CancelToken.source = function () {
  var cancel
  var token = new CancelToken(function executor (c) {
    cancel = c
  })
  return {
    cancel, 
    token
  }
}

Axios类与axios实例

// src/myAxios.js
class Axios {
  constructor() {
    this.interceptors = {
      request: new Interceptors(),
      response: new Interceptors()
    }
  }
  request(config) {
    var chain = [this.sendAjax.bind(this), undefined]

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

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

    var promise = Promise.resolve(config)
    while (chain.length > 0) {
      promise = promise.then(chain.shift(), chain.shift())
    }
    return promise   
  }
  sendAjax (config) {
    return new Promise(resolve => {
      const {url = '', method = 'get', data={}} = config
      const xhr = new XMLHttpRequest()
      xhr.open(method, url, true)
      xhr.onload = function () {
        console.log('a:', xhr.responseText)
        resolve(xhr.responseText);
      }
      xhr.send(data)
      // 取消request请求
      if (config.cancelToken) {
        config.cancelToken.promise.then(function (cancelMessage) {
          if (!xhr) {
            return;
          }
          xhr.abort();
          reject(cancelMessage)
          xhr = null
        })
      }
    })
  }
}
// 定义get,post...方法,挂在到Axios原型上
var methodsArr = ['get','delete', 'head', 'options', 'put', 'patch', 'post']
methodsArr.forEach(met => {
  Axios.prototype[met] = function () {
    if(['get','delete', 'head', 'options'].includes(met)) {
      return this.request({
        method: met,
        url: arguments[0],
        ...arguments[1] || {}
      })
    } else {
      return this.request({
        method: met,
        url: arguments[0],
        data: arguments[1],
        ...arguments[2] || {}
      })
    }
  }
})

var util = {
  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]
        }
      }
    }
  }
}


function createInstance () {
  let axios = new Axios();
  let req = axios.request.bind(axios)
  util.extend(req, Axios.property, axios)  // 将Axios原型上的方法搬运到request中
  util.extend(req, axios)                  // 将axios实例上的属性搬运至request中
  return req
}

var axios = createInstance()
axios.CancalToken = CancelToken

测试一下

// src/index.html
<script type="text/javascript" src="./interceptors.js"></script>
<script type="text/javascript" src="./cancelToken.js"></script>
<script type="text/javascript" src="./myAxios.js"></script>
<script>
    document.querySelector('.btn').onclick = function() {
        // axios(config)测试
        //axios({
        //  method: 'get',
        //  url: '/getTest'
        //}).then(res => {
        //  console.log('getAxios 成功响应', res);
        //})
        axios.interceptors.request.use(function (config) {
          config.method = 'get'
          console.log("被我请求拦截器拦截了,哈哈:",config);
          console.log('config', config)
          return config;
        }, function (error) {
          return Promise.reject(error)
        })

        axios.interceptors.response.use(function (response) {
          console.log('response拦截了, 哈哈')
          response = {message:"响应数据被我替换了,啊哈哈哈"}
          return response;
        }, function (error) {
          console.log('response错了么:', error)
          return Promise.reject(error)
        })

        var CancelToken = axios.CancelToken
        var source = CancelToken.source()
        axios.get('/getTest', {
          cancelToken:  source.token // CancelToken的实例
        }).then(res => {
          console.log('getAxios 成功响应', res);
        })
        
        // 测试cancelToken可以将以下注释放开
        //setTimeout(() => {
        //  source.cancel('不想请求了')
        //}, 2000)

    }
</script>

五、ajax、fetch和axios的区别

ajax

  • 存在嵌套地狱问题,不利于代码维护
  • 针对mvc模式编程,不符合前端mvvm的浪潮
  • 不符合关注分离的原则

fetch

  • fetch(号称是ajax的替代品)内部不是使用XMLHttpRequest对象,而是原生js,fetch的代码结构比ajax简单很多
  • 更加底层,提供丰富的API(request、response)
  • fetch只对网络请求报错,调用reject,对400,500等都当作成功的请求,因此需要封装去处理
  • 默认不会带cookie,需要添加配置项, 如:fetch(url, {credentials: 'include'})
  • 不支持abort,不支持超时控制,造成资源浪费
  • 不能监测请求进度,而xhr可以
  • fetch不兼容IE,其他的一些低版本浏览器也不兼容

axios

axios 是一个基于Promise 用于浏览器和 nodejs 的 HTTP 客户端,它有如下特性:

  • 从浏览器中创建XMLHTTPRequests
  • 从node.js中创建http请求
  • 支持Promise API
  • 拦截请求和响应
  • 转换请求和响应数据
  • 取消请求
  • 自动转换JSON数据
  • 客户端支持防御XRSF axios既提供了并发的封装,也没有fetch的各种问题,而且体积也较小,当之无愧现在最应该选用的请求的方式。

六、其他

CSRF攻击

CSRF(Cross-site request forgery)跨站请求伪造,也被称为“One Click Attack”或者Session Riding,通常缩写为CSRF或者XSRF,是一种对网站的恶意利用。

v2-fc4613eb5590443569e07e0db28c6a79_r.jpeg

解决办法:

1) 检查HTTP头上的检查 Referer 字段,一般请求的地址与Referer字段是位于同一个域名下的。当然这个方法也有局限性,因为攻击者也有可能直接去攻击浏览器,篡改其Referer字段

2) 同步表单CSRF校验 CSRF攻击之所以能成功,是因为服务器无法区分正常请求和攻击请求,因此针对这个问题,我们可以将CSRF token保存至表单的隐藏域中,当表单提交时,就可以一并提交了

3) 双重Cookie防御 就是将token保存只Cookie中,在提交(post、put, path, delete)等请求时,通过请求头或者请求体带上Cookie中已设置的token,服务器接收到请求后进行对比校验。

Axios就是通过双重Cookie,源码实现部分如下:

// lib/defaults.js
var defaults = {
  adapter: getDefaultAdapter(),

  .....
  xsrfCookieName: 'XSRF-TOKEN',
  xsrfHeaderName: 'X-XSRF-TOKEN',
};


// lib/adapters/xhr.js
module.exports = function xhrAdapter(config) {
 ....
    // 添加xsrf头部
    if (utils.isStandardBrowserEnv()) {
      var xsrfValue = (config.withCredentials || isURLSameOrigin(fullPath)) && config.xsrfCookieName ?
        cookies.read(config.xsrfCookieName) :
        undefined;

      if (xsrfValue) {
        requestHeaders[config.xsrfHeaderName] = xsrfValue;
      }
    }
...
};

XSS攻击

Cross-Site Scripting(跨站脚本攻击)简称XSS,是一种代码注入攻击。攻击者在目标网站注入恶意脚本,使之在用户的浏览器上运行。利用这些恶意脚本,攻击者可获取用户的敏感信息如cookie,SessionId等,进而危害数据安全。

XSS 的本质是:恶意代码未经过滤,与网站正常的代码混在一起;浏览器无法分辨哪些脚本是可信的,导致恶意脚本被执行。 而由于直接在用户的终端执行,恶意代码能够直接获取用户的信息,或者利用这些信息冒充用户向网站发起攻击者定义的请求。

解决办法:

1) 过滤用户输入的,检查用户输入的内容中是否有非法内容,如:<>(尖括号)、”(引号)、 ‘(单引号)、%(百分比符号)、;(分号)、()(括号)、&(& 符号)、+(加号)等。、严格控制输出。

2) 表单提交,或url参数传递前,需要对参数进行过滤

function safeStr(str){
return str.replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g, "&quot;").replace(/'/g, "&#039;");
}

七、参考