彻底搞懂ajax
每个会用
jquery的人都会用$.ajax发起一个ajax请求,然后再回调函数里面取得返回的数据,但是并不是所有的人都知道这个流程里面发生了什么事,本文就深入讨论下关于ajax的所有(大部分)细节。
[TOC]
¶1.Ajax其实是标准
很多人误以为XMLHttpRequest就是Ajax,其实不然,他们两个是两个不同维度的概念。
搜索ajax的wiki,是这样介绍的:
AJAX即“Asynchronous JavaScript and XML”(异步的JavaScript与XML技术),指的是一套综合了多项技术的浏览器端网页开发技术。Ajax的概念由杰西·詹姆士·贾瑞特所提出。
¶ajax技术发展史
上个世纪90年代,几乎所有的网站都由HTML页面实现,服务器处理每一个用户请求都需要重新加载网页。这样的处理方式效率不高。用户的体验是所有页面都会消失,再重新加载,即使只是一部分页面元素改变也要重新加载整个页面,不仅要刷新改变的部分,连没有变化的部分也要刷新。这会加重服务器的负担。
这可以用异步加载来解决。1995年,JAVA语言的第一版发布,随之发布的的Java applets(JAVA小程序)首次实现了异步加载。浏览器通过运行嵌入网页中的Java applets与服务器交换数据,不必刷新网页。1996年,Internet Explorer将iframe元素加入到HTML,支持局部刷新网页。
1998年前后,Outlook Web Access小组写成了允许客户端脚本发送HTTP请求(XMLHTTP)的第一个组件。该组件原属于微软Exchange Server,并且迅速地成为了Internet Explorer 4.0[2]的一部分。部分观察家认为,Outlook Web Access是第一个应用了Ajax技术的成功的商业应用程序,并成为包括Oddpost的网络邮件产品在内的许多产品的领头羊。但是,2005年初,许多事件使得Ajax被大众所接受。Google在它著名的交互应用程序中使用了异步通讯,如Google讨论组、Google地图、Google搜索建议、Gmail等。Ajax这个词由《Ajax: A New Approach to Web Applications》一文所创,该文的迅速流传提高了人们使用该项技术的意识。另外,对Mozilla/Gecko的支持使得该技术走向成熟,变得更为简单易用。
所以ajax是一种技术方案,XMLHttpRequest是ajax在web端实现所依赖的一个对象,是这个对象使得浏览器可以发出HTTP请求与接收HTTP响应,来异步的操作网页。
只是现在,市面上基本都用XMLHttpRequest 来发送ajax请求。
es6新增了fetch来代替XMLHttpRequest ,比起XMLHttpRequest具有更好的可扩展性和高效性,下文会详细介绍。
¶2.XMLHttpRequest介绍
XMLHTTP是一组API函数集,可被JavaScript、JScript、VBScript以及其它web浏览器内嵌的脚本语言调用,通过HTTP在浏览器和web服务器之间收发XML或其它数据。XMLHTTP最大的好处在于可以动态地更新网页,它无需重新从服务器读取整个网页,也不需要安装额外的插件。该技术被许多网站使用,以实现快速响应的动态网页应用。例如:Google的Gmail服务、Google Suggest动态查找界面以及Google Map地理信息服务。
一开始,巨硬公司发明了这个可以用来构造无刷新页面的对象,这个对象可以通过javascript,VBScript或者其他的浏览器内置脚本访问,后来其他的浏览器开发公司也逐步实现了这个标准对象,到了dom3标准时代,它已经成为W3C推荐的方法。截止2011年,大多数浏览器已经支持。互联网程序迎来了无刷新的页面时代,诞生了一大批优秀的web应用。
现代浏览器基本都支持ajax,但是他们的技术方案却分为两种:
标准浏览器通过 XMLHttpRequest 对象实现了ajax的功能. 只需要通过一行语句便可创建一个用于发送ajax请求的对象.
var xhr = new XMLHttpRequest();
IE浏览器通过XMLHttpRequest或者ActiveXObject对象同样实现了ajax的功能。
var xhr = new ActiveXObject(ProgID);
// progID可以是下面的值,对应不同的版本
Microsoft.XMLHTTP
Microsoft.XMLHTTP.1.0
Msxml2.ServerXMLHTTP
Msxml2.ServerXMLHTTP.3.0
Msxml2.ServerXMLHTTP.4.0
Msxml2.ServerXMLHTTP.5.0
Msxml2.ServerXMLHTTP.6.0
Msxml2.XMLHTTP
Msxml2.XMLHTTP.3.0
Msxml2.XMLHTTP.4.0
Msxml2.XMLHTTP.5.0
Msxml2.XMLHTTP.6.0
r如何取得全平台兼容的XMLHttpRequest对象。
function getXHR(){
var xhr = null;
if(window.XMLHttpRequest) {
xhr = new XMLHttpRequest();
} else if (window.ActiveXObject) {
try {
xhr = new ActiveXObject("Msxml2.XMLHTTP");
} catch (e) {
try {
xhr = new ActiveXObject("Microsoft.XMLHTTP");
} catch (e) {
alert("您的浏览器暂不支持Ajax!");
}
}
}
return xhr;
}
HTML 5的概念形成后,W3C开始考虑标准化这个接口。2008年2月,就提出了XMLHttpRequest Level 2 草案(之前是Level 1)。这个新版本提出了很多实用的功能,大大的加快了互联网革新。
这里对比level 1 和 level 2 的区别
| 特性 | level 1 | level 2 |
|---|---|---|
| 文本数据传送 | 支持 | 支持 |
| 读取上传二进制 | 不支持 | 支持 |
| 进度信息 | 不支持 | 支持 |
| 同源限制 | 不支持跨域 | 可发送跨域请求 |
| 超时时间 | 不支持自己设置 | xhr.timeout设置超时时间 |
此外,为了方便表单处理,html5新加了个FormData对象,新的标准同样支持这个对象,用来模拟表单。
关于level 1 和 level 2的区别,可也参考XMLHttpRequest Level 2 使用指南,阮老师对每个新特性都写了代码实例。
XMLHttpRequest对象:
属性:
channel 草案,不知道是啥
mozAnon
mozBackgroundRequest草案,布尔值,为True 时,本次请求不带cookies或者头部认证信息
mozResponseArrayBuffer 草案
mozSystem 草案
multipart 草案
onreadystatechange 只要``readyState` 发生改变,就调用这个函数。
readyState 当前的状态,分别是0:UNSENT,1:OPENED,2:HEADERS_RECEIVED,3:LOADING,4:DONE。在ie里,状态名称不一样。
response请求响应的正文
responseText 返回一个DOMString,它包含对文本的请求的响应。
responseType 返回数据的类型,具体见下文
responseURL 返回响应的序列化URL或空字符串
responseXML 返回一个包含请求检索的HTML或XML的Document
status 响应的数字状态码,采用标准HTTP 状态码
statusText 状态码对应的文本信息
timeout 超时时间
upload 返回
XMLHttpRequestUpload 对象,用来表示上传的进度
withCredentials 点击查看详情,和跨域请求认证有关
方法:
abort()终止请求,readyState会变为0
getAllResponseHeaders()返回所有的响应头
getResponseHeader()返回指定的响应头
open()初始化一个请求。
overrideMimeType()level 1的方法,用responseType代替
send()发送http请求
sendAsBinary()
setRequestHeader()设置http请求头
¶3.使用XMLHttpRequest
下面是用 XMLHttpRequest发送ajax get 请求的例子(用到了大部分api)。
var xhr = new XMLHttpRequest(); // 新建一个XMLHttpRequest对象实例
xhr.open('get',"url"); // 设置请求方法和地址
xhr.responseType = "text"; // 设置返回的数据类型
xhr.timeout = 3000; // 设置超时时间
// 超时处理函数
xhr.ontimeout = function(e) {
console.error("timeout")
};
// 请求错误处理函数
xhr.onerror = function(e) {
console.error("error")
};
// 请求进度处理函数
xhr.upload.onprogress = function(e) {
if (e.lengthComputable) {
var completedPercent = e.loaded / e.total; // 请求百分比
}
// 对completedPercent 操作
};
// 成功接收到请求的函数
xhr.onload = function(e) {
if(this.status = 200) {
console.log(this.responseText);
}
}
xhr.send(); // 发送请求
接下来,介绍一些常用的api:
更详细的可以看mdn的文档,里面有XMLHttpRequest对象所有的属性,方法。
¶open()`
XMLHttpRequest.open(method, url)
XMLHttpRequest.open(method, url, async)
XMLHttpRequest.open(method, url, async, user)
XMLHttpRequest.open(method, url, async, user, password)
method 就是需要使用的http方法,包括[GET],[POST],[PUT],[DELETE]等。
url就是请求的地址url。
async @可选 一个可选的布尔参数,默认为true,表示要不要异步执行操作。如果值为false,send()方法直到收到答复前不会返回。如果true,已完成事务的通知可供事件监听器使用。如果multipart属性为true则这个必须为true,否则将引发异常。
user@ 可选 用户名用于认证用途;默认为null。
password @可选 密码用于认证用途,默认为null。
¶setRequestHeader()
**XMLHttpRequest.setRequestHeader()** 是设置HTTP请求头部的方法。此方法必须在 open()方法和 send() 之间调用。如果多次对同一个请求头赋值,只会生成一个合并了多个值的请求头。
¶send()
void send();
void send(ArrayBuffer data);
void send(ArrayBufferView data);
void send(Blob data);
void send(Document data);
void send(DOMString? data);
void send(FormData data);
方法用于发送 HTTP 请求。如果是异步请求(默认为异步请求),则此方法会在请求发送后立即返回;如果是同步请求,则此方法直到响应到达后才会返回。XMLHttpRequest.send() 方法接受一个可选的参数,其作为请求主体;如果请求方法是 GET 或者 HEAD,则应将请求主体设置为 null,此时参数应该在url后面。
😏 自定义一些header属性进行跨域请求时,可能会遇到"not allowed by Access-Control-Allow-Headers in preflight response",你可能需要在你的服务端设置"Access-Control-Allow-Headers"。
接下来从一个完整的http请求流程介绍使用
¶设置request header
发送一个http请求之前,有时候我们需要设置一些请求头部信息。
比如常用的cookie,content-type等。
我们可以用setRequestHeader(DOMString header, DOMString value)这个函数设置。
- header大小写不明显,
cookie和Cookie是等价的。 Content-Type可以发送的数据类型可以看上面send函数的参数类型。- 设置header 必须在
open和send之间,否则会报错。 - 这个函数可以调用多次,在
send之前会合并多个调用设置的值。
eg:
var xhr = new XMLHttpRequest()
xhr.open('get','url')
xhr.setRequestHeader('content-type','multipart/form-data')
xhr.setRequestHeader('cookie','asdaadfasdfasdfa')
// 上面设置后会合并两个header
xhr.send()
¶获取Response Header
获取response header 可以通过getAllResponseHeaders() 和getResponseHeader(header)
从名字上可以看出来,一个是获取所有的header ,一个是获取指定的某个header 对应的值。
getResponseHeader(header) 的参数不分大小写。
但是: 这里获取部分header 是有限制的,为了安全考虑,不管是不是同源都规定客户端无法获取Set-Cookie这个字段,在跨域的请求里,只能获取“simple response header”和“Access-Control-Expose-Headers”。
解释:
“
simple response header“包括的 header 字段有:Cache-Control,Content-Language,Content-Type,Expires,Last-Modified,Pragma;
”Access-Control-Expose-Headers”:首先得注意是"Access-Control-Expose-Headers"进行跨域请求时响应头部中的一个字段,对于同域请求,响应头部是没有这个字段的。这个字段中列举的 header 字段就是服务器允许暴露给客户端访问的字段。
eg:
xhr.getResponseHeader('Cache-Control')
¶指定返回的数据类型
用过jquery的ajax都知道里面有个属性dataType可以指定服务器返回的数据类型。
如果想通过XMLHttpRequest设置返回的数据类型,可以通过xhr.responseType属性来设置。responseType是xhr level 2新增的属性,用来指定xhr.response的数据类型,目前还存在些兼容性问题。
可以设置的值是:
| 值 | xhr.response 数据类型 |
说明 |
|---|---|---|
"" |
String字符串 |
默认值(在不设置responseType时) |
"text" |
String字符串 |
|
"document" |
Document对象 |
希望返回 XML 格式数据时使用 |
"json" |
javascript 对象 |
存在兼容性问题,IE10/IE11不支持 |
"blob" |
Blob对象 |
|
"arrayBuffer" |
ArrayBuffer对象 |
eg.
var xhr = new XMLHttpRequest();
xhr.open('GET', '/path/to/image.png', true);
xhr.responseType = 'blob'
xhr.onload = function(e){
if (this.status == 200) {
// 这里有三个可以获取返回值的属性,分别是 response,responseType responseXml
var bloob = this.response;
}
}
xhr.send()
获取返回值:
xhr.response- 默认值:空字符串
"" - 当请求完成时,此属性才有正确的值
- 请求未完成时,此属性的值可能是
""或者null,具体与xhr.responseType有关:当responseType为""或"text"时,值为"";responseType为其他值时,值为null
- 默认值:空字符串
xhr.responseText- 默认值为空字符串
"" - 只有当
responseType为"text"、""时,xhr对象上才有此属性,此时才能调用xhr.responseText,否则抛错 - 只有当请求成功时,才能拿到正确值。以下2种情况下值都为空字符串
"":请求未完成、请求失败
- 默认值为空字符串
xhr.responseXML- 默认值为
null - 只有当
responseType为"text"、""、"document"时,xhr对象上才有此属性,此时才能调用xhr.responseXML,否则抛错 - 只有当请求成功且返回数据被正确解析时,才能拿到正确值。以下3种情况下值都为
null:请求未完成、请求失败、请求成功但返回数据无法被正确解析时
- 默认值为
¶追踪ajax 的当前状态
上文已经说过了XMLHttpRequest有readyState状态,共有5种。
| 值 | 状态 | 描述 |
|---|---|---|
0 |
UNSENT |
代理被创建,但尚未调用 open() 方法。 |
1 |
OPENED |
open() 方法已经被调用。 |
2 |
HEADERS_RECEIVED |
send() 方法已经被调用,并且头部和状态已经可获得。 |
3 |
LOADING |
下载中; responseText 属性已经包含部分数据。 |
4 |
DONE |
下载操作已完成。 |
状态改变的时候,会触发onreadystatechange事件。
xhr.onreadystatechange = function () {
switch(xhr.readyState){
case 1://OPENED
//do something
break;
case 2://HEADERS_RECEIVED
//do something
break;
case 3://LOADING
//do something
break;
case 4://DONE
//do something
break;
}
¶设置超时时间
timeout 属性 单位 毫秒
如何计算时间?
开始时间指的是 send方法调用的时候
结束时间是loadend事件触发
😏注意在同步的请求里面,这个值必须设置为0.否则会报错。
¶同步请求
不建议发送同步请求
一定要发送的话
open(method, url [, async = true [, username = null [, password = null]]])
// async 设置为false
¶显示进度
我们可以通过onprogress事件来实时显示进度,默认情况下这个事件每50ms触发一次。
但是,上传和下载,是不同对象的onprogress事件。
| 上传 | xhr.upload |
|---|---|
| 下载 | xhr |
xhr.onprogress = function(e) {
if(e.lengthComputable) {
var p = e.loaded / e.total
}
}
¶send(data)的参数
xhr.send(data)的参数data可以是以下几种类型:
ArrayBufferBlobDocumentDOMStringFormDatanull
data的值会影响请求头部content-type的默认值。
- 如果
data是Document类型,同时也是HTML Document类型,则content-type默认值为text/html;charset=UTF-8;否则为application/xml;charset=UTF-8; - 如果
data是DOMString类型,content-type默认值为text/plain;charset=UTF-8; - 如果
data是FormData类型,content-type默认值为multipart/form-data; boundary=[xxx] - 如果
data是其他类型,则不会设置content-type的默认值
¶4.ajax和js单线程机制
在说这个话题之前,需要先了解下js的单线程机制和浏览器的多线程机制:
JavaScript 运行机制详解:再谈Event Loop
批注版:JavaScript 运行机制详解:再谈Event Loop
这里没有黑阮老师的意思,阮老师翻译了很多优质的外文,他的本业是金融,对于计算机知识不够严谨,但对于科普,做简单的了解是足够的,并且阮老师也对博文内的错误做了修改。所以,我们也要明白,要想真正学到东西,还是要去看标准文档,比如mdn web doc,去看学校里的基础课程,任何人的二手知识,只能拿来参考,绝不能作为标准。
我们都知道,JavaScript是一门单线程的语言,但是它运行的环境(这里☞的是浏览器)是多线程的。用当前最流行的Chrome浏览器作为例子。
有以下几个常驻线程:
- 渲染引擎线程:顾名思义,该线程负责页面的渲染
JS引擎线程:负责JS的解析和执行- 定时触发器线程:处理定时事件,比如
setTimeout,setInterval - 事件触发线程:处理
DOM事件 - 异步
http请求线程:处理http请求
需要注意的是,渲染线程和JS引擎线程是不能同时进行的。渲染线程在执行任务的时候,JS引擎线程会被挂起。因为JS可以操作DOM,若在渲染中JS处理了DOM,浏览器可能就不知所措了。
另外一个需要提起的是浏览器实现异步的消息队列和事件循环
浏览器有一个主线程用来执行代码,还有一些其他的线程执行比较耗时的操作,比如http请求,定时器等,所有的函数调用都在一个stack里面,主线程会依次执行这个stack里的代码,当遇到一个异步的操作时,异步线程执行异步操作,然后当异步执行完毕,会把回调函数作为一个任务加到一个消息队列queue里面(实际上,有多个消息队列,这里为了说明做了简化),浏览器不断的监听着stack,一旦stack被清空。就从队列里取出一个任务加到stack里面,然后主线程执行这个任务。
由于js引擎从消息队列里面读取事件任务时不间断的,只要stack清空,就会有新的任务加到里面,如果消息队列里没有任务,就会一直等待,这就是事件循环Event Loop。
类似下面代码:
while (queue.waitForMessage()) {
queue.processNextMessage();
}
这里插播一道题:
new Promise(resolve => {
resolve(1);
Promise.resolve().then(() => console.log(2));
console.log(4)
}).then(t => console.log(t));
console.log(3);
答案是 4,3,2,1
具体的解释点这里从一道题浅说 JavaScript 的事件循环。
回归正传,
当发起一个ajax请求,调用send方法后,浏览器开启新的线程,发起网络请求,js主线程会继续向下执行,当当ajax请求被服务器响应并且收到response后, 浏览器事件触发线程捕获到了ajax的回调事件 onreadystatechange (当然也可能触发onload, 或者 onerror等等) . 该回调事件并没有被立即执行, 而是被添加到 任务队列 的末尾. 直到js引擎空闲了, 任务队列 的任务才被捞出来, 按照添加顺序, 挨个执行, 当然也包括刚刚append到队列末尾的 onreadystatechange 事件。
¶5.ajax和跨域
首先要了解什么是跨域,跨域就是因为浏览器的同源策略,造成的访问限制。
关于解决方案,看下文:
[ajax跨域,这应该是最全的解决方案了](https://segmentfault.com/a/1190000012469713)
¶6.fetch api
Fetch API 提供了一个获取资源的接口(包括跨域请求)。任何使用过 XMLHttpRequest 的人都能轻松上手,但新的API提供了更强大和灵活的功能集。并且fetch支持promise语法。
关于promise点击Promise对象
关于fetch点击 Fetch API
Fetch有个缺点是不支持进度。
使用方法:
// 简单例子
fetch('http://example.com/movies.json')
.then(function(response) {
return response.json();
})
.then(function(myJson) {
console.log(myJson);
});
// 支持请求参数
function postData(url, data) {
// Default options are marked with *
return fetch(url, {
body: JSON.stringify(data), // must match 'Content-Type' header
cache: 'no-cache', // *default, no-cache, reload, force-cache, only-if-cached
credentials: 'same-origin', // include, same-origin, *omit
headers: {
'user-agent': 'Mozilla/4.0 MDN Example',
'content-type': 'application/json'
},
method: 'POST', // *GET, POST, PUT, DELETE, etc.
mode: 'cors', // no-cors, cors, *same-origin
redirect: 'follow', // manual, *follow, error
referrer: 'no-referrer', // *client, no-referrer
})
.then(response => response.json()) // parses response to JSON
}
// 默认情况下,fetch 不会从服务端发送或接收任何 cookies,如果站点依赖于用户 session,则会导致未经认证的请求(要发送 cookies,必须设置 credentials 选项)
fetch('https://example.com', {
credentials: 'include'
})
tags:要在不支持的浏览器中使用Fetch,可以使用Fetch Polyfill。
¶7.Axios使用
Axios 是一个基于 promise 的 HTTP 库,可以用在浏览器和 node.js 中。如果你不想为了简单的发送ajax 请求就要下载庞大的jquery,只有几kb大小的axios是很好的替代产品。
使用方法:
// 执行get 请求
// 为给定 ID 的 user 创建请求
axios.get('/user?ID=12345')
.then(function (response) {
console.log(response);
})
.catch(function (error) {
console.log(error);
});
// 可选地,上面的请求可以这样做
axios.get('/user', {
params: {
ID: 12345
}
})
.then(function (response) {
console.log(response);
})
.catch(function (error) {
console.log(error);
});
// 执行post
axios.post('/user', {
firstName: 'Fred',
lastName: 'Flintstone'
})
.then(function (response) {
console.log(response);
})
.catch(function (error) {
console.log(error);
});
// 执行多个并发请求
function getUserAccount() {
return axios.get('/user/12345');
}
function getUserPermissions() {
return axios.get('/user/12345/permissions');
}
axios.all([getUserAccount(), getUserPermissions()])
.then(axios.spread(function (acct, perms) {
// 两个请求现在都执行完成
}));