异步请求-XMLHttpRequest、ajax、axios原理浅析

XMLHTTPRequest

参考

XML与Ajax

readyState

状态描述
0UNSENT (未打开)表示已创建 XHR 对象,open() 方法还未被调用
1OPENED (未发送)open() 方法已被成功调用,send() 方法还未被调用
2HEADERS_RECEIVED (已获取响应头)send() 方法已经被调用,响应头和响应状态已经返回
3LOADING (正在下载响应体)响应体下载中,responseText中已经获取了部分数据
4DONE (请求完成)整个请求过程已经完毕

responseType

enum XMLHttpRequestResponseType {
  "",
  "arraybuffer",
  "blob",
  "document",
  "json",
  "text"
};

XMLHttpRequest Level 1

var xhr = new XMLHttpRequest();
xhr.open('GET', 'example.php');
xhr.send();
xhr.onreadystatechange = function(){
  if ( xhr.readyState == 4 && xhr.status == 200 ) {
     alert( xhr.responseText );
  } else {
     alert( xhr.statusText );
  }
};

只支持文本数据的传送,无法用来读取和上传二进制文件。
传送和接收数据时,没有进度信息,只能提示有没有完成。
受到"同域限制"(Same Origin Policy),只能向同一域名的服务器请求数据。

XMLHttpRequest Level 2

XMLHttpRequest Level 2 针对 XMLHttpRequest Level 1 的缺点,做了大幅改进。具体如下:

  • 可以设置HTTP请求的超时时间。
  • 可以使用FormData对象管理表单数据。
  • 可以上传文件。
  • 可以请求不同域名下的数据(跨域请求)。
  • 可以获取服务器端的二进制数据。
  • 可以获得数据传输的进度信息
<input id="input" type="file">
var input = document.getElementById("input"),
    formData = new FormData();
formData.append("file",input.files[0]); // file名称与后台接收的名称一致

var xhr = new XMLHttpRequest();
xhr.open('POST', url);

xhr.timeout = 5000; // 1

xhr.addEventListener('load', function() { ... }); // 2
xhr.addEventListener('error', function() { ... }); // 3

var onProgressHandler = function(event) {
  if(event.lengthComputable) {
    var progress = (event.loaded / event.total) * 100; // 4
    ...
  }
}

xhr.upload.addEventListener('progress', onProgressHandler); // 5
xhr.addEventListener('progress', onProgressHandler); // 6
// (1) 设置请求超时时间为 5,000 ms (默认无超时时间)

// (2) 注册成功回调

// (3) 注册异常回调

// (4) 计算已完成的进度

// (5) 注册上传进度事件回调

// (6) 注册下载进度事件回调

xhr.send(formData);

轮询

定时轮询

从服务器检索更新的最简单的策略之一是让客户端进行定期检查:客户端可以以周期性间隔(轮询服务器)启动后台XHR请求,以检查更新。如果新数据在服务器上可用,则在响应中返回,否则响应为空。

长轮询

通过保持长连接,直到更新可用,数据可以立即发送到客户端,一旦它在服务器上可用。因此,长时间轮询为消息延迟提供了最佳的情况,并且还消除了空检查,这减少了 XHR 请求的数量和轮询的总体开销。一旦更新被传递,长的轮询请求完成,并且客户端可以发出另一个长轮询请求并等待下一个可用的消息:

function checkUpdates(url) {
  var xhr = new XMLHttpRequest();
  xhr.open('GET', url);
  xhr.onload = function() { // 1
    ...
    checkUpdates('/updates'); // 2
  };
  xhr.send();
}

checkUpdates('/updates'); // 3

// (1) 处理接收到的数据并启动下一轮检测更新

// (2) 启动下一轮检测更新

// (3) 发起首次更新请求

Ajax

基于xmlHttpRequest

参考

ajax与xmlHttpRequest

模仿ajax封装xmlHttpRequest

const http = {
  /**
   * js封装ajax请求
   * >>使用new XMLHttpRequest 创建请求对象,所以不考虑低端IE浏览器(IE6及以下不支持XMLHttpRequest)
   * >>使用es6语法,如果需要在正式环境使用,则可以用babel转换为es5语法 https://babeljs.cn/docs/setup/#installation
   *  @param settings 请求参数模仿jQuery ajax
   *  调用该方法,data参数需要和请求头Content-Type对应
   *  Content-Type                        data                                     描述
   *  application/x-www-form-urlencoded   'name=哈哈&age=12'或{name:'哈哈',age:12}  查询字符串,用&分割
   *  application/json                     name=哈哈&age=12'                        json字符串
   *  multipart/form-data                  new FormData()                           FormData对象,当为FormData类型,不要手动设置Content-Type
   *  注意:请求参数如果包含日期类型.是否能请求成功需要后台接口配合
   */
  ajax: (settings = {}) => {
    // 初始化请求参数
    let _s = Object.assign({
      url: '', // string
      type: 'GET', // string 'GET' 'POST' 'DELETE'
      dataType: 'json', // string 期望的返回数据类型:'json' 'text' 'document' ...
      async: true, //  boolean true:异步请求 false:同步请求 required
      data: null, // any 请求参数,data需要和请求头Content-Type对应
      headers: {}, // object 请求头
      timeout: 1000, // string 超时时间:0表示不设置超时
      beforeSend: (xhr) => {
      },
      success: (result, status, xhr) => {
      },
      error: (xhr, status, error) => {
      },
      complete: (xhr, status) => {
      }
    }, settings);
    // 参数验证
    if (!_s.url || !_s.type || !_s.dataType || _s.async === undefined) {
      alert('参数有误');
      return;
    }
    // 创建XMLHttpRequest请求对象
    let xhr = new XMLHttpRequest();
    // 请求开始回调函数
    xhr.addEventListener('loadstart', e => {
      _s.beforeSend(xhr);
    });
    // 请求成功回调函数
    xhr.addEventListener('load', e => {
      const status = xhr.status;
      if ((status >= 200 && status < 300) || status === 304) {
        let result;
        if (xhr.responseType === 'text') {
          result = xhr.responseText;
        } else if (xhr.responseType === 'document') {
          result = xhr.responseXML;
        } else {
          result = xhr.response;
        }
        // 注意:状态码200表示请求发送/接受成功,不表示业务处理成功
        _s.success(result, status, xhr);
      } else {
        _s.error(xhr, status, e);
      }
    });
    // 请求结束
    xhr.addEventListener('loadend', e => {
      _s.complete(xhr, xhr.status);
    });
    // 请求出错
    xhr.addEventListener('error', e => {
      _s.error(xhr, xhr.status, e);
    });
    // 请求超时
    xhr.addEventListener('timeout', e => {
      _s.error(xhr, 408, e);
    });
    let useUrlParam = false;
    let sType = _s.type.toUpperCase();
    // 如果是"简单"请求,则把data参数组装在url上
    if (sType === 'GET' || sType === 'DELETE') {
      useUrlParam = true;
      _s.url += http.getUrlParam(_s.url, _s.data);
    }
    // 初始化请求
    xhr.open(_s.type, _s.url, _s.async);
    // 设置期望的返回数据类型
    xhr.responseType = _s.dataType;
    // 设置请求头
    for (const key of Object.keys(_s.headers)) {
      xhr.setRequestHeader(key, _s.headers[key]);
    }
    // 设置超时时间
    if (_s.async && _s.timeout) {
      xhr.timeout = _s.timeout;
    }
    // 发送请求.如果是简单请求,请求参数应为null.否则,请求参数类型需要和请求头Content-Type对应
    xhr.send(useUrlParam ? null : http.getQueryData(_s.data));
  },
  // 把参数data转为url查询参数
  getUrlParam: (url, data) => {
    if (!data) {
      return '';
    }
    let paramsStr = data instanceof Object ? http.getQueryString(data) : data;
    return (url.indexOf('?') !== -1) ? paramsStr : '?' + paramsStr;
  },
  // 获取ajax请求参数
  getQueryData: (data) => {
    if (!data) {
      return null;
    }
    if (typeof data === 'string') {
      return data;
    }
    if (data instanceof FormData) {
      return data;
    }
    return http.getQueryString(data);
  },
  // 把对象转为查询字符串
  getQueryString: (data) => {
    let paramsArr = [];
    if (data instanceof Object) {
      Object.keys(data).forEach(key => {
        let val = data[key];
        // todo 参数Date类型需要根据后台api酌情处理
        if (val instanceof Date) {
          // val = dateFormat(val, 'yyyy-MM-dd hh:mm:ss');
        }
        paramsArr.push(encodeURIComponent(key) + '=' + encodeURIComponent(val));
      });
    }
    return paramsArr.join('&');
  }
    
   /**
   * 根据实际业务情况装饰 ajax 方法
   * 如:统一异常处理,添加http请求头,请求展示loading等
   * @param settings
   */
  request: (settings = {}) => {
    // 统一异常处理函数
    let errorHandle = (xhr, status) => {
      console.log('request error...');
      if (status === 401) {
        console.log('request 没有权限...');
      }
      if (status === 408) {
        console.log('request timeout');
      }
    };
    // 使用before拦截参数的 beforeSend 回调函数
    settings.beforeSend = (settings.beforeSend || function () {
    }).before(xhr => {
      console.log('request show loading...');
    });
    // 保存参数success回调函数
    let successFn = settings.success;
    // 覆盖参数success回调函数
    settings.success = (result, status, xhr) => {
      // todo 根据后台api判断是否请求成功
      if (result && result instanceof Object && result.code !== 1) {
        errorHandle(xhr, status);
      } else {
        console.log('request success');
        successFn && successFn(result, status, xhr);
      }
    };
    // 拦截参数的 error
    settings.error = (settings.error || function () {
    }).before((result, status, xhr) => {
      errorHandle(xhr, status);
    });
    // 拦截参数的 complete
    settings.complete = (settings.complete || function () {
    }).after((xhr, status) => {
      console.log('request hide loading...');
    });
    // 请求添加权限头,然后调用http.ajax方法
    (http.ajax.before(http.addAuthorizationHeader))(settings);
  },
  // 添加权限请求头
  addAuthorizationHeader: (settings) => {
    settings.headers = settings.headers || {};
    const headerKey = 'Authorization'; // todo 权限头名称
    // 判断是否已经存在权限header
    let hasAuthorization = Object.keys(settings.headers).some(key => {
      return key === headerKey;
    });
    if (!hasAuthorization) {
      settings.headers[headerKey] = 'test'; // todo 从缓存中获取headerKey的值
    }
  }
   // 给http对象添加了get,post等方法,这些方法主要设置了默认参数然后调http.request
    get: (url, data, successCallback, dataType = 'json') => {
    http.request({
      url: url,
      type: 'GET',
      dataType: dataType,
      data: data,
      success: successCallback
    });
  },
  delete: (url, data, successCallback, dataType = 'json') => {
    http.request({
      url: url,
      type: 'DELETE',
      dataType: dataType,
      data: data,
      success: successCallback
    });
  },
  // 调用此方法,参数data应为查询字符串或普通对象
  post: (url, data, successCallback, dataType = 'json') => {
    http.request({
      url: url,
      type: 'POST',
      dataType: dataType,
      data: data,
      headers: {
        'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8'
      },
      success: successCallback
    });
  },
  // 调用此方法,参数data应为json字符串
  postBody: (url, data, successCallback, dataType = 'json') => {
    http.request({
      url: url,
      type: 'POST',
      dataType: dataType,
      data: data,
      headers: {
        'Content-Type': 'application/json; charset=UTF-8'
      },
      success: successCallback
    });
  }
}

Function.prototype.before = function (beforeFn) { // eslint-disable-line
  let _self = this;
  return function () {
    beforeFn.apply(this, arguments);
    _self.apply(this, arguments);
  };
};

Function.prototype.after = function (afterFn) { // eslint-disable-line
  let _self = this;
  return function () {
    _self.apply(this, arguments);
    afterFn.apply(this, arguments);
  };
};

axios

参考

手写axios

axios 是一个基于 Promise 的 http请求库,可以用在浏览器和 node.js 中,本质上也是对原生XHR的封装,只不过它是Promise 的实现版本,符合最新的ES规则。

axios有什么特性?

  1. .可以用在浏览器和node.js的环境中,从浏览器中创建XMLHttpRequests;基于node内置核心模块http实现axios,从中创建http请求,也就是说,axios可以在浏览器上和服务器上都可以发起请求
  2. 是一个基于Promise的HTTP库,支持promise的所有API,源码中返回的是一个promise对象
  3. 拦截请求和响应
  4. 可以转换请求数据和响应数据,并对响应的内容自动转换为json类型的数据
  5. 安全性更高,客户端支持防御XSRF
  6. 首先axios会判断当前环境是否为标准的浏览器环境,如果是标准的浏览器环境的话,就会继续执行。
  7. 对于XSRF攻击来说,最基本的就是跨域了,所以我们对发出请求的域和当前的域做同源判断,如果是跨域的话,就必须有凭证config.withCredentials || isURLSameOrigin(fullPath),凭证简单来说就是当前请求在跨域时是否可以带上cookie。
  8. 取消请求
    axios提供了取消请求的接口,实际上在源码中还是通过最关键的一步request.abort()来取消请求,使用isCancel来作为是否已经取消的标识,使用Cancel来作为重复取消时抛出的错误,使用CancelToken来作为取消请求的核心处理,在处理中采取promise异步的方法

实现axios

  1. 基于XMLHttpRequest实现request方法(代码里的sendAjax),通过Promise,resolve(xhr.responseText)。
  2. 将request方法绑定到axios对象,通过bind(即最终返回的是axios.request对象)。
  3. 将get、post、put等方法挂载到axios.request原型上(prototype[method]),这里先将其挂载到Axios原型,再挂载到Axios.request原型上。
  4. 添加request和response拦截器,改写request方法,将request方法变为方法对象数组[{onFullFilled, onRejected}],先添加[sendAjax, undefined],然后将request回调依次插入队首,response回调依次插入队尾,最后通过then方法返回一个Promise(r => { resolve(data); })
class InterceptorsManage {
    constructor() {
        this.handlers = [];
    }

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

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.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;
    }
    sendAjax(){
        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);
        })
    }
}

// 定义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] || {}
            })
        }

    }
})


// 工具方法,实现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的方法-》即实例的request方法
function CreateAxiosFn() {
    let axios = new Axios();

    let req = axios.request.bind(axios);
    // 混入方法, 处理axios的request方法,使之拥有get,post...方法
    utils.extend(req, Axios.prototype, axios)
    return req;
}

// 得到最后的全局变量axios
let axios = CreateAxiosFn();
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Jimmy-Enjoy

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值