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 对象发送。 - 0:未初始化(Uninitialized)。尚未调用
2. axios封装库
axios 时一个基于 promise
的 HTTP库,其本质是利用 promise
对 XMLHttpRequest 对象的一个封装,可以用于浏览器和 NodeJS 客户端,其特点如下:
- 在浏览器中创建 XHR 请求
- 在 NodeJS 中创建 HTTP 请求
- 支持 Promise API
- 可以拦截请求/响应
- 转换请求数据和响应数据
- 可以取消请求
- 自动转换 JSON 数据
- 客户端支持防止 CSRF/XSRF
使用 axios 发送 HTTP 请求,分为两种情况:
-
在前端工程中使用:
在这种情况下,首先需要通过
npm install axios --save
命令在当前工程中安装 axios 依赖库。然后,需要在定义请求或者需要发送 ajax 请求的文件中通过import axios from 'axois'
引入模块。然后,在文件中编写发送请求的代码。 -
在 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 对象)处理数据,可以分块读取,有利于提高网站性能表现,减少内存占用,对于请求大文件或者网速慢的场景相当有用。