一文搞定 ajax 网络编程

117 阅读7分钟

0. 前言

这篇文章系统的整理了有关 ajax 请求的相关内容,包括在整个前端体系中对于 ajax 的原生支持,对 ajax 的 promise 封装库axios,以及新的 fetch API。其中,fetch api 是XMLHttpRequest 的升级版, 浏览器原生提供这个对象,其返回值也是 promise 对象。

1. ajax实现原理

相信有过前端网络编程经验的同学,对ajax(asynchronous javascript and XML, 异步 JavaScript 和 XML)都不会太陌生。这项技术设计可以实现发送服务器请求额外数据而不刷新页面,从而是用户有更好的体验。

ajax 的关键是浏览器原生支持的 XMLHttpRequest(XHR)对象。在介绍这个对象之前,我们先来介绍一段历史:微软的IE5是第一个引入XHR的浏览器,然后被其他浏览器所借鉴。在XHR出现之前,ajax风格的通信必须通过一些黑科技来实现,主要采用的是隐藏的窗格或内嵌窗格也就是说iframe

现在回归正题 ——

我们先从它的构造函数说起,按照常规的调用构造函数的方式进行调用:

const xhr = new XMLHttpRequest();

上述代码采用 new 关键字调用了 XHR 的构造函数,生成了一个 XMLHttpRequest 的实例化对象。发送 HTTP 请求,并且接收响应等相关操作信息都从这个对象中获取。

接下来,我们重点关注这个对象如何发送请求,以及对于响应进度以及结果的获取。

  • 发送请求:

    使用 XHR 对象发送 HTTP 请求,首先要调用 open() 方法,定义请求类型,请求URL,以及请求是否异步的布尔值;然后调用 send() 方法,该方法接收一个参数,作为请求体发送的数据,如果不需要发送请求体,则必须传入 null。具体的代码示例如下:

    xhr.open('get', 'htttps://www.example.com/', false);
    xhr.send(null);
    

    此外,还可以通过 setRequestHeader(header,value) 方法来添加请求头,其中,参数 header表示属性的名称,value 表示请求头的值。在收到相应之前,如果想要取消异步请求,可以使用 abort() 方法。

  • 服务器返回响应:

    服务器返回的响应可以通过对于已被创建的表示某个 HTTP 请求的特定实例化对象的 readyState 属性来标明,该属性有五个值(0-4),分别表示以下含义:

    • 0:未初始化(Uninitialized)。尚未调用 open() 方法
    • 1:已打开(Open)。已调用 open() 方法,但是没有调用 send() 方法
    • 2:已发送(Sent)。已调用 send() 方法,但是没有接收到响应
    • 3:接受中(Receiving)。已接收到部分相应。
    • 4:完成(Complete)。已接收到所有响应,可以使用了。

    readyState 属性的值为 4 时,表示已经接收到所有的响应。这时,我们需要对接收到的响应进行处理。通过 status 来判断 HTTP 响应的状态,该属性和 HTTP 协议中的状态码是一致的。除了这个属性,XHR 对象还提供了一个 statusText 属性,来表示对 HTTP 响应状态的描述。对于 HTTP 的响应,我们除了表示响应是否成功的响应状态之外,最关心的就是响应返回的数据。我们可以通过 responseText 或者 responseXML 属性来获取响应的数据,前者为响应体的文本返回;如果响应的内容类型是 text/xml 或者 application/xml ,则后者为响应体数据的 XML DOM 文档数据。对于如何获取 readyState 的变化这个问题,我们可以注册 onreadyStateChange 这个事件来监听。因此,对于服务器返回相应的处理,最后的成品代码如下:

    xhr.onreadyStateChange = function() {
        if(xhr.readyState === 4) {
            if((xhr.status >= 200 && xhr < 300) || xhr.status === 304) {
                // 响应成功返回的处理函数
                alert(xhr.responseText);
            } else {
                alert(`Request was unsuccessful: ${xhr.status}`);
            }
        }
    }
    

    此外,还可以设置请求超时 timeout 属性设值超时时间,ontimeout 时间注册超时的处理函数。还可以通过 onprocess 事件处理函数处理接收的进度。

    写在最后的最后,XHR对象的 POST 方法,往往与 FormData 对象一起使用,该对象用来创建表单类型格式的数据,然后通过 XHR 对象发送。

2. axios封装库

axios 时一个基于 promise 的 HTTP库,其本质是利用 promise 对 XMLHttpRequest 对象的一个封装,可以用于浏览器和 NodeJS 客户端,其特点如下:

  • 在浏览器中创建 XHR 请求
  • 在 NodeJS 中创建 HTTP 请求
  • 支持 Promise API
  • 可以拦截请求/响应
  • 转换请求数据和响应数据
  • 可以取消请求
  • 自动转换 JSON 数据
  • 客户端支持防止 CSRF/XSRF

使用 axios 发送 HTTP 请求,分为两种情况:

  1. 在前端工程中使用:

    在这种情况下,首先需要通过 npm install axios --save 命令在当前工程中安装 axios 依赖库。然后,需要在定义请求或者需要发送 ajax 请求的文件中通过 import axios from 'axois' 引入模块。然后,在文件中编写发送请求的代码。

  2. 在 html 页面中使用:

    在这种情况下,直接通过 script 标签引用 CDN 的线上库就可以了。代码如下:

    <script src="https://unpkg.com/axios/dist/axios.min.js"></script>
    

使用 axios 发送 HTTP 请求,有两种方式:

  • 一种是通过 axios() 方法, 例如:
axios({
    method: 'post',
    url: 'someURL',
    data: {
        a: 'a',
        b: 'b'
    }
});
  • 一种是通过 axios 对象对应的方法来发起请求, 例如:
    • axios.get(url)
    • axios.post(url, data)
    • axios.delete(url)
    • axios.update(url) ...

axios 发送请求之后,会返回一个 promise 对象。比如说,上文提到的一个 post 请求

axios({
    method: 'post',
    url: 'someURL',
    data: {
        a: 'a',
        b: 'b'
    }
}).then(res => {
    console.log(res);
}).catch(err => {
    console.log(err);
});

当请求正常返回,则会进入到 .then() 中进行处理,如果请求发生错误,则会在 .catch() 方法中进行处理。

关于 axios,我想重点介绍的是它的拦截器(因为在面试中被问到好几次)。

axios 的拦截器分为两种,请求拦截器响应拦截器,axios 的执行顺序是:请求拦截器 -> api 请求 -> 响应拦截器。使用方式如下:

// 请求拦截器
axios.interceptor.request.use(function(config) {
    // 在请求之前做了什么
    return config;
});


// 响应拦截器
axios.interceptor.response.use(function(response) {
    // 对响应数据做什么
    return response;
}, function(error) {
    // 对响应的错误做什么
    return Promise.reject(error);
});

除此之外,拦截器还可以取消。想要取消拦截器,只需在定义拦截器的时候获取到拦截器的ID,然后在拦截器提供的 eject 方法中传入 ID 即可。代码如下:

const id = axios.interceptor.request.use(function() {/*...*/});
axios.interceptor.request.eject(id);

虽然拦截器看着很高深的样子,但是其本质就是一个执行顺序的关系,具体的实现也是通过一个数组来做的。

const chain = [请求拦截器函数, api函数, 相应拦截器函数];

执行的时候,依次执行数组里面的函数即可。拦截器的实现源码如下:

// 拦截器构造函数
function InterceptorManager() {
  // 保存拦截器的数组,axios.interceptors.use的拦截函数会被push到handlers,可以添加多个拦截器
  this.handlers = [];
}

// 向拦截器原型上挂载 use方法, 向handler数组中push一个对象, 返回一个id
// 这样就可以通过eject(id) 取消拦截函数了。
InterceptorManager.prototype.use = function use(fulfilled, rejected, options) {
  this.handlers.push({
    fulfilled: fulfilled,
    rejected: rejected,
    synchronous: options ? options.synchronous : false,
    runWhen: options ? options.runWhen : null
  });
  return this.handlers.length - 1;
};

// 移除拦截器
InterceptorManager.prototype.eject = function eject(id) {
   // 通过id可以查找对应的拦截器,进行移除
  if (this.handlers[id]) {
    this.handlers[id] = null;
  }
};

// 遍历执行所有拦截器
InterceptorManager.prototype.forEach = function forEach(fn) {
  utils.forEach(this.handlers, function forEachHandler(h) {
    if (h !== null) {
      fn(h);
    }
  });
};

请求拦截器和相应拦截器都是 InterceptorManager 的示例。

function Axios(instanceConfig) {
  this.defaults = instanceConfig;
  // 请求拦截器和响应拦截器使用的都是 InterceptorManager构造函数
  this.interceptors = {
    request: new InterceptorManager(),
    response: new InterceptorManager()
  };
}

最后就是他们的执行顺序问题了,也就是拦截器实现的本质:

...
// dispatchRequest是api请求
var chain = [dispatchRequest, undefined];
// 把请求拦截器数组requestInterceptorChain 放在 chain 数组的前面
Array.prototype.unshift.apply(chain, requestInterceptorChain);

// 把响应拦截器responseInterceptorChain 放在chain数组的后面
chain = chain.concat(responseInterceptorChain);

promise = Promise.resolve(config);
// 遍历执行chain函数
while (chain.length) {
  promise = promise.then(chain.shift(), chain.shift());
}
...

3. fetch API

Fetch 标准定义请求、响应,以及绑定二者的流程:获取(fetch)。Fetch API 可以执行 XMLHttpRequest 对象的所有任务,和 XHR 对象一样,它也是浏览器支持的,暴漏在全局作用域中的。不同的是 fetch() 并不是一个对象,而是一个获取资源的方法。调用这个方法,浏览器就会向给定的 URL 发送请求。

fetch() 方法会返回一个 promise 对象,当 promise 被决策之后,我们可以获取服务器响应的信息。Fetch API 支持通过响应的状态码和状态文本属性检查响应状态。换句话说,状态码的不同并不会引起 promise 的拒绝。那么问题来了,什么时候 promise 会被拒绝呢?

服务器没有响应而导致浏览器超时,违反CORS,无网络连接,HTTPS错配以及其他浏览器/网络策略问题都会导致 promise 被拒绝。

fetch 的请求方式有以下几种:

  • POST 请求:
let data = JSON.stringify({
    foo: bar
});
let jsonHeaders = new Header({
    'content-Type': 'application/json'
});

// fetch() 的第二个参数是可选的配置对象
fetch('/someUrl', {
    method: 'POST',
    body: data,
    headers: jsonHeaders
});
  • GET 请求:
// fetch() 默认向该网址发出 GET 请求,返回一个 Promise 对象
fetch('/someUrl');

整体上来讲,fetch() 的功能和 XMLHttpRequest 基本相同,但是有三个主要的差异:

  • fetch 使用 promise,不使用回调函数
  • fetch 采用模块化设计,API 分散在多个对象上(Request 对象,Response 对象,Header 对象),更合理一些
  • fetch 通过数据流(Stream 对象)处理数据,可以分块读取,有利于提高网站性能表现,减少内存占用,对于请求大文件或者网速慢的场景相当有用。