彻底搞懂ajax

Author Avatar
zzz1220 4月 19, 2019
  • 在其它设备中阅读本文章
👉👉本文共5.2k字📘 读完共需20分钟⌚

每个会用jquery的人都会用$.ajax发起一个ajax请求,然后再回调函数里面取得返回的数据,但是并不是所有的人都知道这个流程里面发生了什么事,本文就深入讨论下关于ajax的所有(大部分)细节。

[TOC]

1.Ajax其实是标准

很多人误以为XMLHttpRequest就是Ajax,其实不然,他们两个是两个不同维度的概念。

搜索ajaxwiki,是这样介绍的:

AJAX即“Asynchronous JavaScript and XML”(异步的JavaScriptXML技术),指的是一套综合了多项技术的浏览器网页开发技术。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是一种技术方案,XMLHttpRequestajax在web端实现所依赖的一个对象,是这个对象使得浏览器可以发出HTTP请求与接收HTTP响应,来异步的操作网页。

​ 只是现在,市面上基本都用XMLHttpRequest 来发送ajax请求。

es6新增了fetch来代替XMLHttpRequest ,比起XMLHttpRequest具有更好的可扩展性和高效性,下文会详细介绍。

2.XMLHttpRequest介绍

XMLHTTP是一组API函数集,可被JavaScript、JScript、VBScript以及其它web浏览器内嵌的脚本语言调用,通过HTTP在浏览器和web服务器之间收发XML或其它数据。XMLHTTP最大的好处在于可以动态地更新网页,它无需重新从服务器读取整个网页,也不需要安装额外的插件。该技术被许多网站使用,以实现快速响应的动态网页应用。例如:GoogleGmail服务、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 1level 2 的区别

特性 level 1 level 2
文本数据传送 支持 支持
读取上传二进制 不支持 支持
进度信息 不支持 支持
同源限制 不支持跨域 可发送跨域请求
超时时间 不支持自己设置 xhr.timeout设置超时时间

​ 此外,为了方便表单处理,html5新加了个FormData对象,新的标准同样支持这个对象,用来模拟表单。

关于level 1level 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对象所有的属性,方法。

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,表示要不要异步执行操作。如果值为falsesend()方法直到收到答复前不会返回。如果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大小写不明显,cookieCookie是等价的。
  • Content-Type可以发送的数据类型可以看上面send函数的参数类型。
  • 设置header 必须在opensend之间,否则会报错。
  • 这个函数可以调用多次,在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')

指定返回的数据类型

用过jqueryajax都知道里面有个属性dataType可以指定服务器返回的数据类型。

如果想通过XMLHttpRequest设置返回的数据类型,可以通过xhr.responseType属性来设置。responseTypexhr 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 的当前状态

上文已经说过了XMLHttpRequestreadyState状态,共有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可以是以下几种类型:

  • ArrayBuffer
  • Blob
  • Document
  • DOMString
  • FormData
  • null

data的值会影响请求头部content-type的默认值。

  • 如果dataDocument 类型,同时也是HTML Document类型,则content-type默认值为text/html;charset=UTF-8;否则为application/xml;charset=UTF-8
  • 如果dataDOMString 类型,content-type默认值为text/plain;charset=UTF-8
  • 如果dataFormData 类型,content-type默认值为multipart/form-data; boundary=[xxx]
  • 如果data是其他类型,则不会设置content-type的默认值

4.ajaxjs单线程机制

​ 在说这个话题之前,需要先了解下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和跨域

http访问控制

首先要了解什么是跨域,跨域就是因为浏览器的同源策略,造成的访问限制。

关于解决方案,看下文:

[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 是一个基于 promiseHTTP 库,可以用在浏览器和 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) {
    // 两个请求现在都执行完成
  }));