聊一聊 Ajax

230 阅读14分钟

什么是 Ajax

Ajax(Asynchronous JavaScript and XML) 即:“异步的JavaScript与XML”,指的是一套综合了多项技术的浏览器端网页开发技术。Ajax的概念由杰西·詹姆士·贾瑞特(Jesse James Garrett)所提出。如 Ajax: A New Approach to Web Applications所述,Ajax整合了以下技术:

  • standards-based presentation using XHTML and CSS;
  • dynamic display and interaction using the Document Object Model;
  • data interchange and manipulation using XML and XSLT;
  • asynchronous data retrieval using XMLHttpRequest;
  • and JavaScript binding everything together.

Ajax 与 传统网络请求方式的区别

传统的Web应用允许用户端填写表单(form),当提交表单时就向网页服务器发送一个请求。服务器接收并处理传来的表单,然后送回一个新的网页,但这个做法浪费了许多带宽,因为在前后两个页面中的大部分HTML代码往往是相同的。由于每次应用的沟通都需要向服务器发送请求,应用的回应时间依赖于服务器的回应时间。这导致了用户界面的回应比本机应用慢得多。

与此不同,AJAX应用可以仅向服务器发送并取回必须的数据,并在客户端采用JavaScript处理来自服务器的回应。因为在服务器和浏览器之间交换的数据大量减少,服务器回应更快了。同时,很多的处理工作可以在发出请求的客户端机器上完成,因此Web服务器的负荷也减少了。

image.png 此外,Ajax引擎允许用户与应用程序的交互以异步方式进行。如下图所示:

image.png

综上所述: Ajax 可以简单理解为是一种从服务器异步加载数据并有选择地更新部分网页而不重新加载整个页面的方法**。

注意:
(1)Ajax 不是一项新技术,事实上,Ajax 甚至根本不是真正的技术。Ajax 只是一个术语,用于描述通过 JavaScript 从 Web 服务器异步交换数据的过程,无需刷新页面。
(2)不要对 AJAX 中的术语X(即XML )感到困惑。 它只是出于历史原因而存在。可以使用 JSON、HTML 或纯文本等其他数据交换格式来代替 XML。

一、XMLHttpRequest

XMLHttpRequest(XHR)对象用于与服务器交互。通过 XMLHttpRequest 可以在不刷新页面的情况下请求特定 URL,获取数据。这允许网页在不影响用户操作的情况下,更新页面的局部内容。XMLHttpRequest 在 AJAX 编程中被大量使用

1.1 XMLHttpRequest() 构造函数

XMLHttpRequest() 构造函数该构造函数用于初始化一个 XMLHttpRequest 实例对象。在调用下列任何其他方法之前,必须先调用该构造函数,或通过其他方式,得到一个实例对象。

const request = new XMLHttpRequest();

1.2 XMLHttpRequest 对象的属性

(1)readyState

XMLHttpRequest.readyState 属性返回 一个无符号短整型(unsigned short)数字,代表请求的状态码

状态描述
0UNSENTXMLHttpRequest 代理已被创建, 但尚未调用 open() 方法。
1OPENEDopen() 方法已经被调用。
2HEADERS_RECEIVEDsend() 方法已经被调用,并且头部和状态已经可获得。
3LOADING响应体部分正在被接收。如果 responseType 属性是“text”或空字符串, responseText 将会在载入的过程中拥有部分响应数据。
4DONE请求操作已经完成。这意味着数据传输已经彻底完成或失败

(2)onreadystatechange

只要 readyState 属性发生变化,就会调用相应的处理函数。这个回调函数会被用户线程所调用。XMLHttpRequest.onreadystatechange 会在 XMLHttpRequest 的 readyState属性发生改变时触发 readystatechange 事件的时候被调用。

当一个 XMLHttpRequest 请求被 abort() 方法取消时,其对应的 readystatechange 事件不会被触发。

XMLHttpRequest.onreadystatechange = callback;

(3)response

XMLHttpRequest.response 属性返回响应的正文

返回的类型为 ArrayBuffer 、Blob 、 Document 、 JavaScript Object 或 DOMString 中的一个。 这取决于 responseType 属性。

var body = XMLHttpRequest.response;

(4)responseText

XMLHttpRequest.responseText 在一个请求被发送后,从服务器端返回文本

  • DOMStringXMLHttpRequest 返回的纯文本的值。

    • DOMStringnull时,表示请求失败了。
    • DOMString为""时,表示这个请求还没有被send()
  • 当处理一个异步request的时候,尽管当前请求并没有结束,responseText的返回值是当前从后端收到的内容。

  • 当请求状态readyState 变为 XMLHttpRequest.DONEstatus 值为200("OK")时,responseText全部后端的返回数据

var resultText = XMLHttpRequest.responseText;

注意:只有 responseType 是 '' 或者 'text' 时,该属性才能被读取。否则会抛出 DOMException 异常。

(5)responseType

XMLHttpRequest.responseType 是一个枚举字符串值,用于指定响应中包含的数据类型

它还允许更改响应类型。如果将 responseType 的值设置为空字符串,则会使用 text 作为默认值。

var type = XMLHttpRequest.responseType;

XMLHttpRequest.responseType = type;

可以设置为以下值:

  • "":空的 responseType 字符串与默认类型 "text" 相同。
  • "arraybuffer"response 是一个包含二进制数据的 JavaScript ArrayBuffer
  • "blob"response 是一个包含二进制数据的 Blob 对象。
  • "document"response 是一个 HTML Document 或 XML XMLDocument,根据接收到的数据的 MIME 类型而定。
  • "json"response 是通过将接收到的数据内容解析为 JSON 而创建的 JavaScript 对象。
  • "text"response 是 DOMString 对象中的文本。

注意:responseType 属性一定要在 LOADING 状态之前设置

(6)XMLHttpRequest.responseURL

只读属性XMLHttpRequest.responseURL返回响应的序列化URL

  • 如果URL为空则返回空字符串。
  • 如果URL有锚点,则位于URL # 后面的内容会被删除。
  • 如果URL有重定向, responseURL 的值会是经过多次重定向后的最终 URL 。

(7)responseXML

XMLHttpRequest.responseXML 属性是一个只读值,它返回一个包含请求检索的 HTML 或 XML 的Document

  • 如果请求未成功,尚未发送,或者检索的数据无法正确解析为 XML 或 HTML,则为 null。
  • 默认是当作“text / xml” 来解析。
  • 当 responseType 设置为 “document” 并且请求已异步执行时,响应将被当作 “text / html” 来解析。
  • 对于任何其他类型的数据以及 data: URLs 为 null。

InvalidStateErrorresponseType 既不是 "document" 也不是空字符串 (接收的数据应是XML 或 HTML).

var data = XMLHttpRequest.responseXML;

注意:只有 responseType 是 '' 或者 'document' 时,该属性才能被读取。否则会抛出 DOMException 异常。

(8)status

只读属性 XMLHttpRequest.status 返回了XMLHttpRequest 响应中的数字状态码

status 的值是一个无符号短整型。在请求完成前,status的值为0。值得注意的是,如果 XMLHttpRequest 出错,浏览器返回的 status 也为0。

该状态码是标准的HTTP状态码,取值如下所示: HTTP状态码.png

(9)statusText

只读属性 XMLHttpRequest.statusText 返回了XMLHttpRequest 请求中由服务器返回的一个 DOMString 类型的文本信息,这则信息中也包含了响应的数字状态码

不同于使用一个数字来指示的状态码XMLHTTPRequest.status,这个属性包含了返回状态对应的文本信息,例如"OK"或是"Not Found"。

  • 如果请求的状态readyState的值为"UNSENT"或者"OPENED",则这个属性的值将会是一个空字符串。
  • 如果服务器未明确指定一个状态文本信息,则statusText的值将会被自动赋值为"OK"。

(10)timeout

XMLHttpRequest.timeout 是一个无符号长整型数,代表着一个请求在被自动终止前所消耗的毫秒数。默认值为 0,意味着没有超时。

超时并不应该用在一个 document environment中的同步 XMLHttpRequests  请求中,否则将会抛出一个 InvalidAccessError 类型的错误。

当超时发生, timeout事件将会被触发。

在IE中,超时属性可能只能在调用 open() 方法之后且在调用 send() 方法之前设置。

(11)upload

XMLHttpRequest.upload 属性返回一个XMLHttpRequestUpload对象,用来表示上传的进度。这个对象是不透明的,但是作为一个XMLHttpRequestEventTarget,可以通过对其绑定事件来追踪它的进度。

(12)withCredentials

XMLHttpRequest.withCredentials 属性是一个Boolean类型,它指示了是否该使用类似cookies,authorization headers(头部授权)或者TLS客户端证书这一类资格证书来创建一个跨站点访问控制(cross-site Access-Control)请求。在同一个站点下使用withCredentials属性是无效的。

此外,这个指示也会被用做响应中cookies 被忽视的标示。默认值是false。

如果在发送来自其他域的XMLHttpRequest请求之前,未设置withCredentials 为true,那么就不能为它自己的域设置cookie值。而通过设置withCredentials 为true获得的第三方cookies,将会依旧享受同源策略,因此不能被通过 document.cookie 或者从头部相应请求的脚本等访问。

(13)channel

创建请求的时候, XMLHttpRequest.channel 是一个被对象使用的  nsIChannel 。如果管道(channel) 还没被创建的话,它的值是 null 。在一个 multi-part  请求案例中,它是初始化的管道,不是 multi-part 请求中的不同部分。

(14)XMLHttpRequest.mozAnon

XMLHttpRequest.mozAnon 是布尔类型。如果这个变量为真,则这次请求将不携带cookies或头部认证信息来发送。

(15)mozSystem

XMLHttpRequest.mozSystem 是一个布尔值。如果它被设置为true,那么在请求时就不会强制要求执行同源策略(Same Origin Policy)。

(16)mozBackgroundRequest

XMLHttpRequest.mozBackgroundRequest是一个布尔对象,表示object是否为后台的服务请求。 如果为true,则不会将任何加载组与请求关联,并且不会向用户显示安全对话框。`

请求失败时通常会显示安全对话框(例如身份验证或错误证书通知)

1.3 XMLHttpRequest 对象的方法

(1)open()

XMLHttpRequest.open() 方法初始化一个请求

xhrReq.open(method, url);
xhrReq.open(method, url, async);
xhrReq.open(method, url, async, user);
xhrReq.open(method, url, async, user, password);
  • method:要使用的HTTP方法,比如「GET」、「POST」、「PUT」、「DELETE」、等。对于非HTTP(S) URL被忽略。
  • url:一个 DOMString 表示要向其发送请求的URL。
  • async(可选):一个可选的布尔参数,表示是否异步执行操作,默认为true
    • 如果值为falsesend()方法直到收到答复前不会返回。
    • 如果true,已完成事务的通知可供事件监听器使用。 如果multipart属性为true则这个必须为true,否则将引发异常
  • user(可选):可选的用户名用于认证用途;默认为null
  • password(可选):选的密码用于认证用途,默认为null

(2)send()

XMLHttpRequest.send()  方法用于发送 HTTP 请求

  • 如果是异步请求(默认为异步请求),则此方法会在请求发送后立即返回;
  • 如果是同步请求,则此方法直到响应到达后才会返回。 XMLHttpRequest.send() 方法接受一个可选的参数,其作为请求主体;如果请求方法是 GET 或者 HEAD,则应将请求主体设置为 null。
XMLHttpRequest.send(body)
  • body(可选):在XHR请求中要发送的数据体
    • 可以为 Document, 在这种情况下,它在发送之前被序列化.
    • 为 XMLHttpRequestBodyInit,可以是 BlobBufferSourceFormDataURLSearchParams, 或者 USVString 对象.
    • null: 如果body没有指定值,则默认值为 null .
XMLHttpRequest.send();
XMLHttpRequest.send(ArrayBuffer data);
XMLHttpRequest.send(ArrayBufferView data);
XMLHttpRequest.send(Blob data);
XMLHttpRequest.send(Document data);
XMLHttpRequest.send(DOMString? data);
XMLHttpRequest.send(FormData data);

异常:

ExceptionDescription
InvalidStateErrorsend() has already been invoked for the request, and/or the request is complete.
NetworkErrorThe resource type to be fetched is a Blob, and the method is not GET.

(3)abort()

如果该请求已被发出,XMLHttpRequest.abort() 方法将终止该请求。当一个请求被终止,它的 readyState 将被置为0 , 并且请求的 status 置为 0。

xhrInstance.abort();

(4)getAllResponseHeaders()

XMLHttpRequest.getAllResponseHeaders()  方法返回所有的响应头,以 CRLF 分割的字符串,或者 null 如果没有收到任何响应。

注意:  对于复合请求 ( multipart requests ),这个方法返回当前请求的头部,而不是最初的请求的头部。

var headers = XMLHttpRequest.getAllResponseHeaders();

(5)getResponseHeader()

XMLHttpRequest.getResponseHeader() 方法返回包含指定响应头文本的字符串。

如果在返回的响应头中有多个一样的名称,那么返回的值就会是用逗号和空格将值分隔的字符串。getResponseHeader()方法以UTF编码返回值。搜索的报文名是不区分大小写的。

var myHeader = XMLHttpRequest.getResponseHeader(name);

(6)setRequestHeader()

XMLHttpRequest.setRequestHeader()设置HTTP请求头部的方法。

此方法必须在  open()方法和 send() 之间调用。如果多次对同一个请求头赋值,只会生成一个合并了多个值的请求头。

如果没有设置 Accept属性,则此发送出send()的值为此属性的默认值*/* 。

myReq.setRequestHeader(header, value);
  • header:属性的名称。
  • value:属性的值。

(7)overrideMimeType()

XMLHttpRequest.overrideMimeType 方法是指定一个MIME类型用于替代服务器指定的类型,使服务端响应信息中传输的数据按照该指定MIME类型处理。

此方法必须在send方法之前调用方为有效。

XMLHttpRequest.overrideMimeType(mimeType)

1.4 XMLHttpRequestEventTarget

XMLHttpRequestEventTarget 是一个描述事件处理程序的接口,你可以在一个用于处理 XMLHttpRequest 事件的对象中使用到该事件处理程序。

事件相应属性的信息类型
onloadstart开始传送数据
onprogress数据传输进行中
onabort获取操作终止
onerror获取失败
onload获取成功
ontimeout获取操作在用户规定的时间内未完成
onloadend获取完成(不论成功与否)

(1)onloadstart

XMLHttpRequestEventTarget.onloadstart 在 XMLHttpRequest开始传送数据时被调用

XMLHttpRequest.onloadstart = callback;

(2)onprogress

XMLHttpRequestEventTarget.onprogress 是在 XMLHttpRequest 完成之前周期性调用的函数。

XMLHttpRequest.onprogress = function (event) {
  event.loaded;
  event.total;
};
  • event.loaded  已传输的数据量
  • event.total  总共的数据量

(3)onabort

XMLHttpRequestEventTarget.onabort 会在 XMLHttpRequest 请求操作被诸如 XMLHttpRequest.abort()函数中止时调用。

(4)onerror

XMLHttpRequestEventTarget.onerror 是XMLHttpRequest 事务由于错误而失败时调用的函数。

请注意只有在网络层级出现错误时才会调用此函数。如果错误只出现在应用层(比如发送一个HTTP的错误码),这个方法将不会被调用。

(5)onload

XMLHttpRequestEventTarget.onload 是 XMLHttpRequest请求成功完成时调用的函数。

(6)ontimeout

XMLHttpRequestEventTarget.ontimeout当超时时调用,接受 timeout 对象作为参数;只有设置了 XMLHttpRequest 对象的 timeout 属性时,才可能发生超时事件。

(7)onloadend

XMLHttpRequestEventTarget.onloadend当内容加载完成,不管失败与否,都会调用该方法,接受 loadend 对象作为参数。

1.5 XMLHttpRequest 的使用

通常使用XMLHttpRequest发送请求的一般步骤如下:

let xhr = new XMLHttpRequest()  // 初始化 XMLHttpRequest 实例对象

// 设置 onreadystatechange 事件处理程序
xhr.onreadystatechange = () => {  
    if (xhr.readyState === 4) {  // readyState 为 4 代表请求完成
        if (xhr.status >= 200 && xhr.status < 300) {  //200-300请求成功
            console.log(xhr.responseText);
        } else {
            console.log('request failed!')
        }
    }
}
xhr.open('post', 'http://localhost:3000/test')  // 初始化一个请求
xhr.send();  // 发送 HTTP 请求

有关XMLHttpRequest的详细使用可参见MDN中的使用 XMLHttpRequest

二、跨域资源共享(Cross-Origin Resource Sharing, CORS)

跨源资源共享 (CORS)一种基于 HTTP 头的机制,该机制通过允许服务器标示除了它自己以外的其它 origin(域,协议和端口),使得浏览器允许这些 origin 访问加载自己的资源。

出于安全性,浏览器限制脚本内发起的跨源HTTP请求。 例如,XMLHttpRequest 和 Fetch API遵循同源策略。这意味着使用这些 API 的 Web 应用程序只能从加载应用程序的同一个域请求 HTTP 资源,除非响应报文包含了正确 CORS 响应头。

2.1 浏览器的同源策略

同源策略是一个重要的安全策略,它用于限制一个 origin 的文档或者它加载的脚本如何能与另一个源的资源进行交互。它能帮助阻隔恶意文档,减少可能被攻击的媒介。

(1)同源的定义

源(origin) 由用于访问它的URL的协议、主机(域名)、端口定义。

如果两个 URL 的 协议(protocol)、主机(host)、端口(port) 都相同的话,则这两个 URL 是同源。这个方案也被称为“协议/主机/端口元组”。

下表给出了与 URL http://store.company.com/dir/page.html 的源进行对比的示例:

URL结果原因
http://store.company.com/dir2/other.html同源只有路径不同
http://store.company.com/dir/inner/another.html同源只有路径不同
https://store.company.com/secure.html失败协议不同
http://store.company.com:81/dir/etc.html失败端口不同 ( http:// 默认端口是80)
http://news.company.com/dir/other.html失败主机不同

2.2 简单请求

某些请求不会触发 CORS 预检请求,例如:

  • method 为 GETHEADPOST 的请求
  • Content-Type的值为下列三者之一:
    • text/plain
    • multipart/form-data
    • application/x-www-form-urlencoded

这种简单请求没有自定义头部,但是在发送时会有一个额外的头部 OriginOrigin 包含发送请求的页面的源(协议、主机和端口),以便服务器确定是否为其提供响应。例如:

Origin: https://foo.example

如果服务器决定响应请求,那么应该发送 Access-Control-Allow-Origin 头部,包含相同的源,或者如果资源是公开的,那么就包含*,例如:

// 允许任意外域访问
Access-Control-Allow-Origin: https://foo.example

// 仅允许来自 `https://foo.example` 的访问
Access-Control-Allow-Origin: *

出于安全性考虑,跨域 xhr 对象施加了一些额外限制:

  • 不能使用 setRequestHeader() 设置自定义头部
  • 不能发送和接收cookie
  • getAllResponseHeaders() 方法始终返回空字符串

2.3 预检请求(preflight request)

与前述简单请求不同,“需预检的请求”要求必须首先使用 OPTIONS方法发起一个预检请求到服务器,以获知服务器是否允许该实际请求。"预检请求“的使用,可以避免跨域请求对服务器的用户数据产生未预期的影响。

使用 OPTIONS方法发起一个预检请求包含以下头部:

  • Origin:与简单请求相同
  • Access-Control-Request-Method:指示未来对同一资源的 CORS 请求可能使用哪种方法
  • Access-Control-Request-Headers:指示未来对同一资源的 CORS 请求可能使用哪些头部(即使用逗号分隔的自定义头部列表) 如下是一个需要执行预检请求的 HTTP 请求:
const xhr = new XMLHttpRequest();
xhr.open('POST', 'https://bar.other/resources/post-here/');
xhr.setRequestHeader('X-PINGOTHER', 'pingpong');
xhr.setRequestHeader('Content-Type', 'application/xml');
xhr.onreadystatechange = handler;
xhr.send('<person><name>Arun</name></person>');

预检请求中同时携带了下面两个首部字段:

Access-Control-Request-Method: POST
Access-Control-Request-Headers: X-PINGOTHER, Content-Type

在这个请求发送后,服务器可以确定是否允许这种类型的请求。服务器会通过在响应中发送如下头部与浏览器沟通这些信息。

  • Access-Control-Allow-Origin:表示响应是否可以共享,通过返回Origin'请求头的字面值(可以是null')或响应中的`*'
  • Access-Control-Allow-Methods:指示出于 CORS 协议的目的,响应的 URL 支持哪些方法
  • Access-Control-Allow-Headers:指示出于 CORS 协议的目的,响应的 URL 支持哪些头部
  • Access-Control-Allow-Methods:表示 "Access-Control-Allow-Methods "和 "Access-Control-Allow-Headers "头部提供的信息可以被缓存的秒数(默认为5) 例如:
Access-Control-Allow-Origin: https://foo.example
Access-Control-Allow-Methods: POST, GET, OPTIONS
Access-Control-Allow-Headers: X-PINGOTHER, Content-Type
Access-Control-Max-Age: 86400

预检请求完成之后,才会发送实际请求。

2.4 附带身份凭证的请求

默认情况下,跨域请求不通过凭据(cookie、HTTP认证、客户端SSL证书)。可以通过设置withCredentials 属性值为 true 来表明请求会发送凭据。

如果服务器允许带凭据的请求,那么可以在响应中设置如下HTTP头部:

Access-Control-Allow-Credentials: true

有关跨域详解,可见跨源资源共享(CORS)

2.5 JSONP(JSON with padding)

<script> 标签 是一种可以跨域请求内容的机制。

2005 年 12 月,Bob Ippolito 正式提出 JSONP(后来被称为 JSON-P,或 JSON-with-padding)作为一种利用 <script> 标签的属性来跨域请求 JSON 格式的数据的方法。

JSON-P 的工作原理是制作一个 <script> 元素(在 HTML 标记中或通过 JavaScript 插入到 DOM 中),该元素请求远程数据服务位置。响应(加载的“JavaScript”内容)是请求网页上预定义的函数的名称,传递给它的参数是请求的 JSON 数据。当脚本执行时,该函数被调用并传递 JSON 数据,允许请求页面接收和处理数据。

我们向

在提供的 JSONP 示例中,我们将执行如下函数 -

<script src="http://www.example.com/json_data?callback=processData"></script>

通过这样做, processData 将使用给定的参数执行

举个例子:

function handleResponse(response) {
    console.log(`
    You're at IP address ${response.ip}, which is in
    ${response.city}, ${response.region_name}`);
}
let script = document.createElement("script");
script.src = "http://freegeoip.net/json/?callback=handleResponse";
document.body.insertBefore(script, document.body.firstChild);

JSONP 的缺点

  • JSONP 是从不同的域拉取的代码。如果这个域不可信,则可能在响应中加入恶意内容。从事除了完全删除JSONP没有其他办法
  • 不好确定 JSONP 请求是否失败。虽然 HTML5 规定了 <script> 元素的 onerror 事件处理程序,但还没有被任何浏览器实现。

有关更多JSONP的介绍,可见这篇博文

本文到这里就先结束了,有关其他一些异步请求的方法(例如:Fetch APIaxios等)这里就先不介绍了。全文大部分都来自MDN和《JavaScript高级程序设计(第四版)》也是个人的一点学习记录。