1.浏览器环境概述
JavaScript 是浏览器的内置脚本语言。也就是说,浏览器内置了 JavaScript 引擎,并且提供各种接口,让 JavaScript 脚本可以控制浏览器的各种功能。一旦网页内嵌了 JavaScript 脚本,浏览器加载网页,就会去执行脚本,从而达到操作浏览器的目的,实现网页的各种动态效果。
1.浏览器的组成
浏览器的核心是两部分:渲染引擎和 JavaScript 解释器(又称 JavaScript 引擎)。
渲染引擎
渲染引擎的主要作用是,将网页代码渲染为用户视觉可以感知的平面文档。
不同的浏览器有不同的渲染引擎。
- Firefox:Gecko 引擎
- Safari:WebKit 引擎
- Chrome:Blink 引擎
- IE: Trident 引擎
- Edge: EdgeHTML 引擎
渲染引擎处理网页,通常分成四个阶段。
- 解析代码:HTML 代码解析为 DOM,CSS 代码解析为 CSSOM(CSS Object Model)。
- 对象合成:将 DOM 和 CSSOM 合成一棵渲染树(render tree)。
- 布局:计算出渲染树的布局(layout)。
- 绘制:将渲染树绘制到屏幕。
以上四步并非严格按顺序执行,往往第一步还没完成,第二步和第三步就已经开始了。所以,会看到这种情况:网页的 HTML 代码还没下载完,但浏览器已经显示出内容了。
2.重流和重绘
渲染树转换为网页布局,称为“布局流”(flow);布局显示到页面的这个过程,称为“绘制”(paint)。它们都具有阻塞效应,并且会耗费很多时间和计算资源。
...
作为开发者,应该尽量设法降低重绘的次数和成本。比如,尽量不要变动高层的 DOM 元素,而以底层 DOM 元素的变动代替;再比如,重绘table布局和flex布局,开销都会比较大。
下面是一些优化技巧。
- 读取 DOM 或者写入 DOM,尽量写在一起,不要混杂。不要读取一个 DOM 节点,然后立刻写入,接着再读取一个 DOM 节点。
- 缓存 DOM 信息。
- 不要一项一项地改变样式,而是使用 CSS class 一次性改变样式。
- 使用
documentFragment操作 DOM - 动画使用
absolute定位或fixed定位,这样可以减少对其他元素的影响。 - 只在必要时才显示隐藏元素。
- 使用
window.requestAnimationFrame(),因为它可以把代码推迟到下一次重流时执行,而不是立即要求页面重流。 - 使用虚拟 DOM(virtual DOM)库。
3.JavaScript 引擎
JavaScript 引擎的主要作用是,读取网页中的 JavaScript 代码,对其处理后运行。
JavaScript 是一种解释型语言,也就是说,它不需要编译,由解释器实时运行。这样的好处是运行和修改都比较方便,刷新页面就可以重新解释;缺点是每次运行都要调用解释器,系统开销较大,运行速度慢于编译型语言。
为了提高运行速度,目前的浏览器都将 JavaScript 进行一定程度的编译,生成类似字节码(bytecode)的中间代码,以提高运行速度。
早期,浏览器内部对 JavaScript 的处理过程如下:
- 读取代码,进行词法分析(Lexical analysis),将代码分解成词元(token)。
- 对词元进行语法分析(parsing),将代码整理成“语法树”(syntax tree)。
- 使用“翻译器”(translator),将代码转为字节码(bytecode)。
- 使用“字节码解释器”(bytecode interpreter),将字节码转为机器码。
逐行解释将字节码转为机器码,是很低效的。为了提高运行速度,现代浏览器改为采用“即时编译”(Just In Time compiler,缩写 JIT),即字节码只在运行时编译,用到哪一行就编译哪一行,并且把编译结果缓存(inline cache)。通常,一个程序被经常用到的,只是其中一小部分代码,有了缓存的编译结果,整个程序的运行速度就会显著提升。
字节码不能直接运行,而是运行在一个虚拟机(Virtual Machine)之上,一般也把虚拟机称为 JavaScript 引擎。并非所有的 JavaScript 虚拟机运行时都有字节码,有的 JavaScript 虚拟机基于源码,即只要有可能,就通过 JIT(just in time)编译器直接把源码编译成机器码运行,省略字节码步骤。这一点与其他采用虚拟机(比如 Java)的语言不尽相同。这样做的目的,是为了尽可能地优化代码、提高性能。下面是目前最常见的一些 JavaScript 虚拟机:
- Chakra (Microsoft Internet Explorer)
- Nitro/JavaScript Core (Safari)
- Carakan (Opera)
- SpiderMonkey (Firefox)
- V8 (Chrome, Chromium)
2.window 对象
1.概述
浏览器里面,window对象(注意,w为小写)指当前的浏览器窗口。它也是当前页面的顶层对象,即最高一层的对象,所有其他对象都是它的下属。一个变量如果未声明,那么默认就是顶层对象的属性。
2.window 对象的属性
window.name属性是一个字符串,表示当前浏览器窗口的名字。窗口不一定需要名字,这个属性主要配合超链接和表单的target属性使用。
window.closed属性返回一个布尔值,表示窗口是否关闭。
window.opener属性表示打开当前窗口的父窗口。如果当前窗口没有父窗口(即直接在地址栏输入打开),则返回null。
window.self和window.window属性都指向窗口本身。这两个属性只读。
window.frames属性返回一个类似数组的对象,成员为页面内所有框架窗口,包括frame元素和iframe元素。window.frames[0]表示页面中第一个框架窗口。
window.length属性返回当前网页包含的框架总数。如果当前网页不包含frame和iframe元素,那么window.length就返回0。
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属性指向最顶层窗口,主要用于在框架窗口(frame)里面获取顶层窗口。
window.parent属性指向父窗口。如果当前窗口没有父窗口,window.parent指向自身。
window.status属性用于读写浏览器状态栏的文本。但是,现在很多浏览器都不允许改写状态栏文本,所以使用这个方法不一定有效。
window.devicePixelRatio属性返回一个数值,表示一个 CSS 像素的大小与一个物理像素的大小之间的比率。也就是说,它表示一个 CSS 像素由多少个物理像素组成。它可以用于判断用户的显示环境,如果这个比率较大,就表示用户正在使用高清屏幕,因此可以显示较大像素的图片。
组件属性
组件属性返回浏览器的组件对象。这样的属性有下面几个。
window.locationbar:地址栏对象window.menubar:菜单栏对象window.scrollbars:窗口的滚动条对象window.toolbar:工具栏对象window.statusbar:状态栏对象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。
3.window 对象的方法
window.open()
window.open方法用于新建另一个浏览器窗口,类似于浏览器菜单的新建窗口选项。它会返回新窗口的引用,如果无法新建窗口,则返回null。
window.open(url, windowName, [windowFeatures])
url:字符串,表示新窗口的网址。如果省略,默认网址就是about:blank。windowName:字符串,表示新窗口的名字。如果该名字的窗口已经存在,则占用该窗口,不再新建窗口。如果省略,就默认使用_blank,表示新建一个没有名字的窗口。另外还有几个预设值,_self表示当前窗口,_top表示顶层窗口,_parent表示上一层窗口。windowFeatures:字符串,内容为逗号分隔的键值对(详见下文),表示新窗口的参数,比如有没有提示栏、工具条等等。如果省略,则默认打开一个完整 UI 的新窗口。如果新建的是一个已经存在的窗口,则该参数不起作用,浏览器沿用以前窗口的参数。
第三个参数可以设定如下属性。
- 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)。
window.close()
window.close方法用于关闭当前窗口,一般只用来关闭window.open方法新建的窗口。
window.stop()
window.stop()方法完全等同于单击浏览器的停止按钮,会停止加载图像、视频等正在或等待加载的对象。
window.moveTo()方法用于移动浏览器窗口到指定位置。它接受两个参数,分别是窗口左上角距离屏幕左上角的水平距离和垂直距离,单位为像素。
window.moveBy方法将窗口移动到一个相对位置。它接受两个参数,分布是窗口左上角向右移动的水平距离和向下移动的垂直距离,单位为像素。
window.resizeTo()方法用于缩放窗口到指定大小
window.resizeBy()方法用于缩放窗口。它与window.resizeTo()的区别是,它按照相对的量缩放,window.resizeTo()需要给出缩放后的绝对大小。
注意:为了防止有人滥用这两个方法,随意移动用户的窗口,目前只有一种情况,浏览器允许用脚本移动窗口:该窗口是用window.open方法新建的,并且它所在的 Tab 页是当前窗口里面唯一的。除此以外的情况,使用上面两个方法都是无效的。
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.cancelAnimationFrame(),用来取消回调函数的执行。
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()取消回调函数。
4.事件
window.onerror和跨域相关的问题
需要注意的是,如果脚本网址与网页网址不在同一个域(比如使用了 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事件的监听函数。
3.Navigator 对象,Screen 对象。
1.Navigator 对象的属性
1.Navigator.userAgent
navigator.userAgent属性返回浏览器的 User Agent 字符串,表示浏览器的厂商和版本信息。
通过userAgent属性识别浏览器,不是一个好办法。因为必须考虑所有的情况(不同的浏览器,不同的版本),非常麻烦,而且用户可以改变这个字符串。这个字符串的格式并无统一规定,也无法保证未来的适用性,各种上网设备层出不穷,难以穷尽。所以,现在一般不再通过它识别浏览器了,而是使用“功能识别”方法,即逐一测试当前浏览器是否支持要用到的 JavaScript 功能。
总结:userAgent只能大概判断设备类型,还是靠插件稳
Navigator.platform属性返回用户的操作系统信息,比如MacIntel、Win32、Linux x86_64等 。
navigator.onLine属性返回一个布尔值,表示用户当前在线还是离线(浏览器断线)。
Navigator.onLine
navigator.onLine属性返回一个布尔值,表示用户当前在线还是离线(浏览器断线)。能否链接外网
用户变成在线会触发online事件,变成离线会触发offline事件,可以通过window.ononline和window.onoffline指定这两个事件的回调函数。
window.addEventListener('offline', function(e) { console.log('offline'); });
window.addEventListener('online', function(e) { console.log('online'); });
Navigator.language属性返回一个字符串,表示浏览器的首选语言。该属性只读。
Navigator.languages属性返回一个数组,表示用户可以接受的语言。Navigator.language总是这个数组的第一个成员。HTTP 请求头信息的Accept-Language字段,就来自这个数组。
Navigator.geolocation
Navigator.geolocation属性返回一个 Geolocation 对象,包含用户地理位置的信息。注意,该 API 只有在 HTTPS 协议下可用,否则调用下面方法时会报错。
Geolocation 对象提供下面三个方法。
- Geolocation.getCurrentPosition():得到用户的当前位置
- Geolocation.watchPosition():监听用户位置变化
- Geolocation.clearWatch():取消
watchPosition()方法指定的监听函数
2.Navigator 对象的方法
Navigator.javaEnabled()
Navigator.javaEnabled()方法返回一个布尔值,表示浏览器是否能运行 Java Applet 小程序。
Navigator.sendBeacon()
Navigator.sendBeacon()方法用于向服务器异步发送数据,详见《XMLHttpRequest 对象》一章。
3.Screen 对象
Screen 对象表示当前窗口所在的屏幕,提供显示设备的信息。window.screen属性指向这个对象。
该对象有下面的属性。
Screen.height:浏览器窗口所在的屏幕的高度(单位像素)。除非调整显示器的分辨率,否则这个值可以看作常量,不会发生变化。显示器的分辨率与浏览器设置无关,缩放网页并不会改变分辨率。Screen.width:浏览器窗口所在的屏幕的宽度(单位像素)。Screen.availHeight:浏览器窗口可用的屏幕高度(单位像素)。因为部分空间可能不可用,比如系统的任务栏或者 Mac 系统屏幕底部的 Dock 区,这个属性等于height减去那些被系统组件的高度。Screen.availWidth:浏览器窗口可用的屏幕宽度(单位像素)。Screen.pixelDepth:整数,表示屏幕的色彩位数,比如24表示屏幕提供24位色彩。Screen.colorDepth:Screen.pixelDepth的别名。严格地说,colorDepth 表示应用程序的颜色深度,pixelDepth 表示屏幕的颜色深度,绝大多数情况下,它们都是同一件事。Screen.orientation:返回一个对象,表示屏幕的方向。该对象的type属性是一个字符串,表示屏幕的具体方向,landscape-primary表示横放,landscape-secondary表示颠倒的横放,portrait-primary表示竖放,portrait-secondary。
下面是根据屏幕的宽度,将用户导向不同网页的代码。
if ((screen.width <= 800) && (screen.height <= 600)) {
window.location.replace('small.html');
} else {
window.location.replace('wide.html');
}
4.Cookie
1.概述
Cookie 是服务器保存在浏览器的一小段文本信息,每个 Cookie 的大小一般不能超过4KB。浏览器每次向服务器发出请求,就会自动附上这段信息。
Cookie 主要用来分辨两个请求是否来自同一个浏览器,以及用来保存一些状态信息。它的常用场合有以下一些。
- 对话(session)管理:保存登录、购物车等需要记录的信息。
- 个性化:保存用户的偏好,比如网页的字体大小、背景色等等。
- 追踪:记录和分析用户行为。
Cookie 包含以下几方面的信息。
- Cookie 的名字
- Cookie 的值(真正的数据写在这里面)
- 到期时间
- 所属域名(默认是当前域名)
- 生效的路径(默认是当前网址)
举例来说,用户访问网址www.example.com,服务器在浏览器写入一个 Cookie。这个 Cookie 就会包含www.example.com这个域名,以及根路径/。这意味着,这个 Cookie 对该域名的根路径和它的所有子路径都有效。如果路径设为/forums,那么这个 Cookie 只有在访问www.example.com/forums及其子路径时才有效。以后,浏览器一旦访问这个路径,浏览器就会附上这段 Cookie 发送给服务器。
浏览器的同源政策规定,两个网址只要域名相同和端口相同,就可以共享 Cookie(参见《同源政策》一章)。注意,这里不要求协议相同。也就是说,http://example.com设置的 Cookie,可以被https://example.com读取。
2.Cookie 与 HTTP 协议
Cookie 由 HTTP 协议生成,也主要是供 HTTP 协议使用。
总结:cookie是储存在浏览器的数据,可以由前后端共同维护;
5.XMLHttpRequest 对象
1.描述
具体来说,AJAX 包括以下几个步骤。
- 创建 XMLHttpRequest 实例
- 发出 HTTP 请求
- 接收服务器传回的数据
- 更新网页数据
例子
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);
2.XMLHttpRequest 的实例属性
1.XMLHttpRequest.readyState
XMLHttpRequest.readyState返回一个整数,表示实例对象的当前状态。该属性只读。它可能返回以下值。
- 0,表示 XMLHttpRequest 实例已经生成,但是实例的
open()方法还没有被调用。 - 1,表示
open()方法已经调用,但是实例的send()方法还没有调用,仍然可以使用实例的setRequestHeader()方法,设定 HTTP 请求的头信息。 - 2,表示实例的
send()方法已经调用,并且服务器返回的头信息和状态码已经收到。 - 3,表示正在接收服务器传来的数据体(body 部分)。这时,如果实例的
responseType属性等于text或者空字符串,responseText属性就会包含已经收到的部分信息。 - 4,表示服务器返回的数据已经完全接收,或者本次接收已经失败。
通信过程中,每当实例对象发生状态变化,它的readyState属性的值就会改变。这个值每一次变化,都会触发readyStateChange事件。
2.XMLHttpRequest.onreadystatechange
XMLHttpRequest.onreadystatechange属性指向一个监听函数。readystatechange事件发生时(实例的readyState属性变化),就会执行这个属性。
另外,如果使用实例的abort()方法,终止 XMLHttpRequest 请求,也会造成readyState属性变化,导致调用XMLHttpRequest.onreadystatechange属性。
3.XMLHttpRequest.response
XMLHttpRequest.response属性表示服务器返回的数据体(即 HTTP 回应的 body 部分)。
var xhr = new XMLHttpRequest();
xhr.onreadystatechange = function () {
if (xhr.readyState === 4) {
handler(xhr.response);
}
}
4.XMLHttpRequest.responseType
XMLHttpRequest.responseType属性是一个字符串,表示服务器返回数据的类型。这个属性是可写的,可以在调用open()方法之后、调用send()方法之前,设置这个属性的值,告诉服务器返回指定类型的数据。如果responseType设为空字符串,就等同于默认值text。
XMLHttpRequest.responseType属性可以等于以下值。
- ""(空字符串):等同于
text,表示服务器返回文本数据。 - "arraybuffer":ArrayBuffer 对象,表示服务器返回二进制数组。
- "blob":Blob 对象,表示服务器返回二进制对象。
- "document":Document 对象,表示服务器返回一个文档对象。
- "json":JSON 对象。
- "text":字符串。
5.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);
6.XMLHttpRequest.responseURL
XMLHttpRequest.responseURL属性是字符串,表示发送数据的服务器的网址。
7.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,服务器发生错误
8.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);
9.事件监听属性
XMLHttpRequest 对象可以对以下事件指定监听函数。
- XMLHttpRequest.onloadstart:loadstart 事件(HTTP 请求发出)的监听函数
- XMLHttpRequest.onprogress:progress事件(正在发送和加载数据)的监听函数
- XMLHttpRequest.onabort:abort 事件(请求中止,比如用户调用了
abort()方法)的监听函数 - XMLHttpRequest.onerror:error 事件(请求失败)的监听函数
- XMLHttpRequest.onload:load 事件(请求成功完成)的监听函数
- XMLHttpRequest.ontimeout:timeout 事件(用户指定的时限超过了,请求还未完成)的监听函数
- XMLHttpRequest.onloadend:loadend 事件(请求完成,不管成功或失败)的监听函数
10.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属性不影响这一点。
11.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'}));
3.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);
防csrf例子
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,这样浏览器就不会去自动解析,从而我们就可以拿到原始文本了。
XMLHttpRequest.getResponseHeader()
XMLHttpRequest.getResponseHeader()方法返回 HTTP 头信息指定字段的值,如果还没有收到服务器回应或者指定字段不存在,返回null。该方法的参数不区分大小写。
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。
4. XMLHttpRequest 实例的事件
...
5.Navigator.sendBeacon()
户卸载网页的时候,有时需要向服务器发一些数据。很自然的做法是在unload事件或beforeunload事件的监听函数里面,使用XMLHttpRequest对象发送数据。但是,这样做不是很可靠,因为XMLHttpRequest对象是异步发送,很可能在它即将发送的时候,页面已经卸载了,从而导致发送取消或者发送失败。
window.addEventListener('unload', logData, false);
function logData() {
navigator.sendBeacon('/log', analyticsData);
}
Navigator.sendBeacon方法接受两个参数,第一个参数是目标服务器的 URL,第二个参数是所要发送的数据(可选),可以是任意类型(字符串、表单对象、二进制对象等等)。
navigator.sendBeacon(url, data)
这个方法的返回值是一个布尔值,成功发送数据为true,否则为false。
该方法发送数据的 HTTP 方法是 POST,可以跨域,类似于表单提交数据。它不能指定回调函数。
下面是一个例子。
// HTML 代码如下
// <body onload="analytics('start')" onunload="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);
}
总结:在浏览器上运行ajax,不受刷新影响
6.同源限制
浏览器安全的基石是“同源政策”(same-origin policy)。很多开发者都知道这一点,但了解得不全面。
1.概述
1.含义
- 协议相同
- 域名相同
- 端口相同
目的
同源政策的目的,是为了保证用户信息的安全,防止恶意的网站窃取数据。
设想这样一种情况: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()
虽然这些限制是必要的,但是有时很不方便,合理的用途也受到影响。下面介绍如何规避上面的限制。
2.Cookie
Cookie 是服务器写入浏览器的一小段信息,只有同源的网页才能共享。如果两个网页一级域名相同,只是次级域名不同,浏览器允许通过设置document.domain共享 Cookie。
注意,A 和 B 两个网页都需要设置document.domain属性,才能达到同源的目的。因为设置document.domain的同时,会把端口重置为null,因此如果只设置一个网页的document.domain,会导致两个网址的端口不同,还是达不到同源的目的。
注意,这种方法只适用于 Cookie 和 iframe 窗口,LocalStorage 和 IndexedDB 无法通过这种方法,规避同源政策,而要使用下文介绍 PostMessage API。
另外,服务器也可以在设置 Cookie 的时候,指定 Cookie 的所属域名为一级域名,比如.example.com。
Set-Cookie: key=value; domain=.example.com; path=/
这样的话,二级域名和三级域名不用做任何设置,都可以读取这个 Cookie。
3.iframe 和多窗口通信
iframe元素可以在当前网页之中,嵌入其他网页。每个iframe元素形成自己的窗口,即有自己的window对象。iframe窗口之中的脚本,可以获得父窗口和子窗口。但是,只有在同源的情况下,父窗口和子窗口才能通信;如果跨域,就无法拿到对方的 DOM。
对于完全不同源的网站,目前有两种方法,可以解决跨域窗口的通信问题。
- 片段识别符(fragment identifier)
- 跨文档通信API(Cross-document messaging)
3.1片段识别符
片段标识符(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;
3.2window.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,提供以下三个属性。
4.AJAX
同源政策规定,AJAX 请求只能发给同源的网址,否则就报错。
除了架设服务器代理(浏览器请求同源服务器,再由后者请求外部服务),有三种方法规避这个限制。
- JSONP
- WebSocket
- CORS
4.1JSONP
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);
};
4.2WebSocket
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
4.3CORS
CORS 是跨源资源分享(Cross-Origin Resource Sharing)的缩写。它是 W3C 标准,属于跨源 AJAX 请求的根本解决方法。相比 JSONP 只能发GET请求,CORS 允许任何类型的请求。
下一章将详细介绍,如何通过 CORS 完成跨源 AJAX 请求。
7.CORS 通信
CORS 是一个 W3C 标准,全称是“跨域资源共享”(Cross-origin resource sharing)。它允许浏览器向跨域的服务器,发出XMLHttpRequest请求,从而克服了 AJAX 只能同源使用的限制。
1.简介
CORS 需要浏览器和服务器同时支持。目前,所有浏览器都支持该功能。
整个 CORS 通信过程,都是浏览器自动完成,不需要用户参与。对于开发者来说,CORS 通信与普通的 AJAX 通信没有差别,代码完全一样。浏览器一旦发现 AJAX 请求跨域,就会自动添加一些附加的头信息,有时还会多出一次附加的请求,但用户不会有感知。因此,实现 CORS 通信的关键是服务器。只要服务器实现了 CORS 接口,就可以跨域通信。
2.两种请求
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 的限制。对于非简单请求,浏览器会采用新的处理方式。
3.简单请求
1.基本流程
对于简单请求,浏览器直接发出 CORS 请求。具体来说,就是在头信息之中,增加一个Origin字段。
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
(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字段的值。
2.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;
注意:有的浏览器默认将withCredentials属性设为true;这就需要重新设置为false
4.非简单请求
1.预检请求
非简单请求是那种对服务器提出特殊要求的请求,比如请求方法是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。
总结:Access-Control-Request-Method会自动添加,Access-Control-Request-Headers需要自己在ajax请求里添加
2.预检请求的回应
服务器收到“预检”请求以后,检查了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可以请求数据。该字段也可以设为星号,表示同意任意跨源请求。
如果服务器否定了“预检”请求,会返回一个正常的 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回调函数捕获。控制台会打印出如下的报错信息。
服务器回应的其他 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天),在此期间,不用发出另一条预检请求。
3.浏览器的正常请求和回应
一旦服务器通过了“预检”请求,以后每次浏览器正常的 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字段是每次回应都必定包含的。
8.Storage 接口
1.概述
Storage 接口用于脚本在浏览器保存数据。两个对象部署了这个接口:window.sessionStorage和window.localStorage。
sessionStorage保存的数据用于浏览器的一次会话(session),当会话结束(通常是窗口关闭),数据被清空;localStorage保存的数据长期存在,下一次访问该网站的时候,网页可以直接读取以前保存的数据。除了保存期限的长短不同,这两个对象的其他方面都一致。
保存的数据都以“键值对”的形式存在。也就是说,每一项数据都有一个键名和对应的值。所有的数据都是以文本格式保存。
这个接口很像 Cookie 的强化版,能够使用大得多的存储空间。目前,每个域名的存储上限视浏览器而定,Chrome 是 2.5MB,Firefox 和 Opera 是 5MB,IE 是 10MB。其中,Firefox 的存储空间由一级域名决定,而其他浏览器没有这个限制。也就是说,Firefox 中,a.example.com和b.example.com共享 5MB 的存储空间。另外,与 Cookie 一样,它们也受同域限制。某个网页存入的数据,只有同域下的网页才能读取,如果跨域操作会报错。
2.属性和方法
属性
Storage.length:返回保存的数据项个数。
方法
Storage.setItem()方法用于存入数据。它接受两个参数,第一个是键名,第二个是保存的数据。如果键名已经存在,该方法会更新已有的键值。该方法没有返回值。
window.sessionStorage.setItem('key', 'value');
window.localStorage.setItem('key', 'value');
注意,Storage.setItem()两个参数都是字符串。如果不是字符串,会自动转成字符串,再存入浏览器。
Storage.getItem()方法用于读取数据。它只有一个参数,就是键名。如果键名不存在,该方法返回null。
Storage.clear()方法用于清除所有保存的数据。该方法的返回值是undefined。
Storage.key()接受一个整数作为参数(从零开始),返回该位置对应的键值。
3.storage 事件
Storage 接口储存的数据发生变化时,会触发 storage 事件,可以指定这个事件的监听函数。
window.addEventListener('storage', onStorageChange);
监听函数接受一个event实例对象作为参数。这个实例对象继承了 StorageEvent 接口,有几个特有的属性,都是只读属性。
StorageEvent.key:字符串,表示发生变动的键名。如果 storage 事件是由clear()方法引起,该属性返回null。StorageEvent.newValue:字符串,表示新的键值。如果 storage 事件是由clear()方法或删除该键值对引发的,该属性返回null。StorageEvent.oldValue:字符串,表示旧的键值。如果该键值对是新增的,该属性返回null。StorageEvent.storageArea:对象,返回键值对所在的整个对象。也说是说,可以从这个属性上面拿到当前域名储存的所有键值对。StorageEvent.url:字符串,表示原始触发 storage 事件的那个网页的网址。
注意,该事件有一个很特别的地方,就是它不在导致数据变化的当前页面触发,而是在同一个域名的其他窗口触发。也就是说,如果浏览器只打开一个窗口,可能观察不到这个事件。比如同时打开多个窗口,当其中的一个窗口导致储存的数据发生改变时,只有在其他窗口才能观察到监听函数的执行。可以通过这种机制,实现多个窗口之间的通信。
9.History 对象
1.概述
window.history属性指向 History 对象,它表示当前窗口的浏览历史。
2.属性
History.length:当前窗口访问过的网址数量(包括当前网页)History.state:History 堆栈最上层的状态值(详见下文)
3.方法
1.History.back()、History.forward()、History.go()
History.back():移动到上一个网址,等同于点击浏览器的后退键。对于第一个访问的网址,该方法无效果。
History.forward():移动到下一个网址,等同于点击浏览器的前进键。对于最后一个访问的网址,该方法无效果。
History.go():接受一个整数作为参数,以当前网址为基准,移动到参数指定的网址,比如go(1)相当于forward(),go(-1)相当于back()。如果参数超过实际存在的网址范围,该方法无效果;如果不指定参数,默认参数为0,相当于刷新当前页面。
2.History.pushState()
History.pushState()方法用于在历史中添加一条记录。
window.history.pushState(state, title, url)
该方法接受三个参数,依次为:
state:一个与添加的记录相关联的状态对象,主要用于popstate事件。该事件触发时,该对象会传入回调函数。也就是说,浏览器会将这个对象序列化以后保留在本地,重新载入这个页面的时候,可以拿到这个对象。如果不需要这个对象,此处可以填null。title:新页面的标题。但是,现在所有浏览器都忽视这个参数,所以这里可以填空字符串。url:新的网址,必须与当前页面处在同一个域。浏览器的地址栏将显示这个网址。
var stateObj = { foo: 'bar' };
history.pushState(stateObj, 'page 2', '2.html');
总结:会添加一条历史记录,并会设置到当前地址栏,但是不会跳转到这个历史记录的地址
3.History.replaceState()
History.replaceState()方法用来修改 History 对象的当前记录,其他都与pushState()方法一模一样。
假定当前网页是example.com/example.html。
history.pushState({page: 1}, 'title 1', '?page=1')
// URL 显示为 http://example.com/example.html?page=1
history.pushState({page: 2}, 'title 2', '?page=2');
// URL 显示为 http://example.com/example.html?page=2
history.replaceState({page: 3}, 'title 3', '?page=3');
// URL 显示为 http://example.com/example.html?page=3
history.back()
// URL 显示为 http://example.com/example.html?page=1
history.back()
// URL 显示为 http://example.com/example.html
history.go(2)
// URL 显示为 http://example.com/example.html?page=3
4.popstate 事件
每当同一个文档的浏览历史(即history对象)出现变化时,就会触发popstate事件。
注意,仅仅调用pushState()方法或replaceState()方法 ,并不会触发该事件,只有用户点击浏览器倒退按钮和前进按钮,或者使用 JavaScript 调用History.back()、History.forward()、History.go()方法时才会触发。另外,该事件只针对同一个文档,如果浏览历史的切换,导致加载不同的文档,该事件也不会触发。
使用的时候,可以为popstate事件指定回调函数。
window.onpopstate = function (event) {
console.log('location: ' + document.location);
console.log('state: ' + JSON.stringify(event.state));
};
// 或者
window.addEventListener('popstate', function(event) {
console.log('location: ' + document.location);
console.log('state: ' + JSON.stringify(event.state));
});
回调函数的参数是一个event事件对象,它的state属性指向pushState和replaceState方法为当前 URL 所提供的状态对象(即这两个方法的第一个参数)。上面代码中的event.state,就是通过pushState和replaceState方法,为当前 URL 绑定的state对象。
这个state对象也可以直接通过history对象读取。
var currentState = history.state;
注意,页面第一次加载的时候,浏览器不会触发popstate事件。
10.Location 对象,URL 对象,URLSearchParams 对象
URL 是互联网的基础设施之一。浏览器提供了一些原生对象,用来管理 URL。
1.Location 对象
Location对象是浏览器提供的原生对象,提供 URL 相关的信息和操作方法。通过window.location和document.location属性,可以拿到这个对象。
1.属性
Location对象提供以下属性。
Location.href:整个 URL。Location.protocol:当前 URL 的协议,包括冒号(:)。Location.host:主机,包括冒号(:)和端口(默认的80端口和443端口会省略)。Location.hostname:主机名,不包括端口。Location.port:端口号。Location.pathname:URL 的路径部分,从根路径/开始。Location.search:查询字符串部分,从问号?开始。Location.hash:片段字符串部分,从#开始。Location.username:域名前面的用户名。Location.password:域名前面的密码。Location.origin:URL 的协议、主机名和端口。
直接改写location,相当于写入href属性。
document.location = 'http://www.example.com';
// 等同于
document.location.href = 'http://www.example.com';
另外,Location.href属性是浏览器唯一允许跨域写入的属性,即非同源的窗口可以改写另一个窗口(比如子窗口与父窗口)的Location.href属性,导致后者的网址跳转。Location的其他属性都不允许跨域写入。
2.方法
Location.assign()
assign方法接受一个 URL 字符串作为参数,使得浏览器立刻跳转到新的 URL。如果参数不是有效的 URL 字符串,则会报错。
// 跳转到新的网址
document.location.assign('http://www.example.com')
Location.replace()
replace方法接受一个 URL 字符串作为参数,使得浏览器立刻跳转到新的 URL。如果参数不是有效的 URL 字符串,则会报错。
它与assign方法的差异在于,replace会在浏览器的浏览历史History里面删除当前网址,也就是说,一旦使用了该方法,后退按钮就无法回到当前网页了,相当于在浏览历史里面,使用新的 URL 替换了老的 URL。它的一个应用是,当脚本发现当前是移动设备时,就立刻跳转到移动版网页。
Location.reload()
reload方法使得浏览器重新加载当前网址,相当于按下浏览器的刷新按钮。
它接受一个布尔值作为参数。如果参数为true,浏览器将向服务器重新请求这个网页,并且重新加载后,网页将滚动到头部(即scrollTop === 0)。如果参数是false或为空,浏览器将从本地缓存重新加载该网页,并且重新加载后,网页的视口位置是重新加载前的位置。
Location.toString()
toString方法返回整个 URL 字符串,相当于读取Location.href属性。
2.URL 的编码和解码
网页的 URL 只能包含合法的字符。合法字符分成两类。
- URL 元字符:分号(
;),逗号(,),斜杠(/),问号(?),冒号(:),at(@),&,等号(=),加号(+),美元符号($),井号(#) - 语义字符:
a-z,A-Z,0-9,连词号(-),下划线(_),点(.),感叹号(!),波浪线(~),星号(*),单引号('),圆括号(())
除了以上字符,其他字符出现在 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')
// "春节"
3.URL 对象
URL对象是浏览器的原生对象,可以用来构造、解析和编码 URL。一般情况下,通过window.URL可以拿到这个对象。
元素和元素都部署了这个接口。这就是说,它们的 DOM 节点对象可以使用 URL 的实例属性和方法。
1.构造函数
URL对象本身是一个构造函数,可以生成 URL 实例。
它接受一个表示 URL 的字符串作为参数。如果参数不是合法的 URL,会报错。
var url = new URL('http://www.example.com/index.html');
url.href
// "http://www.example.com/index.html"
如果参数是另一个 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/"
2.实例属性
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:返回域名前面的用户名
3.静态方法
URL.createObjectURL()
URL.createObjectURL方法用来为上传/下载的文件、流媒体文件生成一个 URL 字符串。这个字符串代表了File对象或Blob对象的 URL。
// HTML 代码如下
// <div id="display"/>
// <input
// type="file"
// id="fileElem"
// multiple
// accept="image/*"
// onchange="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 实例。
4.URLSearchParams 对象
1.概述
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实例作为表单数据。
toString方法返回实例的字符串形式。
append方法用来追加一个查询参数。它接受两个参数,第一个为键名,第二个为键值,没有返回值。
delete方法用来删除指定的查询参数。它接受键名作为参数。
has方法返回一个布尔值,表示查询字符串是否包含指定的键名。
set方法用来设置查询字符串的键值。
get方法用来读取查询字符串里面的指定键。它接受键名作为参数。
var params = new URLSearchParams('?foo=1');
params.get('foo') // "1"
params.get('bar') // null
getAll方法返回一个数组,成员是指定键的所有键值。它接受键名作为参数。
var params = new URLSearchParams('?foo=1&foo=2');
params.getAll('foo') // ["1", "2"]
sort方法对查询字符串里面的键进行排序,规则是按照 Unicode 码点从小到大排列。
该方法没有返回值,或者说返回值是undefined。
这三个方法都返回一个遍历器对象,供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()) {}
11.ArrayBuffer 对象,Blob 对象
1.ArrayBuffer 对象
ArrayBuffer 对象表示一段二进制数据,用来模拟内存里面的数据。通过这个对象,JavaScript 可以读写二进制数据。这个对象可以看作内存数据的表达。
浏览器原生提供ArrayBuffer()构造函数,用来生成实例。它接受一个整数作为参数,表示这段二进制数据占用多少个字节。
var buffer = new ArrayBuffer(8);
上面代码中,实例对象buffer占用8个字节。
ArrayBuffer 对象有实例属性byteLength,表示当前实例占用的内存长度(单位字节)。
var buffer = new ArrayBuffer(8);
buffer.byteLength // 8
ArrayBuffer 对象有实例方法slice(),用来复制一部分内存。它接受两个整数参数,分别表示复制的开始位置(从0开始)和结束位置(复制时不包括结束位置),如果省略第二个参数,则表示一直复制到结束。
var buf1 = new ArrayBuffer(8);
var buf2 = buf1.slice(0);
上面代码表示复制原来的实例。
2.Blob 对象
1.简介
Blob 对象表示一个二进制文件的数据内容,比如一个图片文件的内容就可以通过 Blob 对象读写。它通常用来读写文件,它的名字是 Binary Large Object (二进制大型对象)的缩写。它与 ArrayBuffer 的区别在于,它用于操作二进制文件,而 ArrayBuffer 用于操作内存。
浏览器原生提供Blob()构造函数,用来生成实例对象。
new Blob(array [, options])
Blob构造函数接受两个参数。第一个参数是数组,成员是字符串或二进制对象,表示新生成的Blob实例对象的内容;第二个参数是可选的,是一个配置对象,目前只有一个属性type,它的值是一个字符串,表示数据的 MIME 类型,默认是空字符串。
var htmlFragment = ['<a id="a"><b id="b">hey!</b></a>'];
var myBlob = new Blob(htmlFragment, {type : 'text/html'});
下面是另一个例子,Blob 保存 JSON 数据。
var obj = { hello: 'world' };
var blob = new Blob([ JSON.stringify(obj) ], {type : 'application/json'});
2.实例属性和实例方法
Blob具有两个实例属性size和type,分别返回数据的大小和类型。
myBlob.size // 32
myBlob.type // "text/html"
Blob具有一个实例方法slice,用来拷贝原来的数据,返回的也是一个Blob实例。
myBlob.slice(start,end, contentType)
slice方法有三个参数,都是可选的。它们依次是起始的字节位置(默认为0)、结束的字节位置(默认为size属性的值,该位置本身将不包含在拷贝的数据之中)、新实例的数据类型(默认为空字符串)。
3.获取文件信息
文件选择器<input type="file">用来让用户选取文件。出于安全考虑,浏览器不允许脚本自行设置这个控件的value属性,即文件必须是用户手动选取的,不能是脚本指定的。一旦用户选好了文件,脚本就可以读取这个文件。
文件选择器返回一个 FileList 对象,该对象是一个类似数组的成员,每个成员都是一个 File 实例对象。File 实例对象是一个特殊的 Blob 实例,增加了name和lastModifiedDate属性。
// HTML 代码如下
// <input type="file" accept="image/*" multiple onchange="fileinfo(this.files)"/>
function fileinfo(files) {
for (var i = 0; i < files.length; i++) {
var f = files[i];
console.log(
f.name, // 文件名,不含路径
f.size, // 文件大小,Blob 实例属性
f.type, // 文件类型,Blob 实例属性
f.lastModifiedDate // 文件的最后修改时间
);
}
}
除了文件选择器,拖放 API 的dataTransfer.files返回的也是一个FileList 对象,它的成员因此也是 File 实例对象。
4.下载文件
AJAX 请求时,如果指定responseType属性为blob,下载下来的就是一个 Blob 对象。
function getBlob(url, callback) {
var xhr = new XMLHttpRequest();
xhr.open('GET', url);
xhr.responseType = 'blob';
xhr.onload = function () {
callback(xhr.response);
}
xhr.send(null);
}
上面代码中,xhr.response拿到的就是一个 Blob 对象。
生成 URL
浏览器允许使用URL.createObjectURL()方法,针对 Blob 对象生成一个临时 URL,以便于某些 API 使用。这个 URL 以blob://开头,表明对应一个 Blob 对象,协议头后面是一个识别符,用来唯一对应内存里面的 Blob 对象。这一点与data://URL(URL 包含实际数据)和file://URL(本地文件系统里面的文件)都不一样。
var droptarget = document.getElementById('droptarget');
droptarget.ondrop = function (e) {
var files = e.dataTransfer.files;
for (var i = 0; i < files.length; i++) {
var type = files[i].type;
if (type.substring(0,6) !== 'image/')
continue;
var img = document.createElement('img');
img.src = URL.createObjectURL(files[i]);
img.onload = function () {
this.width = 100;
document.body.appendChild(this);
URL.revokeObjectURL(this.src);
}
}
}
上面代码通过为拖放的图片文件生成一个 URL,产生它们的缩略图,从而使得用户可以预览选择的文件。
浏览器处理 Blob URL 就跟普通的 URL 一样,如果 Blob 对象不存在,返回404状态码;如果跨域请求,返回403状态码。Blob URL 只对 GET 请求有效,如果请求成功,返回200状态码。由于 Blob URL 就是普通 URL,因此可以下载。
读取文件
取得 Blob 对象以后,可以通过FileReader对象,读取 Blob 对象的内容,即文件内容。
FileReader 对象提供四个方法,处理 Blob 对象。Blob 对象作为参数传入这些方法,然后以指定的格式返回。
FileReader.readAsText():返回文本,需要指定文本编码,默认为 UTF-8。FileReader.readAsArrayBuffer():返回 ArrayBuffer 对象。FileReader.readAsDataURL():返回 Data URL。FileReader.readAsBinaryString():返回原始的二进制字符串。
下面是FileReader.readAsText()方法的例子,用来读取文本文件。
// HTML 代码如下
// <input type=’file' onchange='readfile(this.files[0])'></input>
// <pre id='output'></pre>
function readfile(f) {
var reader = new FileReader();
reader.readAsText(f);
reader.onload = function () {
var text = reader.result;
var out = document.getElementById('output');
out.innerHTML = '';
out.appendChild(document.createTextNode(text));
}
reader.onerror = function(e) {
console.log('Error', e);
};
}
上面代码中,通过指定 FileReader 实例对象的onload监听函数,在实例的result属性上拿到文件内容。
下面是FileReader.readAsArrayBuffer()方法的例子,用于读取二进制文件。
// HTML 代码如下
// <input type="file" onchange="typefile(this.files[0])"></input>
function typefile(file) {
// 文件开头的四个字节,生成一个 Blob 对象
var slice = file.slice(0, 4);
var reader = new FileReader();
// 读取这四个字节
reader.readAsArrayBuffer(slice);
reader.onload = function (e) {
var buffer = reader.result;
// 将这四个字节的内容,视作一个32位整数
var view = new DataView(buffer);
var magic = view.getUint32(0, false);
// 根据文件的前四个字节,判断它的类型
switch(magic) {
case 0x89504E47: file.verified_type = 'image/png'; break;
case 0x47494638: file.verified_type = 'image/gif'; break;
case 0x25504446: file.verified_type = 'application/pdf'; break;
case 0x504b0304: file.verified_type = 'application/zip'; break;
}
console.log(file.name, file.verified_type);
};
}
12.File 对象,FileList 对象,FileReader 对象
1.File 对象 #
File 对象代表一个文件,用来读写文件信息。它继承了 Blob 对象,或者说是一种特殊的 Blob 对象,所有可以使用 Blob 对象的场合都可以使用它。
1.构造函数
浏览器原生提供一个File()构造函数,用来生成 File 实例对象。
new File(array, name [, options])
File()构造函数接受三个参数。
- array:一个数组,成员可以是二进制对象或字符串,表示文件的内容。
- name:字符串,表示文件名或文件路径。
- options:配置对象,设置实例的属性。该参数可选。
第三个参数配置对象,可以设置两个属性。
- type:字符串,表示实例对象的 MIME 类型,默认值为空字符串。
- lastModified:时间戳,表示上次修改的时间,默认为
Date.now()。
下面是一个例子。
var file = new File(
['foo'],
'foo.txt',
{
type: 'text/plain',
}
);
实例属性和实例方法
File 对象有以下实例属性。
- File.lastModified:最后修改时间
- File.name:文件名或文件路径
- File.size:文件大小(单位字节)
- File.type:文件的 MIME 类型
2.FileList 对象
FileList对象是一个类似数组的对象,代表一组选中的文件,每个成员都是一个 File 实例。它主要出现在两个场合。
- 文件控件节点(
<input type="file">)的files属性,返回一个 FileList 实例。 - 拖拉一组文件时,目标区的
DataTransfer.files属性,返回一个 FileList 实例。
// HTML 代码如下
// <input id="fileItem" type="file">
var files = document.getElementById('fileItem').files;
files instanceof FileList // true
3.FileReader 对象
FileReader 对象用于读取 File 对象或 Blob 对象所包含的文件内容。
浏览器原生提供一个FileReader构造函数,用来生成 FileReader 实例。
var reader = new FileReader();
FileReader 有以下的实例属性。
- FileReader.error:读取文件时产生的错误对象
- FileReader.readyState:整数,表示读取文件时的当前状态。一共有三种可能的状态,
0表示尚未加载任何数据,1表示数据正在加载,2表示加载完成。 - FileReader.result:读取完成后的文件内容,有可能是字符串,也可能是一个 ArrayBuffer 实例。
- FileReader.onabort:
abort事件(用户终止读取操作)的监听函数。 - FileReader.onerror:
error事件(读取错误)的监听函数。 - FileReader.onload:
load事件(读取操作完成)的监听函数,通常在这个函数里面使用result属性,拿到文件内容。 - FileReader.onloadstart:
loadstart事件(读取操作开始)的监听函数。 - FileReader.onloadend:
loadend事件(读取操作结束)的监听函数。 - FileReader.onprogress:
progress事件(读取操作进行中)的监听函数。
下面是监听load事件的一个例子。
// HTML 代码如下
// <input type="file" onchange="onChange(event)">
function onChange(event) {
var file = event.target.files[0];
var reader = new FileReader();
reader.onload = function (event) {
console.log(event.target.result)
};
reader.readAsText(file);
}
上面代码中,每当文件控件发生变化,就尝试读取第一个文件。如果读取成功(load事件发生),就打印出文件内容。
FileReader 有以下实例方法。
- FileReader.abort():终止读取操作,
readyState属性将变成2。 - FileReader.readAsArrayBuffer():以 ArrayBuffer 的格式读取文件,读取完成后
result属性将返回一个 ArrayBuffer 实例。 - FileReader.readAsBinaryString():读取完成后,
result属性将返回原始的二进制字符串。 - FileReader.readAsDataURL():读取完成后,
result属性将返回一个 Data URL 格式(Base64 编码)的字符串,代表文件内容。对于图片文件,这个字符串可以用于<img>元素的src属性。注意,这个字符串不能直接进行 Base64 解码,必须把前缀data:*/*;base64,从字符串里删除以后,再进行解码。 - FileReader.readAsText():读取完成后,
result属性将返回文件内容的文本字符串。该方法的第一个参数是代表文件的 Blob 实例,第二个参数是可选的,表示文本编码,默认为 UTF-8。
下面是一个例子。
/* HTML 代码如下
<input type="file" onchange="previewFile()">
<img src="" height="200">
*/
function previewFile() {
var preview = document.querySelector('img');
var file = document.querySelector('input[type=file]').files[0];
var reader = new FileReader();
reader.addEventListener('load', function () {
preview.src = reader.result;
}, false);
if (file) {
reader.readAsDataURL(file);
}
}
13.表单,FormData 对象
1.表单概述
表单(<form>)用来收集用户提交的数据,发送到服务器。比如,用户提交用户名和密码,让服务器验证,就要通过表单。表单提供多种控件,让开发者使用,具体的控件种类和用法请参考 HTML 语言的教程。本章主要介绍 JavaScript 与表单的交互。
点击submit控件,就可以提交表单。
<form>
<input type="submit" value="提交">
</form>
上面表单就包含一个submit控件,点击这个控件,浏览器就会把表单数据向服务器提交。
注意,表单里面的<button>元素如果没有用type属性指定类型,那么默认就是submit控件。
除了点击submit控件提交表单,还可以用表单元素的submit()方法,通过脚本提交表单。
formElement.submit();
表单元素的reset()方法可以重置所有控件的值(重置为默认值)。
formElement.reset()
2.FormData 对象
1.概述
表单数据以键值对的形式向服务器发送,这个过程是浏览器自动完成的。但是有时候,我们希望通过脚本完成过程,构造和编辑表单键值对,然后通过XMLHttpRequest.send()方法发送。浏览器原生提供了 FormData 对象来完成这项工作。
FormData 首先是一个构造函数,用来生成实例。
var formdata = new FormData(form);
FormData()构造函数的参数是一个表单元素,这个参数是可选的。如果省略参数,就表示一个空的表单,否则就会处理表单元素里面的键值对。
2.实例方法
FormData 提供以下实例方法。
FormData.get(key):获取指定键名对应的键值,参数为键名。如果有多个同名的键值对,则返回第一个键值对的键值。FormData.getAll(key):返回一个数组,表示指定键名对应的所有键值。如果有多个同名的键值对,数组会包含所有的键值。FormData.set(key, value):设置指定键名的键值,参数为键名。如果键名不存在,会添加这个键值对,否则会更新指定键名的键值。如果第二个参数是文件,还可以使用第三个参数,表示文件名。FormData.delete(key):删除一个键值对,参数为键名。FormData.append(key, value):添加一个键值对。如果键名重复,则会生成两个相同键名的键值对。如果第二个参数是文件,还可以使用第三个参数,表示文件名。FormData.has(key):返回一个布尔值,表示是否具有该键名的键值对。FormData.keys():返回一个遍历器对象,用于for...of循环遍历所有的键名。FormData.values():返回一个遍历器对象,用于for...of循环遍历所有的键值。FormData.entries():返回一个遍历器对象,用于for...of循环遍历所有的键值对。如果直接用for...of循环遍历 FormData 实例,默认就会调用这个方法。
下面是get()、getAll()、set()、append()方法的例子。
var formData = new FormData();
formData.set('username', '张三');
formData.append('username', '李四');
formData.get('username') // "张三"
formData.getAll('username') // ["张三", "李四"]
formData.append('userpic[]', myFileInput.files[0], 'user1.jpg');
formData.append('userpic[]', myFileInput.files[1], 'user2.jpg');
下面是遍历器的例子。
var formData = new FormData();
formData.append('key1', 'value1');
formData.append('key2', 'value2');
for (var key of formData.keys()) {
console.log(key);
}
// "key1"
// "key2"
for (var value of formData.values()) {
console.log(value);
}
// "value1"
// "value2"
for (var pair of formData.entries()) {
console.log(pair[0] + ': ' + pair[1]);
}
// key1: value1
// key2: value2
// 等同于遍历 formData.entries()
for (var pair of formData) {
console.log(pair[0] + ': ' + pair[1]);
}
// key1: value1
// key2: value2
3.表单的内置验证
1.自动校验
表单提交的时候,浏览器允许开发者指定一些条件,它会自动验证各个表单控件的值是否符合条件。
<!-- 必填 -->
<input required>
<!-- 必须符合正则表达式 -->
<input pattern="banana|cherry">
<!-- 字符串长度必须为6个字符 -->
<input minlength="6" maxlength="6">
<!-- 数值必须在1到10之间 -->
<input type="number" min="1" max="10">
<!-- 必须填入 Email 地址 -->
<input type="email">
<!-- 必须填入 URL -->
<input type="URL">
如果一个控件通过验证,它就会匹配:valid的 CSS 伪类,浏览器会继续进行表单提交的流程。如果没有通过验证,该控件就会匹配:invalid的 CSS 伪类,浏览器会终止表单提交,并显示一个错误信息。
2.checkValidity()
除了提交表单的时候,浏览器自动校验表单,还可以手动触发表单的校验。表单元素和表单控件都有checkValidity()方法,用于手动触发校验。
// 触发整个表单的校验
form.checkValidity()
// 触发单个表单控件的校验
formControl.checkValidity()
checkValidity()方法返回一个布尔值,true表示通过校验,false表示没有通过校验。因此,提交表单可以封装为下面的函数。
function submitForm(action) {
var form = document.getElementById('form');
form.action = action;
if (form.checkValidity()) {
form.submit();
}
}
3.willValidate 属性
控件元素的willValidate属性是一个布尔值,表示该控件是否会在提交时进行校验。(只读)
4.validationMessage 属性
控件元素的validationMessage属性返回一个字符串,表示控件不满足校验条件时,浏览器显示的提示文本。以下两种情况,该属性返回空字符串。
- 该控件不会在提交时自动校验
- 该控件满足校验条件
// HTML 代码如下
// <form><input type="text" required></form>
document.querySelector('form input').validationMessage
// "请填写此字段。"
下面是另一个例子。
var myInput = document.getElementById('myinput');
if (!myInput.checkValidity()) {
document.getElementById('prompt').innerHTML = myInput.validationMessage;
}
5.setCustomValidity()
控件元素的setCustomValidity()方法用来定制校验失败时的报错信息。它接受一个字符串作为参数,该字符串就是定制的报错信息。如果参数为空字符串,则上次设置的报错信息被清除。
如果调用这个方法,并且参数不为空字符串,浏览器就会认为控件没有通过校验,就会立刻显示该方法设置的报错信息。
/* HTML 代码如下
<form>
<p><input type="file" id="fs"></p>
<p><input type="submit"></p>
</form>
*/
document.getElementById('fs').onchange = checkFileSize;
function checkFileSize() {
var fs = document.getElementById('fs');
var files = fs.files;
if (files.length > 0) {
if (files[0].size > 75 * 1024) {
fs.setCustomValidity('文件不能大于 75KB');
return;
}
}
fs.setCustomValidity('');
}
5.validity 属性
控件元素的属性validity属性返回一个ValidityState对象,包含当前校验状态的信息。
该对象有以下属性,全部为只读属性。
ValidityState.badInput:布尔值,表示浏览器是否不能将用户的输入转换成正确的类型,比如用户在数值框里面输入字符串。ValidityState.customError:布尔值,表示是否已经调用setCustomValidity()方法,将校验信息设置为一个非空字符串。ValidityState.patternMismatch:布尔值,表示用户输入的值是否不满足模式的要求。ValidityState.rangeOverflow:布尔值,表示用户输入的值是否大于最大范围。ValidityState.rangeUnderflow:布尔值,表示用户输入的值是否小于最小范围。ValidityState.stepMismatch:布尔值,表示用户输入的值不符合步长的设置(即不能被步长值整除)。ValidityState.tooLong:布尔值,表示用户输入的字数超出了最长字数。ValidityState.tooShort:布尔值,表示用户输入的字符少于最短字数。ValidityState.typeMismatch:布尔值,表示用户填入的值不符合类型要求(主要是类型为 Email 或 URL 的情况)。ValidityState.valid:布尔值,表示用户是否满足所有校验条件。ValidityState.valueMissing:布尔值,表示用户没有填入必填的值。
6.validity 属性
控件元素的属性validity属性返回一个ValidityState对象,包含当前校验状态的信息。
该对象有以下属性,全部为只读属性。
ValidityState.badInput:布尔值,表示浏览器是否不能将用户的输入转换成正确的类型,比如用户在数值框里面输入字符串。ValidityState.customError:布尔值,表示是否已经调用setCustomValidity()方法,将校验信息设置为一个非空字符串。ValidityState.patternMismatch:布尔值,表示用户输入的值是否不满足模式的要求。ValidityState.rangeOverflow:布尔值,表示用户输入的值是否大于最大范围。ValidityState.rangeUnderflow:布尔值,表示用户输入的值是否小于最小范围。ValidityState.stepMismatch:布尔值,表示用户输入的值不符合步长的设置(即不能被步长值整除)。ValidityState.tooLong:布尔值,表示用户输入的字数超出了最长字数。ValidityState.tooShort:布尔值,表示用户输入的字符少于最短字数。ValidityState.typeMismatch:布尔值,表示用户填入的值不符合类型要求(主要是类型为 Email 或 URL 的情况)。ValidityState.valid:布尔值,表示用户是否满足所有校验条件。ValidityState.valueMissing:布尔值,表示用户没有填入必填的值。
下面是一个例子。
var input = document.getElementById('myinput');
if (input.validity.valid) {
console.log('通过校验');
} else {
console.log('校验失败');
}
下面是另外一个例子。
var txt = '';
if (document.getElementById('myInput').validity.rangeOverflow) {
txt = '数值超过上限';
}
document.getElementById('prompt').innerHTML = txt;
7.表单的 novalidate 属性
表单元素的 HTML 属性novalidate,可以关闭浏览器的自动校验。
<form novalidate>
</form>
这个属性也可以在脚本里设置。
form.noValidate = true;
如果表单元素没有设置novalidate属性,那么提交按钮(<button>或<input>元素)的formnovalidate属性也有同样的作用。
<form>
<input type="submit" value="submit" formnovalidate>
</form>
4.enctype 属性
表单能够用四种编码,向服务器发送数据。编码格式由表单的enctype属性决定
1.GET 方法
如果表单使用GET方法发送数据,enctype属性无效。
2.application/x-www-form-urlencoded
如果表单用POST方法发送数据,并省略enctype属性,那么数据以application/x-www-form-urlencoded格式发送(因为这是默认值)。
3.text/plain
如果表单使用POST方法发送数据,enctype属性为text/plain,那么数据将以纯文本格式发送。
4.multipart/form-data
如果表单使用POST方法,enctype属性为multipart/form-data,那么数据将以混合的格式发送。
5.文件上传
用户上传文件,也是通过表单。具体来说,就是通过文件输入框选择本地文件,提交表单的时候,浏览器就会把这个文件发送到服务器。
<input type="file" id="file" name="myFile">
此外,还需要将表单<form>元素的method属性设为POST,enctype属性设为multipart/form-data。其中,enctype属性决定了 HTTP 头信息的Content-Type字段的值,默认情况下这个字段的值是application/x-www-form-urlencoded,但是文件上传的时候要改成multipart/form-data。
file 控件的multiple属性,指定可以一次选择多个文件;如果没有这个属性,则一次只能选择一个文件。
新建一个 FormData 实例对象,模拟发送到服务器的表单数据,把选中的文件添加到这个对象上面。
var formData = new FormData();
for (var i = 0; i < files.length; i++) {
var file = files[i];
// 只上传图片文件
if (!file.type.match('image.*')) {
continue;
}
formData.append('photos[]', file, file.name);
}
最后,使用 Ajax 向服务器上传文件。
var xhr = new XMLHttpRequest();
xhr.open('POST', 'handler.php', true);
xhr.onload = function () {
if (xhr.status !== 200) {
console.log('An error occurred!');
}
};
xhr.send(formData);
除了发送 FormData 实例,也可以直接 AJAX 发送文件。
var file = document.getElementById('test-input').files[0];
var xhr = new XMLHttpRequest();
xhr.open('POST', 'myserver/uploads');
xhr.setRequestHeader('Content-Type', file.type);
xhr.send(file);
14.IndexedDB API
1.概述
随着浏览器的功能不断增强,越来越多的网站开始考虑,将大量数据储存在客户端,这样可以减少从服务器获取数据,直接从本地获取数据。
现有的浏览器数据储存方案,都不适合储存大量数据:Cookie 的大小不超过 4KB,且每次请求都会发送回服务器;LocalStorage 在 2.5MB 到 10MB 之间(各家浏览器不同),而且不提供搜索功能,不能建立自定义的索引。所以,需要一种新的解决方案,这就是 IndexedDB 诞生的背景。
通俗地说,IndexedDB 就是浏览器提供的本地数据库,它可以被网页脚本创建和操作。IndexedDB 允许储存大量数据,提供查找接口,还能建立索引。这些都是 LocalStorage 所不具备的。就数据库类型而言,IndexedDB 不属于关系型数据库(不支持 SQL 查询语句),更接近 NoSQL 数据库。
IndexedDB 具有以下特点。
(1)键值对储存。 IndexedDB 内部采用对象仓库(object store)存放数据。所有类型的数据都可以直接存入,包括 JavaScript 对象。对象仓库中,数据以“键值对”的形式保存,每一个数据记录都有对应的主键,主键是独一无二的,不能有重复,否则会抛出一个错误。
(2)异步。 IndexedDB 操作时不会锁死浏览器,用户依然可以进行其他操作,这与 LocalStorage 形成对比,后者的操作是同步的。异步设计是为了防止大量数据的读写,拖慢网页的表现。
(3)支持事务。 IndexedDB 支持事务(transaction),这意味着一系列操作步骤之中,只要有一步失败,整个事务就都取消,数据库回滚到事务发生之前的状态,不存在只改写一部分数据的情况。
(4)同源限制 IndexedDB 受到同源限制,每一个数据库对应创建它的域名。网页只能访问自身域名下的数据库,而不能访问跨域的数据库。
(5)储存空间大 IndexedDB 的储存空间比 LocalStorage 大得多,一般来说不少于 250MB,甚至没有上限。
(6)支持二进制储存。 IndexedDB 不仅可以储存字符串,还可以储存二进制数据(ArrayBuffer 对象和 Blob 对象)。
2.基本概念
IndexedDB 是一个比较复杂的 API,涉及不少概念。它把不同的实体,抽象成一个个对象接口。学习这个 API,就是学习它的各种对象接口。
- 数据库:IDBDatabase 对象
- 对象仓库:IDBObjectStore 对象
- 索引: IDBIndex 对象
- 事务: IDBTransaction 对象
- 操作请求:IDBRequest 对象
- 指针: IDBCursor 对象
- 主键集合:IDBKeyRange 对象
下面是一些主要的概念。
(1)数据库
数据库是一系列相关数据的容器。每个域名(严格的说,是协议 + 域名 + 端口)都可以新建任意多个数据库。
IndexedDB 数据库有版本的概念。同一个时刻,只能有一个版本的数据库存在。如果要修改数据库结构(新增或删除表、索引或者主键),只能通过升级数据库版本完成。
(2)对象仓库
每个数据库包含若干个对象仓库(object store)。它类似于关系型数据库的表格。
(3)数据记录
对象仓库保存的是数据记录。每条记录类似于关系型数据库的行,但是只有主键和数据体两部分。主键用来建立默认的索引,必须是不同的,否则会报错。主键可以是数据记录里面的一个属性,也可以指定为一个递增的整数编号。
{ id: 1, text: 'foo' }
上面的对象中,id属性可以当作主键。
数据体可以是任意数据类型,不限于对象。
(4)索引
为了加速数据的检索,可以在对象仓库里面,为不同的属性建立索引。
(5)事务
数据记录的读写和删改,都要通过事务完成。事务对象提供error、abort和complete三个事件,用来监听操作结果。
具体在这里: