window 对象
概述
浏览器里面,window
对象(注意,w
为小写)指当前的浏览器窗口。它也是当前页面的顶层对象,即最高一层的对象,所有其他对象都是它的下属。一个变量如果未声明,那么默认就是顶层对象的属性。
a = 1;
window.a // 1
上面代码中,a
是一个没有声明就直接赋值的变量,它自动成为顶层对象的属性。
window
有自己的实体含义,其实不适合当作最高一层的顶层对象,这是一个语言的设计失误。最早,设计这门语言的时候,原始设想是语言内置的对象越少越好,这样可以提高浏览器的性能。因此,语言设计者 Brendan Eich 就把window
对象当作顶层对象,所有未声明就赋值的变量都自动变成window
对象的属性。这种设计使得编译阶段无法检测出未声明变量,但到了今天已经没有办法纠正了。
window 对象的属性
window.name
window.name
属性是一个字符串,表示当前浏览器窗口的名字。窗口不一定需要名字,这个属性主要配合超链接和表单的target
属性使用。
window.name = 'Hello World!';
console.log(window.name)
// "Hello World!"
该属性只能保存字符串,如果写入的值不是字符串,会自动转成字符串。各个浏览器对这个值的储存容量有所不同,但是一般来说,可以高达几MB。
只要浏览器窗口不关闭,这个属性是不会消失的。举例来说,访问a.com
时,该页面的脚本设置了window.name
,接下来在同一个窗口里面载入了b.com
,新页面的脚本可以读到上一个网页设置的window.name
。页面刷新也是这种情况。一旦浏览器窗口关闭后,该属性保存的值就会消失,因为这时窗口已经不存在了。
window.closed,window.opener
window.closed
属性返回一个布尔值,表示窗口是否关闭。
window.closed // false
上面代码检查当前窗口是否关闭。这种检查意义不大,因为只要能运行代码,当前窗口肯定没有关闭。这个属性一般用来检查,使用脚本打开的新窗口是否关闭。
var popup = window.open();
if ((popup !== null) && !popup.closed) {
// 窗口仍然打开着
}
window.opener
属性表示打开当前窗口的父窗口。如果当前窗口没有父窗口(即直接在地址栏输入打开),则返回null
。
window.open().opener === window // true
上面表达式会打开一个新窗口,然后返回true
。
如果两个窗口之间不需要通信,建议将子窗口的opener
属性显式设为null
,这样可以减少一些安全隐患。
var newWin = window.open('example.html', 'newWindow', 'height=400,width=400');
newWin.opener = null;
上面代码中,子窗口的opener
属性设为null
,两个窗口之间就没办法再联系了。
通过opener
属性,可以获得父窗口的全局属性和方法,但只限于两个窗口同源的情况(参见《同源限制》一章),且其中一个窗口由另一个打开。<a>
元素添加rel="noopener"
属性,可以防止新打开的窗口获取父窗口,减轻被恶意网站修改父窗口 URL 的风险。
<a href="https://an.evil.site" target="_blank" rel="noopener">
恶意网站
</a>
window.self,window.window
window.self
和window.window
属性都指向窗口本身。这两个属性只读。
window.self === window // true
window.window === window // true
window.frames,window.length
window.frames
属性返回一个类似数组的对象,成员为页面内所有框架窗口,包括frame
元素和iframe
元素。window.frames[0]
表示页面中第一个框架窗口。
如果iframe
元素设置了id
或name
属性,那么就可以用属性值,引用这个iframe
窗口。比如<iframe name="myIFrame">
可以用frames['myIFrame']
或者frames.myIFrame
来引用。
frames
属性实际上是window
对象的别名。
frames === window // true
因此,frames[0]
也可以用window[0]
表示。但是,从语义上看,frames
更清晰,而且考虑到window
还是全局对象,因此推荐表示多窗口时,总是使用frames[0]
的写法。更多介绍请看下文的《多窗口操作》部分。
window.length
属性返回当前网页包含的框架总数。如果当前网页不包含frame
和iframe
元素,那么window.length
就返回0
。
window.frames.length === window.length // true
上面代码表示,window.frames.length
与window.length
应该是相等的。
window.frameElement
window.frameElement
属性主要用于当前窗口嵌在另一个网页的情况(嵌入<object>
、<iframe>
或<embed>
元素),返回当前窗口所在的那个元素节点。如果当前窗口是顶层窗口,或者所嵌入的那个网页不是同源的,该属性返回null
。
// HTML 代码如下
// <iframe src="about.html"></iframe>
// 下面的脚本在 about.html 里面
var frameEl = window.frameElement;
if (frameEl) {
frameEl.src = 'other.html';
}
上面代码中,frameEl
变量就是<iframe>
元素。
window.top,window.parent
window.top
属性指向最顶层窗口,主要用于在框架窗口(frame)里面获取顶层窗口。
window.parent
属性指向父窗口。如果当前窗口没有父窗口,window.parent
指向自身。
if (window.parent !== window.top) {
// 表明当前窗口嵌入不止一层
}
对于不包含框架的网页,这两个属性等同于window
对象。
window.status
window.status
属性用于读写浏览器状态栏的文本。但是,现在很多浏览器都不允许改写状态栏文本,所以使用这个方法不一定有效。
window.devicePixelRatio
window.devicePixelRatio
属性返回一个数值,表示一个 CSS 像素的大小与一个物理像素的大小之间的比率。也就是说,它表示一个 CSS 像素由多少个物理像素组成。它可以用于判断用户的显示环境,如果这个比率较大,就表示用户正在使用高清屏幕,因此可以显示较大像素的图片。
位置大小属性
以下属性返回window
对象的位置信息和大小信息。
(1)window.screenX,window.screenY
window.screenX
和window.screenY
属性,返回浏览器窗口左上角相对于当前屏幕左上角的水平距离和垂直距离(单位像素)。这两个属性只读。
(2) window.innerHeight,window.innerWidth
window.innerHeight
和window.innerWidth
属性,返回网页在当前窗口中可见部分的高度和宽度,即“视口”(viewport)的大小(单位像素)。这两个属性只读。
用户放大网页的时候(比如将网页从100%的大小放大为200%),这两个属性会变小。因为这时网页的像素大小不变(比如宽度还是960像素),只是每个像素占据的屏幕空间变大了,因此可见部分(视口)就变小了。
注意,这两个属性值包括滚动条的高度和宽度。
(3)window.outerHeight,window.outerWidth
window.outerHeight
和window.outerWidth
属性返回浏览器窗口的高度和宽度,包括浏览器菜单和边框(单位像素)。这两个属性只读。
(4)window.scrollX,window.scrollY
window.scrollX
属性返回页面的水平滚动距离,window.scrollY
属性返回页面的垂直滚动距离,单位都为像素。这两个属性只读。
注意,这两个属性的返回值不是整数,而是双精度浮点数。如果页面没有滚动,它们的值就是0
。
举例来说,如果用户向下拉动了垂直滚动条75像素,那么window.scrollY
就是75左右。用户水平向右拉动水平滚动条200像素,window.scrollX
就是200左右。
if (window.scrollY < 75) {
window.scroll(0, 75);
}
上面代码中,如果页面向下滚动的距离小于75像素,那么页面向下滚动75像素。
(5)window.pageXOffset,window.pageYOffset
window.pageXOffset
属性和window.pageYOffset
属性,是window.scrollX
和window.scrollY
别名。
组件属性
组件属性返回浏览器的组件对象。这样的属性有下面几个。
window.locationbar
:地址栏对象window.menubar
:菜单栏对象window.scrollbars
:窗口的滚动条对象window.toolbar
:工具栏对象window.statusbar
:状态栏对象window.personalbar
:用户安装的个人工具栏对象
这些对象的visible
属性是一个布尔值,表示这些组件是否可见。这些属性只读。
window.locationbar.visible
window.menubar.visible
window.scrollbars.visible
window.toolbar.visible
window.statusbar.visible
window.personalbar.visible
全局对象属性
全局对象属性指向一些浏览器原生的全局对象。
window.document
:指向document
对象,详见《document 对象》一章。注意,这个属性有同源限制。只有来自同源的脚本才能读取这个属性。window.location
:指向Location
对象,用于获取当前窗口的 URL 信息。它等同于document.location
属性,详见《Location 对象》一章。window.navigator
:指向Navigator
对象,用于获取环境信息,详见《Navigator 对象》一章。window.history
:指向History
对象,表示浏览器的浏览历史,详见《History 对象》一章。window.localStorage
:指向本地储存的 localStorage 数据,详见《Storage 接口》一章。window.sessionStorage
:指向本地储存的 sessionStorage 数据,详见《Storage 接口》一章。window.console
:指向console
对象,用于操作控制台,详见《console 对象》一章。window.screen
:指向Screen
对象,表示屏幕信息,详见《Screen 对象》一章。
window.isSecureContext
window.isSecureContext
属性返回一个布尔值,表示当前窗口是否处在加密环境。如果是 HTTPS 协议,就是true
,否则就是false
。
window 对象的方法
window.alert(),window.prompt(),window.confirm()
window.alert()
、window.prompt()
、window.confirm()
都是浏览器与用户互动的全局方法。它们会弹出不同的对话框,要求用户做出回应。注意,这三个方法弹出的对话框,都是浏览器统一规定的式样,无法定制。
(1)window.alert()
window.alert()
方法弹出的对话框,只有一个“确定”按钮,往往用来通知用户某些信息。
window.alert('Hello World');
用户只有点击“确定”按钮,对话框才会消失。对话框弹出期间,浏览器窗口处于冻结状态,如果不点“确定”按钮,用户什么也干不了。
window.alert()
方法的参数只能是字符串,没法使用 CSS 样式,但是可以用\n
指定换行。
alert('本条提示\n分成两行');
(2)window.prompt()
window.prompt()
方法弹出的对话框,提示文字的下方,还有一个输入框,要求用户输入信息,并有“确定”和“取消”两个按钮。它往往用来获取用户输入的数据。
var result = prompt('您的年龄?', 25)
上面代码会跳出一个对话框,文字提示为“您的年龄?”,要求用户在对话框中输入自己的年龄(默认显示25)。用户填入的值,会作为返回值存入变量result
。
window.prompt()
的返回值有两种情况,可能是字符串(有可能是空字符串),也有可能是null
。具体分成三种情况。
- 用户输入信息,并点击“确定”,则用户输入的信息就是返回值。
- 用户没有输入信息,直接点击“确定”,则输入框的默认值就是返回值。
- 用户点击了“取消”(或者按了 ESC 按钮),则返回值是
null
。
window.prompt()
方法的第二个参数是可选的,但是最好总是提供第二个参数,作为输入框的默认值。
(3)window.confirm()
window.confirm()
方法弹出的对话框,除了提示信息之外,只有“确定”和“取消”两个按钮,往往用来征询用户是否同意。
var result = confirm('你最近好吗?');
上面代码弹出一个对话框,上面只有一行文字“你最近好吗?”,用户选择点击“确定”或“取消”。
confirm
方法返回一个布尔值,如果用户点击“确定”,返回true
;如果用户点击“取消”,则返回false
。
var okay = confirm('Please confirm this message.');
if (okay) {
// 用户按下“确定”
} else {
// 用户按下“取消”
}
confirm
的一个用途是,用户离开当前页面时,弹出一个对话框,问用户是否真的要离开。
window.onunload = function () {
return window.confirm('你确定要离开当面页面吗?');
}
这三个方法都具有堵塞效应,一旦弹出对话框,整个页面就是暂停执行,等待用户做出反应。
window.open(), window.close(),window.stop()
(1)window.open()
window.open
方法用于新建另一个浏览器窗口,类似于浏览器菜单的新建窗口选项。它会返回新窗口的引用,如果无法新建窗口,则返回null
。
var popup = window.open('somefile.html');
上面代码会让浏览器弹出一个新建窗口,网址是当前域名下的somefile.html
。
open
方法一共可以接受三个参数。
window.open(url, windowName, [windowFeatures])
url
:字符串,表示新窗口的网址。如果省略,默认网址就是about:blank
。windowName
:字符串,表示新窗口的名字。如果该名字的窗口已经存在,则占用该窗口,不再新建窗口。如果省略,就默认使用_blank
,表示新建一个没有名字的窗口。另外还有几个预设值,_self
表示当前窗口,_top
表示顶层窗口,_parent
表示上一层窗口。windowFeatures
:字符串,内容为逗号分隔的键值对(详见下文),表示新窗口的参数,比如有没有提示栏、工具条等等。如果省略,则默认打开一个完整 UI 的新窗口。如果新建的是一个已经存在的窗口,则该参数不起作用,浏览器沿用以前窗口的参数。
下面是一个例子。
var popup = window.open(
'somepage.html',
'DefinitionsWindows',
'height=200,width=200,location=no,status=yes,resizable=yes,scrollbars=yes'
);
上面代码表示,打开的新窗口高度和宽度都为200像素,没有地址栏,但有状态栏和滚动条,允许用户调整大小。
第三个参数可以设定如下属性。
- left:新窗口距离屏幕最左边的距离(单位像素)。注意,新窗口必须是可见的,不能设置在屏幕以外的位置。
- top:新窗口距离屏幕最顶部的距离(单位像素)。
- height:新窗口内容区域的高度(单位像素),不得小于100。
- width:新窗口内容区域的宽度(单位像素),不得小于100。
- outerHeight:整个浏览器窗口的高度(单位像素),不得小于100。
- outerWidth:整个浏览器窗口的宽度(单位像素),不得小于100。
- menubar:是否显示菜单栏。
- toolbar:是否显示工具栏。
- location:是否显示地址栏。
- personalbar:是否显示用户自己安装的工具栏。
- status:是否显示状态栏。
- dependent:是否依赖父窗口。如果依赖,那么父窗口最小化,该窗口也最小化;父窗口关闭,该窗口也关闭。
- minimizable:是否有最小化按钮,前提是
dialog=yes
。 - noopener:新窗口将与父窗口切断联系,即新窗口的
window.opener
属性返回null
,父窗口的window.open()
方法也返回null
。 - resizable:新窗口是否可以调节大小。
- scrollbars:是否允许新窗口出现滚动条。
- dialog:新窗口标题栏是否出现最大化、最小化、恢复原始大小的控件。
- titlebar:新窗口是否显示标题栏。
- alwaysRaised:是否显示在所有窗口的顶部。
- alwaysLowered:是否显示在父窗口的底下。
- close:新窗口是否显示关闭按钮。
对于那些可以打开和关闭的属性,设为yes
或1
或不设任何值就表示打开,比如status=yes
、status=1
、status
都会得到同样的结果。如果想设为关闭,不用写no
,而是直接省略这个属性即可。也就是说,如果在第三个参数中设置了一部分属性,其他没有被设置的yes/no
属性都会被设成no
,只有titlebar
和关闭按钮除外(它们的值默认为yes
)。
上面这些属性,属性名与属性值之间用等号连接,属性与属性之间用逗号分隔。
'height=200,width=200,location=no,status=yes,resizable=yes,scrollbars=yes'
另外,open()
方法的第二个参数虽然可以指定已经存在的窗口,但是不等于可以任意控制其他窗口。为了防止被不相干的窗口控制,浏览器只有在两个窗口同源,或者目标窗口被当前网页打开的情况下,才允许open
方法指向该窗口。
window.open
方法返回新窗口的引用。
var windowB = window.open('windowB.html', 'WindowB');
windowB.window.name // "WindowB"
注意,如果新窗口和父窗口不是同源的(即不在同一个域),它们彼此不能获取对方窗口对象的内部属性。
下面是另一个例子。
var w = window.open();
console.log('已经打开新窗口');
w.location = 'http://example.com';
上面代码先打开一个新窗口,然后在该窗口弹出一个对话框,再将网址导向example.com
。
由于open
这个方法很容易被滥用,许多浏览器默认都不允许脚本自动新建窗口。只允许在用户点击链接或按钮时,脚本做出反应,弹出新窗口。因此,有必要检查一下打开新窗口是否成功。
var popup = window.open();
if (popup === null) {
// 新建窗口失败
}
(2)window.close()
window.close
方法用于关闭当前窗口,一般只用来关闭window.open
方法新建的窗口。
popup.close()
该方法只对顶层窗口有效,iframe
框架之中的窗口使用该方法无效。
(3)window.stop()
window.stop()
方法完全等同于单击浏览器的停止按钮,会停止加载图像、视频等正在或等待加载的对象。
window.stop()
window.moveTo(),window.moveBy()
window.moveTo()
方法用于移动浏览器窗口到指定位置。它接受两个参数,分别是窗口左上角距离屏幕左上角的水平距离和垂直距离,单位为像素。
window.moveTo(100, 200)
上面代码将窗口移动到屏幕(100, 200)
的位置。
window.moveBy()
方法将窗口移动到一个相对位置。它接受两个参数,分别是窗口左上角向右移动的水平距离和向下移动的垂直距离,单位为像素。
window.moveBy(25, 50)
上面代码将窗口向右移动25像素、向下移动50像素。
为了防止有人滥用这两个方法,随意移动用户的窗口,目前只有一种情况,浏览器允许用脚本移动窗口:该窗口是用window.open()
方法新建的,并且窗口里只有它一个 Tab 页。除此以外的情况,使用上面两个方法都是无效的。
window.resizeTo(),window.resizeBy()
window.resizeTo()
方法用于缩放窗口到指定大小。
它接受两个参数,第一个是缩放后的窗口宽度(outerWidth
属性,包含滚动条、标题栏等等),第二个是缩放后的窗口高度(outerHeight
属性)。
window.resizeTo(
window.screen.availWidth / 2,
window.screen.availHeight / 2
)
上面代码将当前窗口缩放到,屏幕可用区域的一半宽度和高度。
window.resizeBy()
方法用于缩放窗口。它与window.resizeTo()
的区别是,它按照相对的量缩放,window.resizeTo()
需要给出缩放后的绝对大小。
它接受两个参数,第一个是水平缩放的量,第二个是垂直缩放的量,单位都是像素。
window.resizeBy(-200, -200)
上面的代码将当前窗口的宽度和高度,都缩小200像素。
window.scrollTo(),window.scroll(),window.scrollBy()
window.scrollTo
方法用于将文档滚动到指定位置。它接受两个参数,表示滚动后位于窗口左上角的页面坐标。
window.scrollTo(x-coord, y-coord)
它也可以接受一个配置对象作为参数。
window.scrollTo(options)
配置对象options
有三个属性。
top
:滚动后页面左上角的垂直坐标,即 y 坐标。left
:滚动后页面左上角的水平坐标,即 x 坐标。behavior
:字符串,表示滚动的方式,有三个可能值(smooth
、instant
、auto
),默认值为auto
。
window.scrollTo({
top: 1000,
behavior: 'smooth'
});
window.scroll()
方法是window.scrollTo()
方法的别名。
window.scrollBy()
方法用于将网页滚动指定距离(单位像素)。它接受两个参数:水平向右滚动的像素,垂直向下滚动的像素。
window.scrollBy(0, window.innerHeight)
上面代码用于将网页向下滚动一屏。
如果不是要滚动整个文档,而是要滚动某个元素,可以使用下面三个属性和方法。
- Element.scrollTop
- Element.scrollLeft
- Element.scrollIntoView()
window.print()
window.print
方法会跳出打印对话框,与用户点击菜单里面的“打印”命令效果相同。
常见的打印按钮代码如下。
document.getElementById('printLink').onclick = function () {
window.print();
}
非桌面设备(比如手机)可能没有打印功能,这时可以这样判断。
if (typeof window.print === 'function') {
// 支持打印功能
}
window.focus(),window.blur()
window.focus()
方法会激活窗口,使其获得焦点,出现在其他窗口的前面。
var popup = window.open('popup.html', 'Popup Window');
if ((popup !== null) && !popup.closed) {
popup.focus();
}
上面代码先检查popup
窗口是否依然存在,确认后激活该窗口。
window.blur()
方法将焦点从窗口移除。
当前窗口获得焦点时,会触发focus
事件;当前窗口失去焦点时,会触发blur
事件。
window.getSelection()
window.getSelection
方法返回一个Selection
对象,表示用户现在选中的文本。
var selObj = window.getSelection();
使用Selection
对象的toString
方法可以得到选中的文本。
var selectedText = selObj.toString();
window.getComputedStyle(),window.matchMedia()
window.getComputedStyle()
方法接受一个元素节点作为参数,返回一个包含该元素的最终样式信息的对象,详见《CSS 操作》一章。
window.matchMedia()
方法用来检查 CSS 的mediaQuery
语句,详见《CSS 操作》一章。
window.requestAnimationFrame()
window.requestAnimationFrame()
方法跟setTimeout
类似,都是推迟某个函数的执行。不同之处在于,setTimeout
必须指定推迟的时间,window.requestAnimationFrame()
则是推迟到浏览器下一次重流时执行,执行完才会进行下一次重绘。重绘通常是 16ms 执行一次,不过浏览器会自动调节这个速率,比如网页切换到后台 Tab 页时,requestAnimationFrame()
会暂停执行。
如果某个函数会改变网页的布局,一般就放在window.requestAnimationFrame()
里面执行,这样可以节省系统资源,使得网页效果更加平滑。因为慢速设备会用较慢的速率重流和重绘,而速度更快的设备会有更快的速率。
该方法接受一个回调函数作为参数。
window.requestAnimationFrame(callback)
上面代码中,callback
是一个回调函数。callback
执行时,它的参数就是系统传入的一个高精度时间戳(performance.now()
的返回值),单位是毫秒,表示距离网页加载的时间。
window.requestAnimationFrame()
的返回值是一个整数,这个整数可以传入window.cancelAnimationFrame()
,用来取消回调函数的执行。
下面是一个window.requestAnimationFrame()
执行网页动画的例子。
var element = document.getElementById('animate');
element.style.position = 'absolute';
var start = null;
function step(timestamp) {
if (!start) start = timestamp;
var progress = timestamp - start;
// 元素不断向左移,最大不超过200像素
element.style.left = Math.min(progress / 10, 200) + 'px';
// 如果距离第一次执行不超过 2000 毫秒,
// 就继续执行动画
if (progress < 2000) {
window.requestAnimationFrame(step);
}
}
window.requestAnimationFrame(step);
上面代码定义了一个网页动画,持续时间是2秒,会让元素向右移动。
window.requestIdleCallback()
window.requestIdleCallback()
跟setTimeout
类似,也是将某个函数推迟执行,但是它保证将回调函数推迟到系统资源空闲时执行。也就是说,如果某个任务不是很关键,就可以使用window.requestIdleCallback()
将其推迟执行,以保证网页性能。
它跟window.requestAnimationFrame()
的区别在于,后者指定回调函数在下一次浏览器重排时执行,问题在于下一次重排时,系统资源未必空闲,不一定能保证在16毫秒之内完成;window.requestIdleCallback()
可以保证回调函数在系统资源空闲时执行。
该方法接受一个回调函数和一个配置对象作为参数。配置对象可以指定一个推迟执行的最长时间,如果过了这个时间,回调函数不管系统资源有无空闲,都会执行。
window.requestIdleCallback(callback[, options])
callback
参数是一个回调函数。该回调函数执行时,系统会传入一个IdleDeadline
对象作为参数。IdleDeadline
对象有一个didTimeout
属性(布尔值,表示是否为超时调用)和一个timeRemaining()
方法(返回该空闲时段剩余的毫秒数)。
options
参数是一个配置对象,目前只有timeout
一个属性,用来指定回调函数推迟执行的最大毫秒数。该参数可选。
window.requestIdleCallback()
方法返回一个整数。该整数可以传入window.cancelIdleCallback()
取消回调函数。
下面是一个例子。
requestIdleCallback(myNonEssentialWork);
function myNonEssentialWork(deadline) {
while (deadline.timeRemaining() > 0) {
doWorkIfNeeded();
}
}
上面代码中,requestIdleCallback()
用来执行非关键任务myNonEssentialWork
。该任务先确认本次空闲时段有剩余时间,然后才真正开始执行任务。
下面是指定timeout
的例子。
requestIdleCallback(processPendingAnalyticsEvents, { timeout: 2000 });
上面代码指定,processPendingAnalyticsEvents
必须在未来2秒之内执行。
如果由于超时导致回调函数执行,则deadline.timeRemaining()
返回0
,deadline.didTimeout
返回true
。
如果多次执行window.requestIdleCallback()
,指定多个回调函数,那么这些回调函数将排成一个队列,按照先进先出的顺序执行。
事件
window
对象可以接收以下事件。
load 事件和 onload 属性
load
事件发生在文档在浏览器窗口加载完毕时。window.onload
属性可以指定这个事件的回调函数。
window.onload = function() {
var elements = document.getElementsByClassName('example');
for (var i = 0; i < elements.length; i++) {
var elt = elements[i];
// ...
}
};
上面代码在网页加载完毕后,获取指定元素并进行处理。
error 事件和 onerror 属性
浏览器脚本发生错误时,会触发window
对象的error
事件。我们可以通过window.onerror
属性对该事件指定回调函数。
window.onerror = function (message, filename, lineno, colno, error) {
console.log("出错了!--> %s", error.stack);
};
由于历史原因,window
的error
事件的回调函数不接受错误对象作为参数,而是一共可以接受五个参数,它们的含义依次如下。
- 出错信息
- 出错脚本的网址
- 行号
- 列号
- 错误对象
老式浏览器只支持前三个参数。
并不是所有的错误,都会触发 JavaScript 的error
事件(即让 JavaScript 报错)。一般来说,只有 JavaScript 脚本的错误,才会触发这个事件,而像资源文件不存在之类的错误,都不会触发。
下面是一个例子,如果整个页面未捕获错误超过3个,就显示警告。
window.onerror = function(msg, url, line) {
if (onerror.num++ > onerror.max) {
alert('ERROR: ' + msg + '\n' + url + ':' + line);
return true;
}
}
onerror.max = 3;
onerror.num = 0;
需要注意的是,如果脚本网址与网页网址不在同一个域(比如使用了 CDN),浏览器根本不会提供详细的出错信息,只会提示出错,错误类型是“Script error.”,行号为0,其他信息都没有。这是浏览器防止向外部脚本泄漏信息。一个解决方法是在脚本所在的服务器,设置Access-Control-Allow-Origin
的 HTTP 头信息。
Access-Control-Allow-Origin: *
然后,在网页的<script>
标签中设置crossorigin
属性。
<script crossorigin="anonymous" src="//example.com/file.js"></script>
上面代码的crossorigin="anonymous"
表示,读取文件不需要身份信息,即不需要 cookie 和 HTTP 认证信息。如果设为crossorigin="use-credentials"
,就表示浏览器会上传 cookie 和 HTTP 认证信息,同时还需要服务器端打开 HTTP 头信息Access-Control-Allow-Credentials
。
window 对象的事件监听属性
除了具备元素节点都有的 GlobalEventHandlers 接口,window
对象还具有以下的事件监听函数属性。
window.onafterprint
:afterprint
事件的监听函数。window.onbeforeprint
:beforeprint
事件的监听函数。window.onbeforeunload
:beforeunload
事件的监听函数。window.onhashchange
:hashchange
事件的监听函数。window.onlanguagechange
:languagechange
的监听函数。window.onmessage
:message
事件的监听函数。window.onmessageerror
:MessageError
事件的监听函数。window.onoffline
:offline
事件的监听函数。window.ononline
:online
事件的监听函数。window.onpagehide
:pagehide
事件的监听函数。window.onpageshow
:pageshow
事件的监听函数。window.onpopstate
:popstate
事件的监听函数。window.onstorage
:storage
事件的监听函数。window.onunhandledrejection
:未处理的 Promise 对象的reject
事件的监听函数。window.onunload
:unload
事件的监听函数。
多窗口操作
由于网页可以使用iframe
元素,嵌入其他网页,因此一个网页之中会形成多个窗口。如果子窗口之中又嵌入别的网页,就会形成多级窗口。
窗口的引用
各个窗口之中的脚本,可以引用其他窗口。浏览器提供了一些特殊变量,用来返回其他窗口。
top
:顶层窗口,即最上层的那个窗口parent
:父窗口self
:当前窗口,即自身
下面代码可以判断,当前窗口是否为顶层窗口。
if (window.top === window.self) {
// 当前窗口是顶层窗口
} else {
// 当前窗口是子窗口
}
下面的代码让父窗口的访问历史后退一次。
window.parent.history.back();
与这些变量对应,浏览器还提供一些特殊的窗口名,供window.open()
方法、<a>
标签、<form>
标签等引用。
_top
:顶层窗口_parent
:父窗口_blank
:新窗口
下面代码就表示在顶层窗口打开链接。
<a href="somepage.html" target="_top">Link</a>
iframe 元素
对于iframe
嵌入的窗口,document.getElementById
方法可以拿到该窗口的 DOM 节点,然后使用contentWindow
属性获得iframe
节点包含的window
对象。
var frame = document.getElementById('theFrame');
var frameWindow = frame.contentWindow;
上面代码中,frame.contentWindow
可以拿到子窗口的window
对象。然后,在满足同源限制的情况下,可以读取子窗口内部的属性。
// 获取子窗口的标题
frameWindow.title
<iframe>
元素的contentDocument
属性,可以拿到子窗口的document
对象。
var frame = document.getElementById('theFrame');
var frameDoc = frame.contentDocument;
// 等同于
var frameDoc = frame.contentWindow.document;
<iframe>
元素遵守同源政策,只有当父窗口与子窗口在同一个域时,两者之间才可以用脚本通信,否则只有使用window.postMessage
方法。
<iframe>
窗口内部,使用window.parent
引用父窗口。如果当前页面没有父窗口,则window.parent
属性返回自身。因此,可以通过window.parent
是否等于window.self
,判断当前窗口是否为iframe
窗口。
if (window.parent !== window.self) {
// 当前窗口是子窗口
}
<iframe>
窗口的window
对象,有一个frameElement
属性,返回<iframe>
在父窗口中的 DOM 节点。对于非嵌入的窗口,该属性等于null
。
var f1Element = document.getElementById('f1');
var f1Window = f1Element.contentWindow;
f1Window.frameElement === f1Element // true
window.frameElement === null // true
window.frames 属性
window.frames
属性返回一个类似数组的对象,成员是所有子窗口的window
对象。可以使用这个属性,实现窗口之间的互相引用。比如,frames[0]
返回第一个子窗口,frames[1].frames[2]
返回第二个子窗口内部的第三个子窗口,parent.frames[1]
返回父窗口的第二个子窗口。
注意,window.frames
每个成员的值,是框架内的窗口(即框架的window
对象),而不是iframe
标签在父窗口的 DOM 节点。如果要获取每个框架内部的 DOM 树,需要使用window.frames[0].document
的写法。
另外,如果<iframe>
元素设置了name
或id
属性,那么属性值会自动成为全局变量,并且可以通过window.frames
属性引用,返回子窗口的window
对象。
// HTML 代码为 <iframe id="myFrame">
window.myFrame // [HTMLIFrameElement]
frames.myframe === myFrame // true
另外,name
属性的值会自动成为子窗口的名称,可以用在window.open
方法的第二个参数,或者<a>
和<frame>
标签的target
属性。
Location 对象,URL 对象,URLSearchParams 对象
URL 是互联网的基础设施之一。浏览器提供了一些原生对象,用来管理 URL。
Location 对象
Location
对象是浏览器提供的原生对象,提供 URL 相关的信息和操作方法。通过window.location
和document.location
属性,可以拿到这个对象。
属性
Location
对象提供以下属性。
Location.href
:整个 URL。Location.protocol
:当前 URL 的协议,包括冒号(:
)。Location.host
:主机。如果端口不是协议默认的80
和433
,则还会包括冒号(:
)和端口。Location.hostname
:主机名,不包括端口。Location.port
:端口号。Location.pathname
:URL 的路径部分,从根路径/
开始。Location.search
:查询字符串部分,从问号?
开始。Location.hash
:片段字符串部分,从#
开始。Location.username
:域名前面的用户名。Location.password
:域名前面的密码。Location.origin
:URL 的协议、主机名和端口。
// 当前网址为
// http://user:passwd@www.example.com:4097/path/a.html?x=111#part1
document.location.href
// "http://user:passwd@www.example.com:4097/path/a.html?x=111#part1"
document.location.protocol
// "http:"
document.location.host
// "www.example.com:4097"
document.location.hostname
// "www.example.com"
document.location.port
// "4097"
document.location.pathname
// "/path/a.html"
document.location.search
// "?x=111"
document.location.hash
// "#part1"
document.location.username
// "user"
document.location.password
// "passwd"
document.location.origin
// "http://user:passwd@www.example.com:4097"
这些属性里面,只有origin
属性是只读的,其他属性都可写。
注意,如果对Location.href
写入新的 URL 地址,浏览器会立刻跳转到这个新地址。
// 跳转到新网址
document.location.href = 'http://www.example.com';
这个特性常常用于让网页自动滚动到新的锚点。
document.location.href = '#top';
// 等同于
document.location.hash = '#top';
直接改写location
,相当于写入href
属性。
document.location = 'http://www.example.com';
// 等同于
document.location.href = 'http://www.example.com';
另外,Location.href
属性是浏览器唯一允许跨域写入的属性,即非同源的窗口可以改写另一个窗口(比如子窗口与父窗口)的Location.href
属性,导致后者的网址跳转。Location
的其他属性都不允许跨域写入。
方法
(1)Location.assign()
assign
方法接受一个 URL 字符串作为参数,使得浏览器立刻跳转到新的 URL。如果参数不是有效的 URL 字符串,则会报错。
// 跳转到新的网址
document.location.assign('http://www.example.com')
(2)Location.replace()
replace
方法接受一个 URL 字符串作为参数,使得浏览器立刻跳转到新的 URL。如果参数不是有效的 URL 字符串,则会报错。
它与assign
方法的差异在于,replace
会在浏览器的浏览历史History
里面删除当前网址,也就是说,一旦使用了该方法,后退按钮就无法回到当前网页了,相当于在浏览历史里面,使用新的 URL 替换了老的 URL。它的一个应用是,当脚本发现当前是移动设备时,就立刻跳转到移动版网页。
// 跳转到新的网址
document.location.replace('http://www.example.com')
(3)Location.reload()
reload
方法使得浏览器重新加载当前网址,相当于按下浏览器的刷新按钮。
它接受一个布尔值作为参数。如果参数为true
,浏览器将向服务器重新请求这个网页,并且重新加载后,网页将滚动到头部(即scrollTop === 0
)。如果参数是false
或为空,浏览器将从本地缓存重新加载该网页,并且重新加载后,网页的视口位置是重新加载前的位置。
// 向服务器重新请求当前网址
window.location.reload(true);
(4)Location.toString()
toString
方法返回整个 URL 字符串,相当于读取Location.href
属性。
URL 的编码和解码
网页的 URL 只能包含合法的字符。合法字符分成两类。
- URL 元字符:分号(
;
),逗号(,
),斜杠(/
),问号(?
),冒号(:
),at(@
),&
,等号(=
),加号(+
),美元符号($
),井号(#
) - 语义字符:
a-z
,A-Z
,0-9
,连词号(-
),下划线(_
),点(.
),感叹号(!
),波浪线(~
),星号(*
),单引号('
),圆括号(()
)
除了以上字符,其他字符出现在 URL 之中都必须转义,规则是根据操作系统的默认编码,将每个字节转为百分号(%
)加上两个大写的十六进制字母。
比如,UTF-8 的操作系统上,http://www.example.com/q=春节
这个 URL 之中,汉字“春节”不是 URL 的合法字符,所以被浏览器自动转成http://www.example.com/q=%E6%98%A5%E8%8A%82
。其中,“春”转成了%E6%98%A5
,“节”转成了%E8%8A%82
。这是因为“春”和“节”的 UTF-8 编码分别是E6 98 A5
和E8 8A 82
,将每个字节前面加上百分号,就构成了 URL 编码。
JavaScript 提供四个 URL 的编码/解码方法。
encodeURI()
encodeURIComponent()
decodeURI()
decodeURIComponent()
encodeURI()
encodeURI()
方法用于转码整个 URL。它的参数是一个字符串,代表整个 URL。它会将元字符和语义字符之外的字符,都进行转义。
encodeURI('http://www.example.com/q=春节')
// "http://www.example.com/q=%E6%98%A5%E8%8A%82"
encodeURIComponent()
encodeURIComponent()
方法用于转码 URL 的组成部分,会转码除了语义字符之外的所有字符,即元字符也会被转码。所以,它不能用于转码整个 URL。它接受一个参数,就是 URL 的片段。
encodeURIComponent('春节')
// "%E6%98%A5%E8%8A%82"
encodeURIComponent('http://www.example.com/q=春节')
// "http%3A%2F%2Fwww.example.com%2Fq%3D%E6%98%A5%E8%8A%82"
上面代码中,encodeURIComponent()
会连 URL 元字符一起转义,所以如果转码整个 URL 就会出错。
decodeURI()
decodeURI()
方法用于整个 URL 的解码。它是encodeURI()
方法的逆运算。它接受一个参数,就是转码后的 URL。
decodeURI('http://www.example.com/q=%E6%98%A5%E8%8A%82')
// "http://www.example.com/q=春节"
decodeURIComponent()
decodeURIComponent()
用于URL 片段的解码。它是encodeURIComponent()
方法的逆运算。它接受一个参数,就是转码后的 URL 片段。
decodeURIComponent('%E6%98%A5%E8%8A%82')
// "春节"
URL 接口
浏览器原生提供URL()
接口,它是一个构造函数,用来构造、解析和编码 URL。一般情况下,通过window.URL
可以拿到这个构造函数。
构造函数
URL()
作为构造函数,可以生成 URL 实例。它接受一个表示 URL 的字符串作为参数。如果参数不是合法的 URL,会报错。
var url = new URL('http://www.example.com/index.html');
url.href
// "http://www.example.com/index.html"
上面示例生成了一个 URL 实例,用来代表指定的网址。
除了字符串,URL()
的参数也可以是另一个 URL 实例。这时,URL()
会自动读取该实例的href
属性,作为实际参数。
如果 URL 字符串是一个相对路径,那么需要表示绝对路径的第二个参数,作为计算基准。
var url1 = new URL('index.html', 'http://example.com');
url1.href
// "http://example.com/index.html"
var url2 = new URL('page2.html', 'http://example.com/page1.html');
url2.href
// "http://example.com/page2.html"
var url3 = new URL('..', 'http://example.com/a/b.html')
url3.href
// "http://example.com/"
上面代码中,返回的 URL 实例的路径都是在第二个参数的基础上,切换到第一个参数得到的。最后一个例子里面,第一个参数是..
,表示上层路径。
实例属性
URL 实例的属性与Location
对象的属性基本一致,返回当前 URL 的信息。
- URL.href:返回整个 URL
- URL.protocol:返回协议,以冒号
:
结尾 - URL.hostname:返回域名
- URL.host:返回域名与端口,包含
:
号,默认的80和443端口会省略 - URL.port:返回端口
- URL.origin:返回协议、域名和端口
- URL.pathname:返回路径,以斜杠
/
开头 - URL.search:返回查询字符串,以问号
?
开头 - URL.searchParams:返回一个
URLSearchParams
实例,该属性是Location
对象没有的 - URL.hash:返回片段识别符,以井号
#
开头 - URL.password:返回域名前面的密码
- URL.username:返回域名前面的用户名
var url = new URL('http://user:passwd@www.example.com:4097/path/a.html?x=111#part1');
url.href
// "http://user:passwd@www.example.com:4097/path/a.html?x=111#part1"
url.protocol
// "http:"
url.hostname
// "www.example.com"
url.host
// "www.example.com:4097"
url.port
// "4097"
url.origin
// "http://www.example.com:4097"
url.pathname
// "/path/a.html"
url.search
// "?x=111"
url.searchParams
// URLSearchParams {}
url.hash
// "#part1"
url.password
// "passwd"
url.username
// "user"
这些属性里面,只有origin
属性是只读的,其他属性都可写,并且会立即生效。
var url = new URL('http://example.com/index.html#part1');
url.pathname = 'index2.html';
url.href // "http://example.com/index2.html#part1"
url.hash = '#part2';
url.href // "http://example.com/index2.html#part2"
上面代码中,改变 URL 实例的pathname
属性和hash
属性,都会实时反映在 URL 实例当中。
静态方法
(1)URL.createObjectURL()
URL.createObjectURL()
方法用来为上传/下载的文件、流媒体文件生成一个 URL 字符串。这个字符串代表了File
对象或Blob
对象的 URL。
// HTML 代码如下
// <div id="display"/>
// <input
// type="file"
// id="fileElem"
// multiple
// accept="image/*"
// οnchange="handleFiles(this.files)"
// >
var div = document.getElementById('display');
function handleFiles(files) {
for (var i = 0; i < files.length; i++) {
var img = document.createElement('img');
img.src = window.URL.createObjectURL(files[i]);
div.appendChild(img);
}
}
上面代码中,URL.createObjectURL()
方法用来为上传的文件生成一个 URL 字符串,作为<img>
元素的图片来源。
该方法生成的 URL 就像下面的样子。
blob:http://localhost/c745ef73-ece9-46da-8f66-ebes574789b1
注意,每次使用URL.createObjectURL()
方法,都会在内存里面生成一个 URL 实例。如果不再需要该方法生成的 URL 字符串,为了节省内存,可以使用URL.revokeObjectURL()
方法释放这个实例。
(2)URL.revokeObjectURL()
URL.revokeObjectURL()
方法用来释放URL.createObjectURL()
方法生成的 URL 实例。它的参数就是URL.createObjectURL()
方法返回的 URL 字符串。
下面为上一段的示例加上URL.revokeObjectURL()
。
var div = document.getElementById('display');
function handleFiles(files) {
for (var i = 0; i < files.length; i++) {
var img = document.createElement('img');
img.src = window.URL.createObjectURL(files[i]);
div.appendChild(img);
img.onload = function() {
window.URL.revokeObjectURL(this.src);
}
}
}
上面代码中,一旦图片加载成功以后,为本地文件生成的 URL 字符串就没用了,于是可以在img.onload
回调函数里面,通过URL.revokeObjectURL()
方法卸载这个 URL 实例。
URLSearchParams 对象
概述
URLSearchParams
对象是浏览器的原生对象,用来构造、解析和处理 URL 的查询字符串(即 URL 问号后面的部分)。
它本身也是一个构造函数,可以生成实例。参数可以为查询字符串,起首的问号?
有没有都行,也可以是对应查询字符串的数组或对象。
// 方法一:传入字符串
var params = new URLSearchParams('?foo=1&bar=2');
// 等同于
var params = new URLSearchParams(document.location.search);
// 方法二:传入数组
var params = new URLSearchParams([['foo', 1], ['bar', 2]]);
// 方法三:传入对象
var params = new URLSearchParams({'foo' : 1 , 'bar' : 2});
URLSearchParams
会对查询字符串自动编码。
var params = new URLSearchParams({'foo': '你好'});
params.toString() // "foo=%E4%BD%A0%E5%A5%BD"
上面代码中,foo
的值是汉字,URLSearchParams
对其自动进行 URL 编码。
浏览器向服务器发送表单数据时,可以直接使用URLSearchParams
实例作为表单数据。
const params = new URLSearchParams({foo: 1, bar: 2});
fetch('https://example.com/api', {
method: 'POST',
body: params
}).then(...)
上面代码中,fetch
命令向服务器发送命令时,可以直接使用URLSearchParams
实例。
URLSearchParams
可以与URL()
接口结合使用。
var url = new URL(window.location);
var foo = url.searchParams.get('foo') || 'somedefault';
上面代码中,URL 实例的searchParams
属性就是一个URLSearchParams
实例,所以可以使用URLSearchParams
接口的get
方法。
URLSearchParams
实例有遍历器接口,可以用for...of
循环遍历(详见《ES6 标准入门》的《Iterator》一章)。
var params = new URLSearchParams({'foo': 1 , 'bar': 2});
for (var p of params) {
console.log(p[0] + ': ' + p[1]);
}
// foo: 1
// bar: 2
URLSearchParams
没有实例属性,只有实例方法。
URLSearchParams.toString()
toString
方法返回实例的字符串形式。
var url = new URL('https://example.com?foo=1&bar=2');
var params = new URLSearchParams(url.search);
params.toString() // "foo=1&bar=2'
那么需要字符串的场合,会自动调用toString
方法。
var params = new URLSearchParams({version: 2.0});
window.location.href = location.pathname + '?' + params;
上面代码中,location.href
赋值时,可以直接使用params
对象。这时就会自动调用toString
方法。
URLSearchParams.append()
append()
方法用来追加一个查询参数。它接受两个参数,第一个为键名,第二个为键值,没有返回值。
var params = new URLSearchParams({'foo': 1 , 'bar': 2});
params.append('baz', 3);
params.toString() // "foo=1&bar=2&baz=3"
append()
方法不会识别是否键名已经存在。
var params = new URLSearchParams({'foo': 1 , 'bar': 2});
params.append('foo', 3);
params.toString() // "foo=1&bar=2&foo=3"
上面代码中,查询字符串里面foo
已经存在了,但是append
依然会追加一个同名键。
URLSearchParams.delete()
delete()
方法用来删除指定的查询参数。它接受键名作为参数。
var params = new URLSearchParams({'foo': 1 , 'bar': 2});
params.delete('bar');
params.toString() // "foo=1"
URLSearchParams.has()
has()
方法返回一个布尔值,表示查询字符串是否包含指定的键名。
var params = new URLSearchParams({'foo': 1 , 'bar': 2});
params.has('bar') // true
params.has('baz') // false
URLSearchParams.set()
set()
方法用来设置查询字符串的键值。
它接受两个参数,第一个是键名,第二个是键值。如果是已经存在的键,键值会被改写,否则会被追加。
var params = new URLSearchParams('?foo=1');
params.set('foo', 2);
params.toString() // "foo=2"
params.set('bar', 3);
params.toString() // "foo=2&bar=3"
上面代码中,foo
是已经存在的键,bar
是还不存在的键。
如果有多个的同名键,set
会移除现存所有的键。
var params = new URLSearchParams('?foo=1&foo=2');
params.set('foo', 3);
params.toString() // "foo=3"
下面是一个替换当前 URL 的例子。
// URL: https://example.com?version=1.0
var params = new URLSearchParams(location.search.slice(1));
params.set('version', '2.0');
window.history.replaceState({}, '', location.pathname + `?` + params);
// URL: https://example.com?version=2.0
URLSearchParams.get(),URLSearchParams.getAll()
get()
方法用来读取查询字符串里面的指定键。它接受键名作为参数。
var params = new URLSearchParams('?foo=1');
params.get('foo') // "1"
params.get('bar') // null
两个地方需要注意。第一,它返回的是字符串,如果原始值是数值,需要转一下类型;第二,如果指定的键名不存在,返回值是null
。
如果有多个的同名键,get
返回位置最前面的那个键值。
var params = new URLSearchParams('?foo=3&foo=2&foo=1');
params.get('foo') // "3"
上面代码中,查询字符串有三个foo
键,get
方法返回最前面的键值3
。
getAll()
方法返回一个数组,成员是指定键的所有键值。它接受键名作为参数。
var params = new URLSearchParams('?foo=1&foo=2');
params.getAll('foo') // ["1", "2"]
上面代码中,查询字符串有两个foo
键,getAll
返回的数组就有两个成员。
URLSearchParams.sort()
sort()
方法对查询字符串里面的键进行排序,规则是按照 Unicode 码点从小到大排列。
该方法没有返回值,或者说返回值是undefined
。
var params = new URLSearchParams('c=4&a=2&b=3&a=1');
params.sort();
params.toString() // "a=2&a=1&b=3&c=4"
上面代码中,如果有两个同名的键a
,它们之间不会排序,而是保留原始的顺序。
URLSearchParams.keys(),URLSearchParams.values(),URLSearchParams.entries()
这三个方法都返回一个遍历器对象,供for...of
循环遍历。它们的区别在于,keys
方法返回的是键名的遍历器,values
方法返回的是键值的遍历器,entries
返回的是键值对的遍历器。
var params = new URLSearchParams('a=1&b=2');
for(var p of params.keys()) {
console.log(p);
}
// a
// b
for(var p of params.values()) {
console.log(p);
}
// 1
// 2
for(var p of params.entries()) {
console.log(p);
}
// ["a", "1"]
// ["b", "2"]
如果直接对URLSearchParams
进行遍历,其实内部调用的就是entries
接口。
for (var p of params) {}
// 等同于
for (var p of params.entries()) {}
参考链接
- Location, by MDN
- URL, by MDN
- URLSearchParams, by MDN
- Easy URL Manipulation with URLSearchParams, by Eric Bidelman
同源限制
浏览器安全的基石是“同源政策”(same-origin policy)。很多开发者都知道这一点,但了解得不全面。
概述
含义
1995年,同源政策由 Netscape 公司引入浏览器。目前,所有浏览器都实行这个政策。
最初,它的含义是指,A 网页设置的 Cookie,B 网页不能打开,除非这两个网页“同源”。所谓“同源”指的是“三个相同”。
- 协议相同
- 域名相同
- 端口相同(这点可以忽略,详见下文)
举例来说,http://www.example.com/dir/page.html
这个网址,协议是http://
,域名是www.example.com
,端口是80
(默认端口可以省略),它的同源情况如下。
http://www.example.com/dir2/other.html
:同源http://example.com/dir/other.html
:不同源(域名不同)http://v2.www.example.com/dir/other.html
:不同源(域名不同)http://www.example.com:81/dir/other.html
:不同源(端口不同)https://www.example.com/dir/page.html
:不同源(协议不同)
注意,标准规定端口不同的网址不是同源(比如8000端口和8001端口不是同源),但是浏览器没有遵守这条规定。实际上,同一个网域的不同端口,是可以互相读取 Cookie 的。
目的
同源政策的目的,是为了保证用户信息的安全,防止恶意的网站窃取数据。
设想这样一种情况:A 网站是一家银行,用户登录以后,A 网站在用户的机器上设置了一个 Cookie,包含了一些隐私信息。用户离开 A 网站以后,又去访问 B 网站,如果没有同源限制,B 网站可以读取 A 网站的 Cookie,那么隐私就泄漏了。更可怕的是,Cookie 往往用来保存用户的登录状态,如果用户没有退出登录,其他网站就可以冒充用户,为所欲为。因为浏览器同时还规定,提交表单不受同源政策的限制。
由此可见,同源政策是必需的,否则 Cookie 可以共享,互联网就毫无安全可言了。
限制范围
随着互联网的发展,同源政策越来越严格。目前,如果非同源,共有三种行为受到限制。
(1) 无法读取非同源网页的 Cookie、LocalStorage 和 IndexedDB。
(2) 无法接触非同源网页的 DOM。
(3) 无法向非同源地址发送 AJAX 请求(可以发送,但浏览器会拒绝接受响应)。
另外,通过 JavaScript 脚本可以拿到其他窗口的window
对象。如果是非同源的网页,目前允许一个窗口可以接触其他网页的window
对象的九个属性和四个方法。
- window.closed
- window.frames
- window.length
- window.location
- window.opener
- window.parent
- window.self
- window.top
- window.window
- window.blur()
- window.close()
- window.focus()
- window.postMessage()
上面的九个属性之中,只有window.location
是可读写的,其他八个全部都是只读。而且,即使是location
对象,非同源的情况下,也只允许调用location.replace()
方法和写入location.href
属性。
虽然这些限制是必要的,但是有时很不方便,合理的用途也受到影响。下面介绍如何规避上面的限制。
Cookie
Cookie 是服务器写入浏览器的一小段信息,只有同源的网页才能共享。如果两个网页一级域名相同,只是次级域名不同,浏览器允许通过设置document.domain
共享 Cookie。
举例来说,A 网页的网址是http://w1.example.com/a.html
,B 网页的网址是http://w2.example.com/b.html
,那么只要设置相同的document.domain
,两个网页就可以共享 Cookie。因为浏览器通过document.domain
属性来检查是否同源。
// 两个网页都需要设置
document.domain = 'example.com';
注意,A 和 B 两个网页都需要设置document.domain
属性,才能达到同源的目的。因为设置document.domain
的同时,会把端口重置为null
,因此如果只设置一个网页的document.domain
,会导致两个网址的端口不同,还是达不到同源的目的。
现在,A 网页通过脚本设置一个 Cookie。
document.cookie = "test1=hello";
B 网页就可以读到这个 Cookie。
var allCookie = document.cookie;
注意,这种方法只适用于 Cookie 和 iframe 窗口,LocalStorage 和 IndexedDB 无法通过这种方法,规避同源政策,而要使用下文介绍 PostMessage API。
另外,服务器也可以在设置 Cookie 的时候,指定 Cookie 的所属域名为一级域名,比如example.com
。
Set-Cookie: key=value; domain=example.com; path=/
这样的话,二级域名和三级域名不用做任何设置,都可以读取这个 Cookie。
iframe 和多窗口通信
iframe
元素可以在当前网页之中,嵌入其他网页。每个iframe
元素形成自己的窗口,即有自己的window
对象。iframe
窗口之中的脚本,可以获得父窗口和子窗口。但是,只有在同源的情况下,父窗口和子窗口才能通信;如果跨域,就无法拿到对方的 DOM。
比如,父窗口运行下面的命令,如果iframe
窗口不是同源,就会报错。
document
.getElementById("myIFrame")
.contentWindow
.document
// Uncaught DOMException: Blocked a frame from accessing a cross-origin frame.
上面命令中,父窗口想获取子窗口的 DOM,因为跨域导致报错。
反之亦然,子窗口获取主窗口的 DOM 也会报错。
window.parent.document.body
// 报错
这种情况不仅适用于iframe
窗口,还适用于window.open
方法打开的窗口,只要跨域,父窗口与子窗口之间就无法通信。
如果两个窗口一级域名相同,只是二级域名不同,那么设置上一节介绍的document.domain
属性,就可以规避同源政策,拿到 DOM。
对于完全不同源的网站,目前有两种方法,可以解决跨域窗口的通信问题。
- 片段识别符(fragment identifier)
- 跨文档通信API(Cross-document messaging)
片段识别符
片段标识符(fragment identifier)指的是,URL 的#
号后面的部分,比如http://example.com/x.html#fragment
的#fragment
。如果只是改变片段标识符,页面不会重新刷新。
父窗口可以把信息,写入子窗口的片段标识符。
var src = originURL + '#' + data;
document.getElementById('myIFrame').src = src;
上面代码中,父窗口把所要传递的信息,写入 iframe 窗口的片段标识符。
子窗口通过监听hashchange
事件得到通知。
window.onhashchange = checkMessage;
function checkMessage() {
var message = window.location.hash;
// ...
}
同样的,子窗口也可以改变父窗口的片段标识符。
parent.location.href = target + '#' + hash;
window.postMessage()
上面的这种方法属于破解,HTML5 为了解决这个问题,引入了一个全新的API:跨文档通信 API(Cross-document messaging)。
这个 API 为window
对象新增了一个window.postMessage
方法,允许跨窗口通信,不论这两个窗口是否同源。举例来说,父窗口aaa.com
向子窗口bbb.com
发消息,调用postMessage
方法就可以了。
// 父窗口打开一个子窗口
var popup = window.open('http://bbb.com', 'title');
// 父窗口向子窗口发消息
popup.postMessage('Hello World!', 'http://bbb.com');
postMessage
方法的第一个参数是具体的信息内容,第二个参数是接收消息的窗口的源(origin),即“协议 + 域名 + 端口”。也可以设为*
,表示不限制域名,向所有窗口发送。
子窗口向父窗口发送消息的写法类似。
// 子窗口向父窗口发消息
window.opener.postMessage('Nice to see you', 'http://aaa.com');
父窗口和子窗口都可以通过message
事件,监听对方的消息。
// 父窗口和子窗口都可以用下面的代码,
// 监听 message 消息
window.addEventListener('message', function (e) {
console.log(e.data);
},false);
message
事件的参数是事件对象event
,提供以下三个属性。
event.source
:发送消息的窗口event.origin
: 消息发向的网址event.data
: 消息内容
下面的例子是,子窗口通过event.source
属性引用父窗口,然后发送消息。
window.addEventListener('message', receiveMessage);
function receiveMessage(event) {
event.source.postMessage('Nice to see you!', '*');
}
上面代码有几个地方需要注意。首先,receiveMessage
函数里面没有过滤信息的来源,任意网址发来的信息都会被处理。其次,postMessage
方法中指定的目标窗口的网址是一个星号,表示该信息可以向任意网址发送。通常来说,这两种做法是不推荐的,因为不够安全,可能会被恶意利用。
event.origin
属性可以过滤不是发给本窗口的消息。
window.addEventListener('message', receiveMessage);
function receiveMessage(event) {
if (event.origin !== 'http://aaa.com') return;
if (event.data === 'Hello World') {
event.source.postMessage('Hello', event.origin);
} else {
console.log(event.data);
}
}
LocalStorage
通过window.postMessage
,读写其他窗口的 LocalStorage 也成为了可能。
下面是一个例子,主窗口写入 iframe 子窗口的localStorage
。
window.onmessage = function(e) {
if (e.origin !== 'http://bbb.com') {
return;
}
var payload = JSON.parse(e.data);
localStorage.setItem(payload.key, JSON.stringify(payload.data));
};
上面代码中,子窗口将父窗口发来的消息,写入自己的 LocalStorage。
父窗口发送消息的代码如下。
var win = document.getElementsByTagName('iframe')[0].contentWindow;
var obj = { name: 'Jack' };
win.postMessage(
JSON.stringify({key: 'storage', data: obj}),
'http://bbb.com'
);
加强版的子窗口接收消息的代码如下。
window.onmessage = function(e) {
if (e.origin !== 'http://bbb.com') return;
var payload = JSON.parse(e.data);
switch (payload.method) {
case 'set':
localStorage.setItem(payload.key, JSON.stringify(payload.data));
break;
case 'get':
var parent = window.parent;
var data = localStorage.getItem(payload.key);
parent.postMessage(data, 'http://aaa.com');
break;
case 'remove':
localStorage.removeItem(payload.key);
break;
}
};
加强版的父窗口发送消息代码如下。
var win = document.getElementsByTagName('iframe')[0].contentWindow;
var obj = { name: 'Jack' };
// 存入对象
win.postMessage(
JSON.stringify({key: 'storage', method: 'set', data: obj}),
'http://bbb.com'
);
// 读取对象
win.postMessage(
JSON.stringify({key: 'storage', method: "get"}),
"*"
);
window.onmessage = function(e) {
if (e.origin != 'http://aaa.com') return;
console.log(JSON.parse(e.data).name);
};
AJAX
同源政策规定,AJAX 请求只能发给同源的网址,否则就报错。
除了架设服务器代理(浏览器请求同源服务器,再由后者请求外部服务),有三种方法规避这个限制。
- JSONP
- WebSocket
- CORS
JSONP
JSONP 是服务器与客户端跨源通信的常用方法。最大特点就是简单易用,没有兼容性问题,老式浏览器全部支持,服务端改造非常小。
它的做法如下。
第一步,网页添加一个<script>
元素,向服务器请求一个脚本,这不受同源政策限制,可以跨域请求。
<script src="http://api.foo.com?callback=bar"></script>
注意,请求的脚本网址有一个callback
参数(?callback=bar
),用来告诉服务器,客户端的回调函数名称(bar
)。
第二步,服务器收到请求后,拼接一个字符串,将 JSON 数据放在函数名里面,作为字符串返回(bar({...})
)。
第三步,客户端会将服务器返回的字符串,作为代码解析,因为浏览器认为,这是<script>
标签请求的脚本内容。这时,客户端只要定义了bar()
函数,就能在该函数体内,拿到服务器返回的 JSON 数据。
下面看一个实例。首先,网页动态插入<script>
元素,由它向跨域网址发出请求。
function addScriptTag(src) {
var script = document.createElement('script');
script.setAttribute('type', 'text/javascript');
script.src = src;
document.body.appendChild(script);
}
window.onload = function () {
addScriptTag('http://example.com/ip?callback=foo');
}
function foo(data) {
console.log('Your public IP address is: ' + data.ip);
};
上面代码通过动态添加<script>
元素,向服务器example.com
发出请求。注意,该请求的查询字符串有一个callback
参数,用来指定回调函数的名字,这对于 JSONP 是必需的。
服务器收到这个请求以后,会将数据放在回调函数的参数位置返回。
foo({
'ip': '8.8.8.8'
});
由于<script>
元素请求的脚本,直接作为代码运行。这时,只要浏览器定义了foo
函数,该函数就会立即调用。作为参数的 JSON 数据被视为 JavaScript 对象,而不是字符串,因此避免了使用JSON.parse
的步骤。
WebSocket
WebSocket 是一种通信协议,使用ws://
(非加密)和wss://
(加密)作为协议前缀。该协议不实行同源政策,只要服务器支持,就可以通过它进行跨源通信。
下面是一个例子,浏览器发出的 WebSocket 请求的头信息(摘自维基百科)。
GET /chat HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw==
Sec-WebSocket-Protocol: chat, superchat
Sec-WebSocket-Version: 13
Origin: http://example.com
上面代码中,有一个字段是Origin
,表示该请求的请求源(origin),即发自哪个域名。
正是因为有了Origin
这个字段,所以 WebSocket 才没有实行同源政策。因为服务器可以根据这个字段,判断是否许可本次通信。如果该域名在白名单内,服务器就会做出如下回应。
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: HSmrc0sMlYUkAGmm5OPpG2HaGWk=
Sec-WebSocket-Protocol: chat
CORS
CORS 是跨源资源分享(Cross-Origin Resource Sharing)的缩写。它是 W3C 标准,属于跨源 AJAX 请求的根本解决方法。相比 JSONP 只能发GET
请求,CORS 允许任何类型的请求。
下一章将详细介绍,如何通过 CORS 完成跨源 AJAX 请求。
参考链接
- Mozilla Developer Network, Window.postMessage
- Jakub Jankiewicz, Cross-Domain LocalStorage
- David Baron, setTimeout with a shorter delay: 利用 window.postMessage 可以实现0毫秒触发回调函数
XMLHttpRequest 对象
简介
浏览器与服务器之间,采用 HTTP 协议通信。用户在浏览器地址栏键入一个网址,或者通过网页表单向服务器提交内容,这时浏览器就会向服务器发出 HTTP 请求。
1999年,微软公司发布 IE 浏览器5.0版,第一次引入新功能:允许 JavaScript 脚本向服务器发起 HTTP 请求。这个功能当时并没有引起注意,直到2004年 Gmail 发布和2005年 Google Map 发布,才引起广泛重视。2005年2月,AJAX 这个词第一次正式提出,它是 Asynchronous JavaScript and XML 的缩写,指的是通过 JavaScript 的异步通信,从服务器获取 XML 文档从中提取数据,再更新当前网页的对应部分,而不用刷新整个网页。后来,AJAX 这个词就成为 JavaScript 脚本发起 HTTP 通信的代名词,也就是说,只要用脚本发起通信,就可以叫做 AJAX 通信。W3C 也在2006年发布了它的国际标准。
具体来说,AJAX 包括以下几个步骤。
- 创建 XMLHttpRequest 实例
- 发出 HTTP 请求
- 接收服务器传回的数据
- 更新网页数据
概括起来,就是一句话,AJAX 通过原生的XMLHttpRequest
对象发出 HTTP 请求,得到服务器返回的数据后,再进行处理。现在,服务器返回的都是 JSON 格式的数据,XML 格式已经过时了,但是 AJAX 这个名字已经成了一个通用名词,字面含义已经消失了。
XMLHttpRequest
对象是 AJAX 的主要接口,用于浏览器与服务器之间的通信。尽管名字里面有XML
和Http
,它实际上可以使用多种协议(比如file
或ftp
),发送任何格式的数据(包括字符串和二进制)。
XMLHttpRequest
本身是一个构造函数,可以使用new
命令生成实例。它没有任何参数。
var xhr = new XMLHttpRequest();
一旦新建实例,就可以使用open()
方法指定建立 HTTP 连接的一些细节。
xhr.open('GET', 'http://www.example.com/page.php', true);
上面代码指定使用 GET 方法,跟指定的服务器网址建立连接。第三个参数true
,表示请求是异步的。
然后,指定回调函数,监听通信状态(readyState
属性)的变化。
xhr.onreadystatechange = handleStateChange;
function handleStateChange() {
// ...
}
上面代码中,一旦XMLHttpRequest
实例的状态发生变化,就会调用监听函数handleStateChange
最后使用send()
方法,实际发出请求。
xhr.send(null);
上面代码中,send()
的参数为null
,表示发送请求的时候,不带有数据体。如果发送的是 POST 请求,这里就需要指定数据体。
一旦拿到服务器返回的数据,AJAX 不会刷新整个网页,而是只更新网页里面的相关部分,从而不打断用户正在做的事情。
注意,AJAX 只能向同源网址(协议、域名、端口都相同)发出 HTTP 请求,如果发出跨域请求,就会报错(详见《同源政策》和《CORS 通信》两章)。
下面是XMLHttpRequest
对象简单用法的完整例子。
var xhr = new XMLHttpRequest();
xhr.onreadystatechange = function(){
// 通信成功时,状态值为4
if (xhr.readyState === 4){
if (xhr.status === 200){
console.log(xhr.responseText);
} else {
console.error(xhr.statusText);
}
}
};
xhr.onerror = function (e) {
console.error(xhr.statusText);
};
xhr.open('GET', '/endpoint', true);
xhr.send(null);
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', 'zhangsan@example.com');
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='zhangsan@example.com'>
<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,可以跨域,类似于表单提交数据。它不能指定回调函数。
下面是一个例子。
// HTML 代码如下
// <body οnlοad="analytics('start')" οnunlοad="analytics('end')">
function analytics(state) {
if (!navigator.sendBeacon) return;
var URL = 'http://example.com/analytics';
var data = 'state=' + state + '&location=' + window.location;
navigator.sendBeacon(URL, data);
}
CORS 通信
CORS 是一个 W3C 标准,全称是“跨域资源共享”(Cross-origin resource sharing)。它允许浏览器向跨域的服务器,发出XMLHttpRequest
请求,从而克服了 AJAX 只能同源使用的限制。
简介
CORS 需要浏览器和服务器同时支持。目前,所有浏览器都支持该功能。
整个 CORS 通信过程,都是浏览器自动完成,不需要用户参与。对于开发者来说,CORS 通信与普通的 AJAX 通信没有差别,代码完全一样。浏览器一旦发现 AJAX 请求跨域,就会自动添加一些附加的头信息,有时还会多出一次附加的请求,但用户不会有感知。因此,实现 CORS 通信的关键是服务器。只要服务器实现了 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
字段是每次回应都必定包含的。
与 JSONP 的比较
CORS 与 JSONP 的使用目的相同,但是比 JSONP 更强大。JSONP 只支持GET
请求,CORS 支持所有类型的 HTTP 请求。JSONP 的优势在于支持老式浏览器,以及可以向不支持 CORS 的网站请求数据。
参考链接
- Using CORS, Monsur Hossain
- HTTP access control (CORS), MDN
- CORS, Ryan Miller
- Do You Really Know CORS?, Grzegorz Mirek