Node 接口
属性
Node.prototype.nodeType
Node 对象定义了几个常量,对应这些类型值。
document.nodeType === Node.DOCUMENT_NODE // true
上面代码中,文档节点的nodeType
属性等于常量Node.DOCUMENT_NODE 不同节点的
nodeType`属性值和对应的常量如下。
- 文档节点(document):9,对应常量
Node.DOCUMENT_NODE
- 元素节点(element):1,对应常量
Node.ELEMENT_NODE
- 属性节点(attr):2,对应常量
Node.ATTRIBUTE_NODE
- 文本节点(text):3,对应常量
Node.TEXT_NODE
- 文档片断节点(DocumentFragment):11,对应常量
Node.DOCUMENT_FRAGMENT_NODE
- 文档类型节点(DocumentType):10,对应常量
Node.DOCUMENT_TYPE_NODE
- 注释节点(Comment):8,对应常量
Node.COMMENT_NODE
确定节点类型时,使用nodeType
属性是常用方法。
var node = document.documentElement.firstChild;
if (node.nodeType === Node.ELEMENT_NODE) {
console.log('该节点是元素节点');
}
元素特性的相关属性
(1)Element.id
Element.id
属性返回指定元素的id
属性,该属性可读写。
// HTML 代码为 <p id="foo">
var p = document.querySelector('p');
p.id // "foo"
注意,id
属性的值是大小写敏感,即浏览器能正确识别<p id="foo">
和<p id="FOO">
这两个元素的id
属性,但是最好不要这样命名。
(2)Element.tagName
Element.tagName
属性返回指定元素的大写标签名,与nodeName
属性的值相等。
// HTML代码为
// <span id="myspan">Hello</span>
var span = document.getElementById('myspan');
span.id // "myspan"
span.tagName // "SPAN"
(3)Element.dir
Element.dir
属性用于读写当前元素的文字方向,可能是从左到右("ltr"
),也可能是从右到左("rtl"
)。
(4)Element.accessKey
Element.accessKey
属性用于读写分配给当前元素的快捷键。
// HTML 代码如下
// <button accesskey="h" id="btn">点击</button>
var btn = document.getElementById('btn');
btn.accessKey // "h"
上面代码中,btn
元素的快捷键是h
,按下Alt + h
就能将焦点转移到它上面。
(5)Element.draggable
Element.draggable
属性返回一个布尔值,表示当前元素是否可拖动。该属性可读写。
(6)Element.lang
Element.lang
属性返回当前元素的语言设置。该属性可读写。
// HTML 代码如下
// <html lang="en">
document.documentElement.lang // "en"
(7)Element.tabIndex
Element.tabIndex
属性返回一个整数,表示当前元素在 Tab 键遍历时的顺序。该属性可读写。
tabIndex
属性值如果是负值(通常是-1
),则 Tab 键不会遍历到该元素。如果是正整数,则按照顺序,从小到大遍历。如果两个元素的tabIndex
属性的正整数值相同,则按照出现的顺序遍历。遍历完所有tabIndex
为正整数的元素以后,再遍历所有tabIndex
等于0
、或者属性值是非法值、或者没有tabIndex
属性的元素,顺序为它们在网页中出现的顺序。
(8)Element.title
Element.title
属性用来读写当前元素的 HTML 属性title
。该属性通常用来指定,鼠标悬浮时弹出的文字提示框。
元素状态的相关属性
(1)Element.hidden
Element.hidden
属性返回一个布尔值,表示当前 HTML 元素的hidden
属性的值。该属性可读写,用来控制当前元素是否可见。
var btn = document.getElementById('btn');
var mydiv = document.getElementById('mydiv');
btn.addEventListener('click', function () {
mydiv.hidden = !mydiv.hidden;
}, false);
注意,该属性与 CSS 设置是互相独立的。CSS 对当前元素可见性的设置,Element.hidden
并不能反映出来。也就是说,这个属性并不能用来判断当前元素的实际可见性。
CSS 设置的优先级高于Element.hidden
。如果 CSS 指定了该元素不可见(display: none
)或可见(visibility: visible
),那么Element.hidden
并不能改变该元素实际的可见性。换言之,这个属性只在 CSS 没有明确设定当前元素的可见性时才有效。
(2)Element.contentEditable,Element.isContentEditable
HTML 元素可以设置contentEditable
属性,使得元素的内容可以编辑。
<div contenteditable>123</div>
上面代码中,<div>
元素有contenteditable
属性,因此用户可以在网页上编辑这个区块的内容。
Element.contentEditable
属性返回一个字符串,表示是否设置了contenteditable
属性,有三种可能的值。该属性可写。
"true"
:元素内容可编辑"false"
:元素内容不可编辑"inherit"
:元素是否可编辑,继承了父元素的设置
Element.isContentEditable
属性返回一个布尔值,同样表示是否设置了contenteditable
属性。该属性只读。
Element.attributes
Element.attributes
属性返回一个类似数组的对象,成员是当前元素节点的所有属性节点,详见《属性的操作》一章。
var p = document.querySelector('p');
var attrs = p.attributes;
for (var i = attrs.length - 1; i >= 0; i--) {
console.log(attrs[i].name + '->' + attrs[i].value);
}
上面代码遍历p
元素的所有属性。
Element.className,Element.classList
className
属性用来读写当前元素节点的class
属性。它的值是一个字符串,每个class
之间用空格分割。
classList
属性返回一个类似数组的对象,当前元素节点的每个class
就是这个对象的一个成员。
// HTML 代码 <div class="one two three" id="myDiv"></div>
var div = document.getElementById('myDiv');
div.className
// "one two three"
div.classList
// {
// 0: "one"
// 1: "two"
// 2: "three"
// length: 3
// }
上面代码中,className
属性返回一个空格分隔的字符串,而classList
属性指向一个类似数组的对象,该对象的length
属性(只读)返回当前元素的class
数量。
classList
对象有下列方法。
add()
:增加一个 class。remove()
:移除一个 class。contains()
:检查当前元素是否包含某个 class。toggle()
:将某个 class 移入或移出当前元素。item()
:返回指定索引位置的 class。toString()
:将 class 的列表转为字符串。
var div = document.getElementById('myDiv');
div.classList.add('myCssClass');
div.classList.add('foo', 'bar');
div.classList.remove('myCssClass');
div.classList.toggle('myCssClass'); // 如果 myCssClass 不存在就加入,否则移除
div.classList.contains('myCssClass'); // 返回 true 或者 false
div.classList.item(0); // 返回第一个 Class
div.classList.toString();
下面比较一下,className
和classList
在添加和删除某个 class 时的写法。
var foo = document.getElementById('foo');
// 添加class
foo.className += 'bold';
foo.classList.add('bold');
// 删除class
foo.classList.remove('bold');
foo.className = foo.className.replace(/^bold$/, '');
toggle
方法可以接受一个布尔值,作为第二个参数。如果为true
,则添加该属性;如果为false
,则去除该属性。
el.classList.toggle('abc', boolValue);
// 等同于
if (boolValue) {
el.classList.add('abc');
} else {
el.classList.remove('abc');
}
Element.dataset
网页元素可以自定义data-
属性,用来添加数据。
<div data-timestamp="1522907809292"></div>
上面代码中,<div>
元素有一个自定义的data-timestamp
属性,用来为该元素添加一个时间戳。
Element.dataset
属性返回一个对象,可以从这个对象读写data-
属性。
// <article
// id="foo"
// data-columns="3"
// data-index-number="12314"
// data-parent="cars">
// ...
// </article>
var article = document.getElementById('foo');
article.dataset.columns // "3"
article.dataset.indexNumber // "12314"
article.dataset.parent // "cars"
注意,dataset
上面的各个属性返回都是字符串。
HTML 代码中,data-
属性的属性名,只能包含英文字母、数字、连词线(-
)、点(.
)、冒号(:
)和下划线(_
)。它们转成 JavaScript 对应的dataset
属性名,规则如下。
- 开头的
data-
会省略。 - 如果连词线后面跟了一个英文字母,那么连词线会取消,该字母变成大写。
- 其他字符不变。
因此,data-abc-def
对应dataset.abcDef
,data-abc-1
对应dataset["abc-1"]
。
除了使用dataset
读写data-
属性,也可以使用Element.getAttribute()
和Element.setAttribute()
,通过完整的属性名读写这些属性。
var mydiv = document.getElementById('mydiv');
mydiv.dataset.foo = 'bar';
mydiv.getAttribute('data-foo') // "bar"
Element.innerHTML
Element.innerHTML
属性返回一个字符串,等同于该元素包含的所有 HTML 代码。该属性可读写,常用来设置某个节点的内容。它能改写所有元素节点的内容,包括<HTML>
和<body>
元素。
如果将innerHTML
属性设为空,等于删除所有它包含的所有节点。
el.innerHTML = '';
上面代码等于将el
节点变成了一个空节点,el
原来包含的节点被全部删除。
注意,读取属性值的时候,如果文本节点包含&
、小于号(<
)和大于号(>
),innerHTML
属性会将它们转为实体形式&
、<
、>
。如果想得到原文,建议使用element.textContent
属性。
// HTML代码如下 <p id="para"> 5 > 3 </p>
document.getElementById('para').innerHTML
// 5 > 3
写入的时候,如果插入的文本包含 HTML 标签,会被解析成为节点对象插入 DOM。注意,如果文本之中含有<script>
标签,虽然可以生成script
节点,但是插入的代码不会执行。
var name = "<script>alert('haha')</script>";
el.innerHTML = name;
上面代码将脚本插入内容,脚本并不会执行。但是,innerHTML
还是有安全风险的。
var name = "<img src=x οnerrοr=alert(1)>";
el.innerHTML = name;
上面代码中,alert
方法是会执行的。因此为了安全考虑,如果插入的是文本,最好用textContent
属性代替innerHTML
。
Element.outerHTML
Element.outerHTML
属性返回一个字符串,表示当前元素节点的所有 HTML 代码,包括该元素本身和所有子元素。
// HTML 代码如下
// <div id="d"><p>Hello</p></div>
var d = document.getElementById('d');
d.outerHTML
// '<div id="d"><p>Hello</p></div>'
outerHTML
属性是可读写的,对它进行赋值,等于替换掉当前元素。
// HTML 代码如下
// <div id="container"><div id="d">Hello</div></div>
var container = document.getElementById('container');
var d = document.getElementById('d');
container.firstChild.nodeName // "DIV"
d.nodeName // "DIV"
d.outerHTML = '<p>Hello</p>';
container.firstChild.nodeName // "P"
d.nodeName // "DIV"
上面代码中,变量d
代表子节点,它的outerHTML
属性重新赋值以后,内层的div
元素就不存在了,被p
元素替换了。但是,变量d
依然指向原来的div
元素,这表示被替换的DIV
元素还存在于内存中。
注意,如果一个节点没有父节点,设置outerHTML
属性会报错。
var div = document.createElement('div');
div.outerHTML = '<p>test</p>';
// DOMException: This element has no parent node.
上面代码中,div
元素没有父节点,设置outerHTML
属性会报错。
Element.clientHeight,Element.clientWidth
Element.clientHeight
属性返回一个整数值,表示元素节点的 CSS 高度(单位像素),只对块级元素生效,对于行内元素返回0
。如果块级元素没有设置 CSS 高度,则返回实际高度。
除了元素本身的高度,它还包括padding
部分,但是不包括border
、margin
。如果有水平滚动条,还要减去水平滚动条的高度。注意,这个值始终是整数,如果是小数会被四舍五入。
Element.clientWidth
属性返回元素节点的 CSS 宽度,同样只对块级元素有效,也是只包括元素本身的宽度和padding
,如果有垂直滚动条,还要减去垂直滚动条的宽度。
document.documentElement
的clientHeight
属性,返回当前视口的高度(即浏览器窗口的高度),等同于window.innerHeight
属性减去水平滚动条的高度(如果有的话)。document.body
的高度则是网页的实际高度。一般来说,document.body.clientHeight
大于document.documentElement.clientHeight
。
// 视口高度
document.documentElement.clientHeight
// 网页总高度
document.body.clientHeight
Element.clientLeft,Element.clientTop
Element.clientLeft
属性等于元素节点左边框(left border)的宽度(单位像素),不包括左侧的padding
和margin
。如果没有设置左边框,或者是行内元素(display: inline
),该属性返回0
。该属性总是返回整数值,如果是小数,会四舍五入。
Element.clientTop
属性等于网页元素顶部边框的宽度(单位像素),其他特点都与clientLeft
相同。
Element.scrollHeight,Element.scrollWidth
Element.scrollHeight
属性返回一个整数值(小数会四舍五入),表示当前元素的总高度(单位像素),包括溢出容器、当前不可见的部分。它包括padding
,但是不包括border
、margin
以及水平滚动条的高度(如果有水平滚动条的话),还包括伪元素(::before
或::after
)的高度。
Element.scrollWidth
属性表示当前元素的总宽度(单位像素),其他地方都与scrollHeight
属性类似。这两个属性只读。
整张网页的总高度可以从document.documentElement
或document.body
上读取。
// 返回网页的总高度
document.documentElement.scrollHeight
document.body.scrollHeight
注意,如果元素节点的内容出现溢出,即使溢出的内容是隐藏的,scrollHeight
属性仍然返回元素的总高度。
// HTML 代码如下
// <div id="myDiv" style="height: 200px; overflow: hidden;">...<div>
document.getElementById('myDiv').scrollHeight // 356
上面代码中,即使myDiv
元素的 CSS 高度只有200像素,且溢出部分不可见,但是scrollHeight
仍然会返回该元素的原始高度。
Element.scrollLeft,Element.scrollTop
Element.scrollLeft
属性表示当前元素的水平滚动条向右侧滚动的像素数量,Element.scrollTop
属性表示当前元素的垂直滚动条向下滚动的像素数量。对于那些没有滚动条的网页元素,这两个属性总是等于0。
如果要查看整张网页的水平的和垂直的滚动距离,要从document.documentElement
元素上读取。
document.documentElement.scrollLeft
document.documentElement.scrollTop
这两个属性都可读写,设置该属性的值,会导致浏览器将当前元素自动滚动到相应的位置。
Element.offsetParent
Element.offsetParent
属性返回最靠近当前元素的、并且 CSS 的position
属性不等于static
的上层元素。
<div style="position: absolute;">
<p>
<span>Hello</span>
</p>
</div>
上面代码中,span
元素的offsetParent
属性就是div
元素。
该属性主要用于确定子元素位置偏移的计算基准,Element.offsetTop
和Element.offsetLeft
就是offsetParent
元素计算的。
如果该元素是不可见的(display
属性为none
),或者位置是固定的(position
属性为fixed
),则offsetParent
属性返回null
。
<div style="position: absolute;">
<p>
<span style="display: none;">Hello</span>
</p>
</div>
上面代码中,span
元素的offsetParent
属性是null
。
如果某个元素的所有上层节点的position
属性都是static
,则Element.offsetParent
属性指向<body>
元素。
Element.offsetHeight,Element.offsetWidth
Element.offsetHeight
属性返回一个整数,表示元素的 CSS 垂直高度(单位像素),包括元素本身的高度、padding 和 border,以及水平滚动条的高度(如果存在滚动条)。
Element.offsetWidth
属性表示元素的 CSS 水平宽度(单位像素),其他都与Element.offsetHeight
一致。
这两个属性都是只读属性,只比Element.clientHeight
和Element.clientWidth
多了边框的高度或宽度。如果元素的 CSS 设为不可见(比如display: none;
),则返回0
。
Element.offsetLeft,Element.offsetTop
Element.offsetLeft
返回当前元素左上角相对于Element.offsetParent
节点的水平位移,Element.offsetTop
返回垂直位移,单位为像素。通常,这两个值是指相对于父节点的位移。
下面的代码可以算出元素左上角相对于整张网页的坐标。
function getElementPosition(e) {
var x = 0;
var y = 0;
while (e !== null) {
x += e.offsetLeft;
y += e.offsetTop;
e = e.offsetParent;
}
return {
x: x, y: y};
}
Element.style
每个元素节点都有style
用来读写该元素的行内样式信息,具体介绍参见《CSS 操作》一章。
Element.children,Element.childElementCount
Element.children
属性返回一个类似数组的对象(HTMLCollection
实例),包括当前元素节点的所有子元素。如果当前元素没有子元素,则返回的对象包含零个成员。
if (para.children.length) {
var children = para.children;
for (var i = 0; i < children.length; i++) {
// ...
}
}
上面代码遍历了para
元素的所有子元素。
这个属性与Node.childNodes
属性的区别是,它只包括元素类型的子节点,不包括其他类型的子节点。
Element.childElementCount
属性返回当前元素节点包含的子元素节点的个数,与Element.children.length
的值相同。
Element.firstElementChild,Element.lastElementChild
Element.firstElementChild
属性返回当前元素的第一个元素子节点,Element.lastElementChild
返回最后一个元素子节点。
如果没有元素子节点,这两个属性返回null
。
Element.nextElementSibling,Element.previousElementSibling
Element.nextElementSibling
属性返回当前元素节点的后一个同级元素节点,如果没有则返回null
。
// HTML 代码如下
// <div id="div-01">Here is div-01</div>
// <div id="div-02">Here is div-02</div>
var el = document.getElementById('div-01');
el.nextElementSibling
// <div id="div-02">Here is div-02</div>
Element.previousElementSibling
属性返回当前元素节点的前一个同级元素节点,如果没有则返回null
。
Node.prototype.textContent
textContent
属性返回当前节点和它的所有后代节点的文本内容。
// HTML 代码为
// <div id="divA">This is <span>some</span> text</div>
document.getElementById('divA').textContent
// This is some text
textContent
属性自动忽略当前节点内部的 HTML 标签,返回所有文本内容。
该属性是可读写的,设置该属性的值,会用一个新的文本节点,替换所有原来的子节点。它还有一个好处,就是自动对 HTML 标签转义。这很适合用于用户提供的内容。
document.getElementById('foo').textContent = '<p>GoodBye!</p>';
上面代码在插入文本时,会将<p>
标签解释为文本,而不会当作标签处理。
对于文本节点(text)、注释节点(comment)和属性节点(attr),textContent
属性的值与nodeValue
属性相同。对于其他类型的节点,该属性会将每个子节点(不包括注释节点)的内容连接在一起返回。如果一个节点没有子节点,则返回空字符串。
文档节点(document)和文档类型节点(doctype)的textContent
属性为null
。如果要读取整个文档的内容,可以使用document.documentElement.textContent
。
Node.prototype.parentNode
parentNode
属性返回当前节点的父节点。对于一个节点来说,它的父节点只可能是三种类型:元素节点(element)、文档节点(document)和文档片段节点(documentfragment)。
if (node.parentNode) {
node.parentNode.removeChild(node);
}
上面代码中,通过node.parentNode
属性将node
节点从文档里面移除。
文档节点(document)和文档片段节点(documentfragment)的父节点都是null
。另外,对于那些生成后还没插入 DOM 树的节点,父节点也是null
。
Node.prototype.firstChild,Node.prototype.lastChild
firstChild
属性返回当前节点的第一个子节点,如果当前节点没有子节点,则返回null
。
// HTML 代码如下
// <p id="p1"><span>First span</span></p>
var p1 = document.getElementById('p1');
p1.firstChild.nodeName // "SPAN"
上面代码中,p
元素的第一个子节点是span
元素。
注意,firstChild
返回的除了元素节点,还可能是文本节点或注释节点。
// HTML 代码如下
// <p id="p1">
// <span>First span</span>
// </p>
var p1 = document.getElementById('p1');
p1.firstChild.nodeName // "#text"
上面代码中,p
元素与span
元素之间有空白字符,这导致firstChild
返回的是文本节点。
lastChild
属性返回当前节点的最后一个子节点,如果当前节点没有子节点,则返回null
。用法与firstChild
属性相同。
Node.prototype.childNodes
childNodes
属性返回一个类似数组的对象(NodeList
集合),成员包括当前节点的所有子节点。
var children = document.querySelector('ul').childNodes;
上面代码中,children
就是ul
元素的所有子节点。
使用该属性,可以遍历某个节点的所有子节点。
var div = document.getElementById('div1');
var children = div.childNodes;
for (var i = 0; i < children.length; i++) {
// ...
}
文档节点(document)就有两个子节点:文档类型节点(docType)和 HTML 根元素节点。
var children = document.childNodes;
for (var i = 0; i < children.length; i++) {
console.log(children[i].nodeType);
}
// 10
// 1
上面代码中,文档节点的第一个子节点的类型是10(即文档类型节点),第二个子节点的类型是1(即元素节点)。
注意,除了元素节点,childNodes
属性的返回值还包括文本节点和注释节点。如果当前节点不包括任何子节点,则返回一个空的NodeList
集合。由于NodeList
对象是一个动态集合,一旦子节点发生变化,立刻会反映在返回结果之中
CORS
两种请求
CORS 请求分成两类:简单请求(simple request)和非简单请求(not-so-simple request)。
只要同时满足以下两大条件,就属于简单请求。
(1)请求方法是以下三种方法之一。
- HEAD
- GET
- POST
(2)HTTP 的头信息不超出以下几种字段。
- Accept
- Accept-Language
- Content-Language
- Last-Event-ID
- Content-Type:只限于三个值
application/x-www-form-urlencoded
、multipart/form-data
、text/plain
凡是不同时满足上面两个条件,就属于非简单请求。一句话,简单请求就是简单的 HTTP 方法与简单的 HTTP 头信息的结合。
这样划分的原因是,表单在历史上一直可以跨域发出请求。简单请求就是表单请求,浏览器沿袭了传统的处理方式,不把行为复杂化,否则开发者可能转而使用表单,规避 CORS 的限制。对于非简单请求,浏览器会采用新的处理方式。
简单请求
基本流程
对于简单请求,浏览器直接发出 CORS 请求。具体来说,就是在头信息之中,增加一个Origin
字段。
下面是一个例子,浏览器发现这次跨域 AJAX 请求是简单请求,就自动在头信息之中,添加一个Origin
字段。
GET /cors HTTP/1.1
Origin: http://api.bob.com
Host: api.alice.com
Accept-Language: en-US
Connection: keep-alive
User-Agent: Mozilla/5.0...
上面的头信息中,Origin
字段用来说明,本次请求来自哪个域(协议 + 域名 + 端口)。服务器根据这个值,决定是否同意这次请求。
如果Origin
指定的源,不在许可范围内,服务器会返回一个正常的 HTTP 回应。浏览器发现,这个回应的头信息没有包含Access-Control-Allow-Origin
字段(详见下文),就知道出错了,从而抛出一个错误,被XMLHttpRequest
的onerror
回调函数捕获。注意,这种错误无法通过状态码识别,因为 HTTP 回应的状态码有可能是200。
如果Origin
指定的域名在许可范围内,服务器返回的响应,会多出几个头信息字段。
Access-Control-Allow-Origin: http://api.bob.com
Access-Control-Allow-Credentials: true
Access-Control-Expose-Headers: FooBar
Content-Type: text/html; charset=utf-8
上面的头信息之中,有三个与 CORS 请求相关的字段,都以Access-Control-
开头。
(1)Access-Control-Allow-Origin
该字段是必须的。它的值要么是请求时Origin
字段的值,要么是一个*
,表示接受任意域名的请求。
(2)Access-Control-Allow-Credentials
该字段可选。它的值是一个布尔值,表示是否允许发送 Cookie。默认情况下,Cookie 不包括在 CORS 请求之中。设为true
,即表示服务器明确许可,浏览器可以把 Cookie 包含在请求中,一起发给服务器。这个值也只能设为true
,如果服务器不要浏览器发送 Cookie,不发送该字段即可。
(3)Access-Control-Expose-Headers
该字段可选。CORS 请求时,XMLHttpRequest
对象的getResponseHeader()
方法只能拿到6个服务器返回的基本字段:Cache-Control
、Content-Language
、Content-Type
、Expires
、Last-Modified
、Pragma
。如果想拿到其他字段,就必须在Access-Control-Expose-Headers
里面指定。上面的例子指定,getResponseHeader('FooBar')
可以返回FooBar
字段的值。
withCredentials 属性
上面说到,CORS 请求默认不包含 Cookie 信息(以及 HTTP 认证信息等),这是为了降低 CSRF 攻击的风险。但是某些场合,服务器可能需要拿到 Cookie,这时需要服务器显式指定Access-Control-Allow-Credentials
字段,告诉浏览器可以发送 Cookie。
Access-Control-Allow-Credentials: true
同时,开发者必须在 AJAX 请求中打开withCredentials
属性。
var xhr = new XMLHttpRequest();
xhr.withCredentials = true;
否则,即使服务器要求发送 Cookie,浏览器也不会发送。或者,服务器要求设置 Cookie,浏览器也不会处理。
但是,有的浏览器默认将withCredentials
属性设为true
。这导致如果省略withCredentials
设置,这些浏览器可能还是会一起发送 Cookie。这时,可以显式关闭withCredentials
。
xhr.withCredentials = false;
需要注意的是,如果服务器要求浏览器发送 Cookie,Access-Control-Allow-Origin
就不能设为星号,必须指定明确的、与请求网页一致的域名。同时,Cookie 依然遵循同源政策,只有用服务器域名设置的 Cookie 才会上传,其他域名的 Cookie 并不会上传,且(跨域)原网页代码中的document.cookie
也无法读取服务器域名下的 Cookie。
非简单请求
预检请求
非简单请求是那种对服务器提出特殊要求的请求,比如请求方法是PUT
或DELETE
,或者Content-Type
字段的类型是application/json
。
非简单请求的 CORS 请求,会在正式通信之前,增加一次 HTTP 查询请求,称为“预检”请求(preflight)。浏览器先询问服务器,当前网页所在的域名是否在服务器的许可名单之中,以及可以使用哪些 HTTP 方法和头信息字段。只有得到肯定答复,浏览器才会发出正式的XMLHttpRequest
请求,否则就报错。这是为了防止这些新增的请求,对传统的没有 CORS 支持的服务器形成压力,给服务器一个提前拒绝的机会,这样可以防止服务器收到大量DELETE
和PUT
请求,这些传统的表单不可能跨域发出的请求。
下面是一段浏览器的 JavaScript 脚本。
var url = 'http://api.alice.com/cors';
var xhr = new XMLHttpRequest();
xhr.open('PUT', url, true);
xhr.setRequestHeader('X-Custom-Header', 'value');
xhr.send();
上面代码中,HTTP 请求的方法是PUT
,并且发送一个自定义头信息X-Custom-Header
。
浏览器发现,这是一个非简单请求,就自动发出一个“预检”请求,要求服务器确认可以这样请求。下面是这个“预检”请求的 HTTP 头信息。
OPTIONS /cors HTTP/1.1
Origin: http://api.bob.com
Access-Control-Request-Method: PUT
Access-Control-Request-Headers: X-Custom-Header
Host: api.alice.com
Accept-Language: en-US
Connection: keep-alive
User-Agent: Mozilla/5.0...
“预检”请求用的请求方法是OPTIONS
,表示这个请求是用来询问的。头信息里面,关键字段是Origin
,表示请求来自哪个源。
除了Origin
字段,“预检”请求的头信息包括两个特殊字段。
(1)Access-Control-Request-Method
该字段是必须的,用来列出浏览器的 CORS 请求会用到哪些 HTTP 方法,上例是PUT
。
(2)Access-Control-Request-Headers
该字段是一个逗号分隔的字符串,指定浏览器 CORS 请求会额外发送的头信息字段,上例是X-Custom-Header
。
预检请求的回应
服务器收到“预检”请求以后,检查了Origin
、Access-Control-Request-Method
和Access-Control-Request-Headers
字段以后,确认允许跨源请求,就可以做出回应。
HTTP/1.1 200 OK
Date: Mon, 01 Dec 2008 01:15:39 GMT
Server: Apache/2.0.61 (Unix)
Access-Control-Allow-Origin: http://api.bob.com
Access-Control-Allow-Methods: GET, POST, PUT
Access-Control-Allow-Headers: X-Custom-Header
Content-Type: text/html; charset=utf-8
Content-Encoding: gzip
Content-Length: 0
Keep-Alive: timeout=2, max=100
Connection: Keep-Alive
Content-Type: text/plain
上面的 HTTP 回应中,关键的是Access-Control-Allow-Origin
字段,表示http://api.bob.com
可以请求数据。该字段也可以设为星号,表示同意任意跨源请求。
Access-Control-Allow-Origin: *
如果服务器否定了“预检”请求,会返回一个正常的 HTTP 回应,但是没有任何 CORS 相关的头信息字段,或者明确表示请求不符合条件。
OPTIONS http://api.bob.com HTTP/1.1
Status: 200
Access-Control-Allow-Origin: https://notyourdomain.com
Access-Control-Allow-Method: POST
上面的服务器回应,Access-Control-Allow-Origin
字段明确不包括发出请求的http://api.bob.com
。
这时,浏览器就会认定,服务器不同意预检请求,因此触发一个错误,被XMLHttpRequest
对象的onerror
回调函数捕获。控制台会打印出如下的报错信息。
XMLHttpRequest cannot load http://api.alice.com.
Origin http://api.bob.com is not allowed by Access-Control-Allow-Origin.
服务器回应的其他 CORS 相关字段如下。
Access-Control-Allow-Methods: GET, POST, PUT
Access-Control-Allow-Headers: X-Custom-Header
Access-Control-Allow-Credentials: true
Access-Control-Max-Age: 1728000
(1)Access-Control-Allow-Methods
该字段必需,它的值是逗号分隔的一个字符串,表明服务器支持的所有跨域请求的方法。注意,返回的是所有支持的方法,而不单是浏览器请求的那个方法。这是为了避免多次“预检”请求。
(2)Access-Control-Allow-Headers
如果浏览器请求包括Access-Control-Request-Headers
字段,则Access-Control-Allow-Headers
字段是必需的。它也是一个逗号分隔的字符串,表明服务器支持的所有头信息字段,不限于浏览器在“预检”中请求的字段。
(3)Access-Control-Allow-Credentials
该字段与简单请求时的含义相同。
(4)Access-Control-Max-Age
该字段可选,用来指定本次预检请求的有效期,单位为秒。上面结果中,有效期是20天(1728000秒),即允许缓存该条回应1728000秒(即20天),在此期间,不用发出另一条预检请求。
浏览器的正常请求和回应
一旦服务器通过了“预检”请求,以后每次浏览器正常的 CORS 请求,就都跟简单请求一样,会有一个Origin
头信息字段。服务器的回应,也都会有一个Access-Control-Allow-Origin
头信息字段。
下面是“预检”请求之后,浏览器的正常 CORS 请求。
PUT /cors HTTP/1.1
Origin: http://api.bob.com
Host: api.alice.com
X-Custom-Header: value
Accept-Language: en-US
Connection: keep-alive
User-Agent: Mozilla/5.0...
上面头信息的Origin
字段是浏览器自动添加的。
下面是服务器正常的回应。
Access-Control-Allow-Origin: http://api.bob.com
Content-Type: text/html; charset=utf-8
上面头信息中,Access-Control-Allow-Origin
字段是每次回应都必定包含的。
XMLHttpRequest
XMLHttpRequest.readyState
XMLHttpRequest.readyState
返回一个整数,表示实例对象的当前状态。该属性只读。它可能返回以下值。
- 0,表示 XMLHttpRequest 实例已经生成,但是实例的
open()
方法还没有被调用。 - 1,表示
open()
方法已经调用,但是实例的send()
方法还没有调用,仍然可以使用实例的setRequestHeader()
方法,设定 HTTP 请求的头信息。 - 2,表示实例的
send()
方法已经调用,并且服务器返回的头信息和状态码已经收到。 - 3,表示正在接收服务器传来的数据体(body 部分)。这时,如果实例的
responseType
属性等于text
或者空字符串,responseText
属性就会包含已经收到的部分信息。 - 4,表示服务器返回的数据已经完全接收,或者本次接收已经失败。
通信过程中,每当实例对象发生状态变化,它的readyState
属性的值就会改变。这个值每一次变化,都会触发readyStateChange
事件。
var xhr = new XMLHttpRequest();
if (xhr.readyState === 4) {
// 请求结束,处理服务器返回的数据
} else {
// 显示提示“加载中……”
}
上面代码中,xhr.readyState
等于4
时,表明脚本发出的 HTTP 请求已经完成。其他情况,都表示 HTTP 请求还在进行中。
XMLHttpRequest.onreadystatechange
XMLHttpRequest.onreadystatechange
属性指向一个监听函数。readystatechange
事件发生时(实例的readyState
属性变化),就会执行这个属性。
另外,如果使用实例的abort()
方法,终止 XMLHttpRequest 请求,也会造成readyState
属性变化,导致调用XMLHttpRequest.onreadystatechange
属性。
下面是一个例子。
var xhr = new XMLHttpRequest();
xhr.open( 'GET', 'http://example.com' , true );
xhr.onreadystatechange = function () {
if (xhr.readyState !== 4 || xhr.status !== 200) {
return;
}
console.log(xhr.responseText);
};
xhr.send();
XMLHttpRequest.response
XMLHttpRequest.response
属性表示服务器返回的数据体(即 HTTP 回应的 body 部分)。它可能是任何数据类型,比如字符串、对象、二进制对象等等,具体的类型由XMLHttpRequest.responseType
属性决定。该属性只读。
如果本次请求没有成功或者数据不完整,该属性等于null
。但是,如果responseType
属性等于text
或空字符串,在请求没有结束之前(readyState
等于3的阶段),response
属性包含服务器已经返回的部分数据。
var xhr = new XMLHttpRequest();
xhr.onreadystatechange = function () {
if (xhr.readyState === 4) {
handler(xhr.response);
}
}
XMLHttpRequest.responseType
XMLHttpRequest.responseType
属性是一个字符串,表示服务器返回数据的类型。这个属性是可写的,可以在调用open()
方法之后、调用send()
方法之前,设置这个属性的值,告诉浏览器如何解读返回的数据。如果responseType
设为空字符串,就等同于默认值text
。
XMLHttpRequest.responseType
属性可以等于以下值。
- “”(空字符串):等同于
text
,表示服务器返回文本数据。 - “arraybuffer”:ArrayBuffer 对象,表示服务器返回二进制数组。
- “blob”:Blob 对象,表示服务器返回二进制对象。
- “document”:Document 对象,表示服务器返回一个文档对象。
- “json”:JSON 对象。
- “text”:字符串。
上面几种类型之中,text
类型适合大多数情况,而且直接处理文本也比较方便。document
类型适合返回 HTML / XML 文档的情况,这意味着,对于那些打开 CORS 的网站,可以直接用 Ajax 抓取网页,然后不用解析 HTML 字符串,直接对抓取回来的数据进行 DOM 操作。blob
类型适合读取二进制数据,比如图片文件。
var xhr = new XMLHttpRequest();
xhr.open('GET', '/path/to/image.png', true);
xhr.responseType = 'blob';
xhr.onload = function(e) {
if (this.status === 200) {
var blob = new Blob([xhr.response], {
type: 'image/png'});
// 或者
var blob = xhr.response;
}
};
xhr.send();
如果将这个属性设为ArrayBuffer
,就可以按照数组的方式处理二进制数据。
var xhr = new XMLHttpRequest();
xhr.open('GET', '/path/to/image.png', true);
xhr.responseType = 'arraybuffer';
xhr.onload = function(e) {
var uInt8Array = new Uint8Array(this.response);
for (var i = 0, len = uInt8Array.length; i < len; ++i) {
// var byte = uInt8Array[i];
}
};
xhr.send();
如果将这个属性设为json
,浏览器就会自动对返回数据调用JSON.parse()
方法。也就是说,从xhr.response
属性(注意,不是xhr.responseText
属性)得到的不是文本,而是一个 JSON 对象。
XMLHttpRequest.responseText
XMLHttpRequest.responseText
属性返回从服务器接收到的字符串,该属性为只读。只有 HTTP 请求完成接收以后,该属性才会包含完整的数据。
var xhr = new XMLHttpRequest();
xhr.open('GET', '/server', true);
xhr.responseType = 'text';
xhr.onload = function () {
if (xhr.readyState === 4 && xhr.status === 200) {
console.log(xhr.responseText);
}
};
xhr.send(null);
XMLHttpRequest.responseXML
XMLHttpRequest.responseXML
属性返回从服务器接收到的 HTML 或 XML 文档对象,该属性为只读。如果本次请求没有成功,或者收到的数据不能被解析为 XML 或 HTML,该属性等于null
。
该属性生效的前提是 HTTP 回应的Content-Type
头信息等于text/xml
或application/xml
。这要求在发送请求前,XMLHttpRequest.responseType
属性要设为document
。如果 HTTP 回应的Content-Type
头信息不等于text/xml
和application/xml
,但是想从responseXML
拿到数据(即把数据按照 DOM 格式解析),那么需要手动调用XMLHttpRequest.overrideMimeType()
方法,强制进行 XML 解析。
该属性得到的数据,是直接解析后的文档 DOM 树。
var xhr = new XMLHttpRequest();
xhr.open('GET', '/server', true);
xhr.responseType = 'document';
xhr.overrideMimeType('text/xml');
xhr.onload = function () {
if (xhr.readyState === 4 && xhr.status === 200) {
console.log(xhr.responseXML);
}
};
xhr.send(null);
XMLHttpRequest.responseURL
XMLHttpRequest.responseURL
属性是字符串,表示发送数据的服务器的网址。
var xhr = new XMLHttpRequest();
xhr.open('GET', 'http://example.com/test', true);
xhr.onload = function () {
// 返回 http://example.com/test
console.log(xhr.responseURL);
};
xhr.send(null);
注意,这个属性的值与open()
方法指定的请求网址不一定相同。如果服务器端发生跳转,这个属性返回最后实际返回数据的网址。另外,如果原始 URL 包括锚点(fragment),该属性会把锚点剥离。
XMLHttpRequest.status,XMLHttpRequest.statusText
XMLHttpRequest.status
属性返回一个整数,表示服务器回应的 HTTP 状态码。一般来说,如果通信成功的话,这个状态码是200;如果服务器没有返回状态码,那么这个属性默认是200。请求发出之前,该属性为0
。该属性只读。
- 200, OK,访问正常
- 301, Moved Permanently,永久移动
- 302, Moved temporarily,暂时移动
- 304, Not Modified,未修改
- 307, Temporary Redirect,暂时重定向
- 401, Unauthorized,未授权
- 403, Forbidden,禁止访问
- 404, Not Found,未发现指定网址
- 500, Internal Server Error,服务器发生错误
基本上,只有2xx和304的状态码,表示服务器返回是正常状态。
if (xhr.readyState === 4) {
if ( (xhr.status >= 200 && xhr.status < 300)
|| (xhr.status === 304) ) {
// 处理服务器的返回数据
} else {
// 出错
}
}
XMLHttpRequest.statusText
属性返回一个字符串,表示服务器发送的状态提示。不同于status
属性,该属性包含整个状态信息,比如“OK”和“Not Found”。在请求发送之前(即调用open()
方法之前),该属性的值是空字符串;如果服务器没有返回状态提示,该属性的值默认为“OK”。该属性为只读属性。
XMLHttpRequest.timeout,XMLHttpRequestEventTarget.ontimeout
XMLHttpRequest.timeout
属性返回一个整数,表示多少毫秒后,如果请求仍然没有得到结果,就会自动终止。如果该属性等于0,就表示没有时间限制。
XMLHttpRequestEventTarget.ontimeout
属性用于设置一个监听函数,如果发生 timeout 事件,就会执行这个监听函数。
下面是一个例子。
var xhr = new XMLHttpRequest();
var url = '/server';
xhr.ontimeout = function () {
console.error('The request for ' + url + ' timed out.');
};
xhr.onload = function() {
if (xhr.readyState === 4) {
if (xhr.status === 200) {
// 处理服务器返回的数据
} else {
console.error(xhr.statusText);
}
}
};
xhr.open('GET', url, true);
// 指定 10 秒钟超时
xhr.timeout = 10 * 1000;
xhr.send(null);
事件监听属性
XMLHttpRequest 对象可以对以下事件指定监听函数。
- XMLHttpRequest.onloadstart:loadstart 事件(HTTP 请求发出)的监听函数
- XMLHttpRequest.onprogress:progress事件(正在发送和加载数据)的监听函数
- XMLHttpRequest.onabort:abort 事件(请求中止,比如用户调用了
abort()
方法)的监听函数 - XMLHttpRequest.onerror:error 事件(请求失败)的监听函数
- XMLHttpRequest.onload:load 事件(请求成功完成)的监听函数
- XMLHttpRequest.ontimeout:timeout 事件(用户指定的时限超过了,请求还未完成)的监听函数
- XMLHttpRequest.onloadend:loadend 事件(请求完成,不管成功或失败)的监听函数
下面是一个例子。
xhr.onload = function() {
var responseText = xhr.responseText;
console.log(responseText);
// process the response.
};
xhr.onabort = function () {
console.log('The request was aborted');
};
xhr.onprogress = function (event) {
console.log(event.loaded);
console.log(event.total);
};
xhr.onerror = function() {
console.log('There was an error!');
};
progress
事件的监听函数有一个事件对象参数,该对象有三个属性:loaded
属性返回已经传输的数据量,total
属性返回总的数据量,lengthComputable
属性返回一个布尔值,表示加载的进度是否可以计算。所有这些监听函数里面,只有progress
事件的监听函数有参数,其他函数都没有参数。
注意,如果发生网络错误(比如服务器无法连通),onerror
事件无法获取报错信息。也就是说,可能没有错误对象,所以这样只能显示报错的提示。
XMLHttpRequest.withCredentials
XMLHttpRequest.withCredentials
属性是一个布尔值,表示跨域请求时,用户信息(比如 Cookie 和认证的 HTTP 头信息)是否会包含在请求之中,默认为false
,即向example.com
发出跨域请求时,不会发送example.com
设置在本机上的 Cookie(如果有的话)。
如果需要跨域 AJAX 请求发送 Cookie,需要withCredentials
属性设为true
。注意,同源的请求不需要设置这个属性。
var xhr = new XMLHttpRequest();
xhr.open('GET', 'http://example.com/', true);
xhr.withCredentials = true;
xhr.send(null);
为了让这个属性生效,服务器必须显式返回Access-Control-Allow-Credentials
这个头信息。
Access-Control-Allow-Credentials: true
withCredentials
属性打开的话,跨域请求不仅会发送 Cookie,还会设置远程主机指定的 Cookie。反之也成立,如果withCredentials
属性没有打开,那么跨域的 AJAX 请求即使明确要求浏览器设置 Cookie,浏览器也会忽略。
注意,脚本总是遵守同源政策,无法从document.cookie
或者 HTTP 回应的头信息之中,读取跨域的 Cookie,withCredentials
属性不影响这一点。
XMLHttpRequest.upload
XMLHttpRequest 不仅可以发送请求,还可以发送文件,这就是 AJAX 文件上传。发送文件以后,通过XMLHttpRequest.upload
属性可以得到一个对象,通过观察这个对象,可以得知上传的进展。主要方法就是监听这个对象的各种事件:loadstart、loadend、load、abort、error、progress、timeout。
假定网页上有一个<progress>
元素。
<progress min="0" max="100" value="0">0% complete</progress>
文件上传时,对upload
属性指定progress
事件的监听函数,即可获得上传的进度。
function upload(blobOrFile) {
var xhr = new XMLHttpRequest();
xhr.open('POST', '/server', true);
xhr.onload = function (e) {
};
var progressBar = document.querySelector('progress');
xhr.upload.onprogress = function (e) {
if (e.lengthComputable) {
progressBar.value = (e.loaded / e.total) * 100;
// 兼容不支持 <progress> 元素的老式浏览器
progressBar.textContent = progressBar.value;
}
};
xhr.send(blobOrFile);
}
upload(new Blob(['hello world'], {
type: 'text/plain'}));
XMLHttpRequest 的实例方法
XMLHttpRequest.open()
XMLHttpRequest.open()
方法用于指定 HTTP 请求的参数,或者说初始化 XMLHttpRequest 实例对象。它一共可以接受五个参数。
void open(
string method,
string url,
optional boolean async,
optional string user,
optional string password
);
method
:表示 HTTP 动词方法,比如GET
、POST
、PUT
、DELETE
、HEAD
等。url
: 表示请求发送目标 URL。async
: 布尔值,表示请求是否为异步,默认为true
。如果设为false
,则send()
方法只有等到收到服务器返回了结果,才会进行下一步操作。该参数可选。由于同步 AJAX 请求会造成浏览器失去响应,许多浏览器已经禁止在主线程使用,只允许 Worker 里面使用。所以,这个参数轻易不应该设为false
。user
:表示用于认证的用户名,默认为空字符串。该参数可选。password
:表示用于认证的密码,默认为空字符串。该参数可选。
注意,如果对使用过open()
方法的 AJAX 请求,再次使用这个方法,等同于调用abort()
,即终止请求。
下面发送 POST 请求的例子。
var xhr = new XMLHttpRequest();
xhr.open('POST', encodeURI('someURL'));
XMLHttpRequest.send()
XMLHttpRequest.send()
方法用于实际发出 HTTP 请求。它的参数是可选的,如果不带参数,就表示 HTTP 请求只有一个 URL,没有数据体,典型例子就是 GET 请求;如果带有参数,就表示除了头信息,还带有包含具体数据的信息体,典型例子就是 POST 请求。
下面是 GET 请求的例子。
var xhr = new XMLHttpRequest();
xhr.open('GET',
'http://www.example.com/?id=' + encodeURIComponent(id),
true
);
xhr.send(null);
上面代码中,GET
请求的参数,作为查询字符串附加在 URL 后面。
下面是发送 POST 请求的例子。
var xhr = new XMLHttpRequest();
var data = 'email='
+ encodeURIComponent(email)
+ '&password='
+ encodeURIComponent(password);
xhr.open('POST', 'http://www.example.com', true);
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
xhr.send(data);
注意,所有 XMLHttpRequest 的监听事件,都必须在send()
方法调用之前设定。
send
方法的参数就是发送的数据。多种格式的数据,都可以作为它的参数。
void send();
void send(ArrayBufferView data);
void send(Blob data);
void send(Document data);
void send(String data);
void send(FormData data);
如果send()
发送 DOM 对象,在发送之前,数据会先被串行化。如果发送二进制数据,最好是发送ArrayBufferView
或Blob
对象,这使得通过 Ajax 上传文件成为可能。
下面是发送表单数据的例子。FormData
对象可以用于构造表单数据。
var formData = new FormData();
formData.append('username', '张三');
formData.append('email', '[email protected]');
formData.append('birthDate', 1940);
var xhr = new XMLHttpRequest();
xhr.open('POST', '/register');
xhr.send(formData);
上面代码中,FormData
对象构造了表单数据,然后使用send()
方法发送。它的效果与发送下面的表单数据是一样的。
<form id='registration' name='registration' action='/register'>
<input type='text' name='username' value='张三'>
<input type='email' name='email' value='[email protected]'>
<input type='number' name='birthDate' value='1940'>
<input type='submit' onclick='return sendForm(this.form);'>
</form>
下面的例子是使用FormData
对象加工表单数据,然后再发送。
function sendForm(form) {
var formData = new FormData(form);
formData.append('csrf', 'e69a18d7db1286040586e6da1950128c');
var xhr = new XMLHttpRequest();
xhr.open('POST', form.action, true);
xhr.onload = function() {
// ...
};
xhr.send(formData);
return false;
}
var form = document.querySelector('#registration');
sendForm(form);
XMLHttpRequest.setRequestHeader()
XMLHttpRequest.setRequestHeader()
方法用于设置浏览器发送的 HTTP 请求的头信息。该方法必须在open()
之后、send()
之前调用。如果该方法多次调用,设定同一个字段,则每一次调用的值会被合并成一个单一的值发送。
该方法接受两个参数。第一个参数是字符串,表示头信息的字段名,第二个参数是字段值。
xhr.setRequestHeader('Content-Type', 'application/json');
xhr.setRequestHeader('Content-Length', JSON.stringify(data).length);
xhr.send(JSON.stringify(data));
上面代码首先设置头信息Content-Type
,表示发送 JSON 格式的数据;然后设置Content-Length
,表示数据长度;最后发送 JSON 数据。
XMLHttpRequest.overrideMimeType()
XMLHttpRequest.overrideMimeType()
方法用来指定 MIME 类型,覆盖服务器返回的真正的 MIME 类型,从而让浏览器进行不一样的处理。举例来说,服务器返回的数据类型是text/xml
,由于种种原因浏览器解析不成功报错,这时就拿不到数据了。为了拿到原始数据,我们可以把 MIME 类型改成text/plain
,这样浏览器就不会去自动解析,从而我们就可以拿到原始文本了。
xhr.overrideMimeType('text/plain')
注意,该方法必须在send()
方法之前调用。
修改服务器返回的数据类型,不是正常情况下应该采取的方法。如果希望服务器返回指定的数据类型,可以用responseType
属性告诉服务器,就像下面的例子。只有在服务器无法返回某种数据类型时,才使用overrideMimeType()
方法。
var xhr = new XMLHttpRequest();
xhr.onload = function(e) {
var arraybuffer = xhr.response;
// ...
}
xhr.open('GET', url);
xhr.responseType = 'arraybuffer';
xhr.send();
XMLHttpRequest.getResponseHeader()
XMLHttpRequest.getResponseHeader()
方法返回 HTTP 头信息指定字段的值,如果还没有收到服务器回应或者指定字段不存在,返回null
。该方法的参数不区分大小写。
function getHeaderTime() {
console.log(this.getResponseHeader("Last-Modified"));
}
var xhr = new XMLHttpRequest();
xhr.open('HEAD', 'yourpage.html');
xhr.onload = getHeaderTime;
xhr.send();
如果有多个字段同名,它们的值会被连接为一个字符串,每个字段之间使用“逗号+空格”分隔。
XMLHttpRequest.getAllResponseHeaders()
XMLHttpRequest.getAllResponseHeaders()
方法返回一个字符串,表示服务器发来的所有 HTTP 头信息。格式为字符串,每个头信息之间使用CRLF
分隔(回车+换行),如果没有收到服务器回应,该属性为null
。如果发生网络错误,该属性为空字符串。
var xhr = new XMLHttpRequest();
xhr.open('GET', 'foo.txt', true);
xhr.send();
xhr.onreadystatechange = function () {
if (this.readyState === 4) {
var headers = xhr.getAllResponseHeaders();
}
}
上面代码用于获取服务器返回的所有头信息。它可能是下面这样的字符串。
date: Fri, 08 Dec 2017 21:04:30 GMT\r\n
content-encoding: gzip\r\n
x-content-type-options: nosniff\r\n
server: meinheld/0.6.1\r\n
x-frame-options: DENY\r\n
content-type: text/html; charset=utf-8\r\n
connection: keep-alive\r\n
strict-transport-security: max-age=63072000\r\n
vary: Cookie, Accept-Encoding\r\n
content-length: 6502\r\n
x-xss-protection: 1; mode=block\r\n
然后,对这个字符串进行处理。
var arr = headers.trim().split(/[\r\n]+/);
var headerMap = {
};
arr.forEach(function (line) {
var parts = line.split(': ');
var header = parts.shift();
var value = parts.join(': ');
headerMap[header] = value;
});
headerMap['content-length'] // "6502"
XMLHttpRequest.abort()
XMLHttpRequest.abort()
方法用来终止已经发出的 HTTP 请求。调用这个方法以后,readyState
属性变为4
,status
属性变为0
。
var xhr = new XMLHttpRequest();
xhr.open('GET', 'http://www.example.com/page.php', true);
setTimeout(function () {
if (xhr) {
xhr.abort();
xhr = null;
}
}, 5000);
上面代码在发出5秒之后,终止一个 AJAX 请求。
XMLHttpRequest 实例的事件
readyStateChange 事件
readyState
属性的值发生改变,就会触发 readyStateChange 事件。
我们可以通过onReadyStateChange
属性,指定这个事件的监听函数,对不同状态进行不同处理。尤其是当状态变为4
的时候,表示通信成功,这时回调函数就可以处理服务器传送回来的数据。
progress 事件
上传文件时,XMLHttpRequest 实例对象本身和实例的upload
属性,都有一个progress
事件,会不断返回上传的进度。
var xhr = new XMLHttpRequest();
function updateProgress (oEvent) {
if (oEvent.lengthComputable) {
var percentComplete = oEvent.loaded / oEvent.total;
} else {
console.log('无法计算进展');
}
}
xhr.addEventListener('progress', updateProgress);
xhr.open();
load 事件、error 事件、abort 事件
load 事件表示服务器传来的数据接收完毕,error 事件表示请求出错,abort 事件表示请求被中断(比如用户取消请求)。
var xhr = new XMLHttpRequest();
xhr.addEventListener('load', transferComplete);
xhr.addEventListener('error', transferFailed);
xhr.addEventListener('abort', transferCanceled);
xhr.open();
function transferComplete() {
console.log('数据接收完毕');
}
function transferFailed() {
console.log('数据接收出错');
}
function transferCanceled() {
console.log('用户取消接收');
}
loadend 事件
abort
、load
和error
这三个事件,会伴随一个loadend
事件,表示请求结束,但不知道其是否成功。
xhr.addEventListener('loadend', loadEnd);
function loadEnd(e) {
console.log('请求结束,状态未知');
}
timeout 事件
服务器超过指定时间还没有返回结果,就会触发 timeout 事件,具体的例子参见timeout
属性一节。
Navigator.sendBeacon()
用户卸载网页的时候,有时需要向服务器发一些数据。很自然的做法是在unload
事件或beforeunload
事件的监听函数里面,使用XMLHttpRequest
对象发送数据。但是,这样做不是很可靠,因为XMLHttpRequest
对象是异步发送,很可能在它即将发送的时候,页面已经卸载了,从而导致发送取消或者发送失败。
解决方法就是unload
事件里面,加一些很耗时的同步操作。这样就能留出足够的时间,保证异步 AJAX 能够发送成功。
function log() {
let xhr = new XMLHttpRequest();
xhr.open('post', '/log', true);
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
xhr.send('foo=bar');
}
window.addEventListener('unload', function(event) {
log();
// a time-consuming operation
for (let i = 1; i < 10000; i++) {
for (let m = 1; m < 10000; m++) {
continue; }
}
});
上面代码中,强制执行了一次双重循环,拖长了unload
事件的执行时间,导致异步 AJAX 能够发送成功。
类似的还可以使用setTimeout
。下面是追踪用户点击的例子。
// HTML 代码如下
// <a id="target" href="https://baidu.com">click</a>
const clickTime = 350;
const theLink = document.getElementById('target');
function log() {
let xhr = new XMLHttpRequest();
xhr.open('post', '/log', true);
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
xhr.send('foo=bar');
}
theLink.addEventListener('click', function (event) {
event.preventDefault();
log();
setTimeout(function () {
window.location.href = theLink.getAttribute('href');
}, clickTime);
});
上面代码使用setTimeout
,拖延了350毫秒,才让页面跳转,因此使得异步 AJAX 有时间发出。
这些做法的共同问题是,卸载的时间被硬生生拖长了,后面页面的加载被推迟了,用户体验不好。
为了解决这个问题,浏览器引入了Navigator.sendBeacon()
方法。这个方法还是异步发出请求,但是请求与当前页面线程脱钩,作为浏览器进程的任务,因此可以保证会把数据发出去,不拖延卸载流程。
window.addEventListener('unload', logData, false);
function logData() {
navigator.sendBeacon('/log', analyticsData);
}
Navigator.sendBeacon
方法接受两个参数,第一个参数是目标服务器的 URL,第二个参数是所要发送的数据(可选),可以是任意类型(字符串、表单对象、二进制对象等等)。
navigator.sendBeacon(url, data)
这个方法的返回值是一个布尔值,成功发送数据为true
,否则为false
。
该方法发送数据的 HTTP 方法是 POST,可以跨域,类似于表单提交数据。它不能指定回调函数。