面试官:请说一下axios的原理?

441 阅读7分钟

我正在参加「掘金·启航计划」

前言

我们先用一句话概括一下axios是什么?

Axios 是一个基于 promise 的 HTTP 库

思考一下,为什么这么多人用axios,用其他框架不行么?

这就引出了我们axios的特点

  1. 基于 promise 的异步 ajax 请求库,支持promise所有的API
  2. 浏览器端/node 端都可以使用,浏览器中创建XMLHttpRequests,node中使用http
  3. 支持请求/响应拦截器
  4. 支持请求取消
  5. 可以转换请求数据和响应数据,并对响应回来的内容自动转换成 JSON类型的数据
  6. 批量发送多个请求
  7. 安全性更高,客户端支持防御 XSRF,就是让你的每个请求都带一个从cookie中拿到的key, 根据浏览器同源策略,假冒的网站是拿不到你cookie中得key的,这样,后台就可以轻松辨别出这个请求是否是用户在假冒网站上的误导输入,从而采取正确的策略。
  8. 上传和下载进度监控

联想记忆方法:电梯有很多个,他们都在并行处理。电梯支持浏览器端和node端,我按了一下19层,开始等待。他在15层被拦截了,上升速度很慢。我看了一下双层快到了,于是我取消单层。直奔双层而去,结果单层的发生了故障,果然还是双层的更安全呀。

思考:🤔axios这个库上有哪些优点是我们写项目的时候可以借鉴的,我们不用源码抄一遍,而是要思考,我们可以借鉴什么优点,怎么把他的优点改成适合我们项目使用的

优点一:axios源码目录(抽象性)

通过追踪 axios 完整的请求流程源码,可以看出项目抽离出了很多的数据模型和转换模块,比如:Axios 构造函数、defaults 默认配置参数、拦截器、适配器,以及数据转换函数等等,具备很高的抽象性。

image.png

也就是我们写代码的时候要进行高度抽象,能抽离的都进行抽离

优点二:设计模式-----适配器模式

设计模式的有点就是兼容性,对外统一接口

axios在浏览器端使用XMLHttpRequest对象发送ajax请求;在node环境使用http对象发送ajax请求。

var defaults.adapter = getDefaultAdapter();
function getDefaultAdapter () {
	var adapter;
    if (typeof XMLHttpRequest !== 'undefined') {
    	// 浏览器环境
        adapter = require('./adapter/xhr');
    } else if (typeof process !== 'undefined') {
    	// node环境
        adapter = require('./adapter/http');
    }
   return adapter;
}

优点三、设计模式-----工厂模式

工厂模式将对象的创建实现分离(尤其是写库的时候,经常需要把创建和实现进行分离)。

使代码具备良好的封装性,可扩展性(符合开放封闭原则),高解耦性(符合最少知识原则(说白了,就是让你能够傻瓜式应用))。

为什么工厂模式返回的不是axios实例,而是axios.request?

因为我们要进行封装,把实例给用户,体现我们工厂模式的优点。假如我们返回axios实例,就造成了无法传config了,这就很糟糕,所以我们返回了方法,就能传参数了。就能同时满足axios({})和axios.request({})

import Axios from './core/Axios';  // 引入Axios,是放axios的核心代码的
import { extend } from './helper/utils'; // 混入方法

// 工厂
function createInstance() {
  const axios = new Axios();
  const req = axios.request.bind(axios);
  // req继承axios的属性和方法
  // Object.getOwnPropertyNames(Axios.prototype)读取对象属性,可以读取不可不可遍历的属性
  extend(req, Object.getOwnPropertyNames(Axios.prototype), axios);
  extend(req, Object.getOwnPropertyNames(axios), axios);
  return req;
}

const axios = createInstance(); // 创建实例的工厂
export default axios;
// extend方法
export function extend(a, b, context) {
  for (let key of b) {
    if (key === 'constructor') {
      continue;
    }
    
    if (typeof context[key] === 'function') {
      a[key] = context[key].bind(context);
    } else {
      a[key] = context[key];
    }
  }
}

优点四:支持请求/响应拦截器

image.png

整个过程是一个链式调用的方式,并且每个拦截器都可以支持同步和异步处理,我们自然而然地就联想到使用 Promise 链的方式来实现整个调用过程。

首先需要创建一个 InterceptorManage类, 其实就是一个数组,use方法其实就是往数组中添加promise函数

class InterceptorManage{
  constructor() {
    this.handles = [];
  }

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

import InterceptorManage from "./InterceptorManage";


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

  request(config) {
    // 拦截器队列
    let chain = [this.sendAjax.bind(this), undefined];

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

    // 响应拦截
    this.interceptors.response.handles.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;
  }

  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);
    })
  }
}

export default Axios;

chains大概是['fulfilled1','reject1','fulfilled2','reject2','this.sendAjax','undefined','fulfilled2','reject2','fulfilled1','reject1']这种形式

优点五:支持请求取消

其实我们的好多需求能实现,主要是xhr提供的api多,我们先来看一下xhr都有哪些常用的api呢?

  • 主要属性,readystate, response, responseText, responseType, status, timeout
  • 主要事件,error,abort,load, progress, open, send,onreadystatechange,setRequestHeader;

原理

通过传递 config 配置 cancelToken 的形式,来取消的。判断有传cancelToken,在 promise 链式调用的 dispatchRequest 抛出错误,在 adapterrequest.abort() 取消请求,使 promise 走向 rejected,被用户捕获取消信息。

具体使用方法可以查看 浅谈axios中取消请求及阻止重复请求的方法_vue.js_脚本之家 (jb51.net)

优点六:批量发送多个请求

这个功能有点鸡肋,完全可以使用promise.all代替,就不多说了

优点七:XSRF防御

原理

XSRF 的防御手段有很多,比如验证请求的 referer,但是 referer 也是可以伪造的,所以杜绝此类攻击的一种方式是服务器端要求每次请求都包含一个 token,这个 token 不在前端生成,而是在我们每次访问站点的时候生成,并通过 set-cookie 的方式种到客户端,然后客户端发送请求的时候,从 cookie 中对应的字段读取出 token,然后添加到请求 headers 中。这样服务端就可以从请求 headers 中读取这个 token 并验证,由于这个 token 是很难伪造的,所以就能区分这个请求是否是用户正常发起的。

对于我们的 axios 库,我们要自动把这几件事做了,每次发送请求的时候,从 cookie 中读取对应的 token 值,然后添加到请求 headers中。我们允许用户配置 xsrfCookieName 和 xsrfHeaderName,其中 xsrfCookieName 表示存储 token 的 cookie 名称,xsrfHeaderName 表示请求 headers 中 token 对应的 header 名称。

axios.get('/more/get',{
  xsrfCookieName: 'XSRF-TOKEN', // default
  xsrfHeaderName: 'X-XSRF-TOKEN' // default
}).then(res => {
  console.log(res)
})

我们提供 xsrfCookieName 和 xsrfHeaderName 的默认值,当然用户也可以根据自己的需求在请求中去配置 xsrfCookieName 和 xsrfHeaderName

优点八:上传和下载进度监控

原理:

我们希望给 axios 的请求配置提供 onDownloadProgress 和 onUploadProgress 2 个函数属性,用户可以通过这俩函数实现对下载进度和上传进度的监控。

axios.get('/more/get',{
  onDownloadProgress(progressEvent) {
    // 监听下载进度
  }
})

axios.post('/more/post',{
  onUploadProgress(progressEvent) {
    // 监听上传进度
  }
})

xhr 对象提供了一个 progress 事件,我们可以监听此事件对数据的下载进度做监控;另外,xhr.uplaod 对象也提供了 progress 事件,我们可以基于此对上传进度做监控。

实现逻辑

const {
  /*...*/
  onDownloadProgress,
  onUploadProgress
} = config

if (onDownloadProgress) {
  request.onprogress = onDownloadProgress
}

if (onUploadProgress) {
  request.upload.onprogress = onUploadProgress
}

优点九:配置默认参数

axios有自己的默认设置,也支持用户进行配置,配置如下

axios.defaults.headers.common['test'] = 123
axios.defaults.headers.post['Content-Type'] = 'application/x-www-form-urlencoded'
axios.defaults.timeout = 2000

那是如何做到的呢,一看axios.defaults就可以猜到,axios有一个defaults属性,他的值,就是axios的默认属性,我们可以设置默认属性

axios.defaults.headers.common['test'] = 123

也就相当于修改默认值

我们也可以自己传入config,与默认属性进行合并

参考