Ajax
(Asynchronous JavaScript and XML)中文名称:异步JavaScript与XML。由于其可以在不刷新网站页面的的情况下获取新的数据,且还支持同步和异步两种方式(使用方便),而被广泛应用于网站前端,Ajax
名称中的XML含义只因XML曾一度是互联网上存储和传输结构化数据的标准,其获取的数据格式不一定是XML。
Ajax
在Ajax出现之前,网站需要和后台交互惯用的方法是通过表单提交,在html文件中使用<form>
标签,通常一个请求就是一个表单提交。表单提交有一个特点就是,当用户点击‘submit’按钮后(表单提交的请求发出)浏览器就会刷新页面,当前网页不会接收请求的具体结果,然后在新页面里告诉你操作是成功了还是失败了。如果不幸由于网络太慢或者其他原因,就会得到一个404页面。
这就是Web的运作原理:一次HTTP请求对应一个页面。比如我们经常会遇到的场景,在登录页面,我们输入帐号和密码,点击登录,如果登录成功,页面就会跳转到网站首页或者个人信息页面,如果输入的帐号或者密码是错误的,通常页面会再次跳转到登录页面提示我们帐号错误或者密码错误,需要重新输入,重新登录。
而当我们有了Ajax
之后,我们就可以使用JavaScript发送请求,再由JavaScript更新DOM,用户体验是数据请求时仍然停留在当前页面,但是数据却可以不断地更新。比较典型的例子就是谷歌地图,打开谷歌地图后,我们可以用鼠标拖动地图,然后新的区域的地图在不需要页面刷新的情况下就可以展现出来。
通过以上对比我们可以看出有了Ajax
之后我们的网页的体验,和速度确实增幅不少。用JavaScript写一个完整的Ajax
代码并不复杂,在现代浏览器上写Ajax主要依靠XMLHttpRequest对象。
下面是一个DOM Level 0
风格的Ajax的代码,每行代码后面标注了其含义:
1 | // 新建XMLHttpRequest对象 |
xhr.readyState
属性有如下可能的值
- 0:未初始化(Uninitialized),尚未调用open()方法
- 1:已打开(Open)。已调用open()方法,尚未调用send()方法
- 2:已发送(Send)。已调用send()方法,尚未收到响应
- 3:接收中(Receiving)。已收到部分响应
- 4:完成(Complete)。已经收到了所有响应
对于低版本的IE,需要换一个ActiveXObject对象,其使用方法和XMLHttpRequest一般无二,在这里就不再多举例子。
XMLHttpRequest Level 2
XMLHttpRequest Level 1
只是把已存在的XHR对象的实现细节明确了一下,XMLHttpRequest Level 2
又进一步发展了XHR对象。
FormData类型
FormData类型便于表单序列化,也便于创建与表单类似格式的数据然后通过XHR发送。
1 | // 通过直接给FormData构造函数传入一个表单元素,也可以将表单中的数据作为键值对填充进去 |
使用FormData的另一个方便之处是不再需要给xhr设置任何请求头部了,xhr对象能够识别作为FormData实例传入的数据类型并自动配置响应头部。
超时
XMLHttpRequest Level 2
中规范了xhr对象增加一个timeout
属性和timeout
事件,给timeout
属性设置一个时间(单位ms),且在该时间过后没有收到响应时,xhr对象就会触发timeout
事件。
overrideMimeType()方法
XMLHttpRequest Level 2
中规范了xhr对象引入了 overrideMimeType()方法用于重写xhr响应的MIME类型,因为响应返回的MIME类型决定了xhr对象如何处理响应,所以覆盖了响应返回的类型及影响了响应的解析。假设服务器发送了XML数据,但响应头设置了MIME类型是text/plain
。结果会导致虽然数据是XML,但responseXML的属性值是null,调用overrideMimeType('text/xml')
可以保证响应当成XML而不是纯文本处理。
为了正确覆盖响应的MIME类型,必须在调用send()之前调用overrideMimeType()。
进度事件
Progress Events
是W3C的工作草案,定义了客户端-服务器端通信。这些事件最初只针对xhr,现在也推广到了其他类似API。有以下6个进度相关的事件。
- loadstart: 在接收到响应的第一个字节时触发
- progress:在接收响应期间反复触发
onprogress
事件处理程序都会收到event对象,其target属性是xhr对象,且包含3个额外属性- lengthComputable: 是一个布尔值,表示进度信息是否可用
- position:是接收到的字节数
- totalSize:是响应的Content-Length头部定义的总字节数
- 为了保证正确执行,必须在调用open()之前添加
onprogress
事件处理程序
- error:请求出错时触发
- abort:在调用abort()终止连接时触发
- load:在成功接收完响应时触发
- loadend:在通信完成时,且在error、abort或load之后触发
每次请求都会
- 首先触发loadstart事件
- 之后是一个或多个progress事件
- 接着是error、abort或load中的一个
- 最后是已loadend事件结束
跨域
Ajax
除了使用方便等优点外,也有一些限制,浏览器出于安全考虑有着同源策略,默认只能访问同源的资源。
同源是指两个页面拥有相同的协议(protocol),和主机(host),端口(port),那么这两个页面就属于同一个源(origin)。
完全不允许跨域访问对于web的发展也是一个严重的制约,大约有为以下几种可以实现跨域请求:
Flash
通过Flash插件发送HTTP请求,这种方式可以绕过浏览器的安全限制,但必须安装Flash,并且跟Flash交互。不过Flash用起来麻烦,目前已基本淘汰。
图像ping
图像ping
,我们知道,一个网页可以从任何网页中加载图像,没有跨域的限制,因此我们就可以动态的创建图像,使用它们的onload
和onerrer
事件处理程序来确定是否请求是否完成。
这种方式多用于与服务器进行简单、单向的跨域通信的一种方式(比如日志上报)。请求的数据是通过查询字符串形式发送的,而响应通常是一个位图图片,或者204的状态码,通过图像ping,浏览器得不到任何具体的数据。
1 | var img = new Image(); |
这里创建了一个image的实例,然后注册onload
和onerror
的监听,这样无论结果如何,只要请求完成,就能得到通知。当我们为img指定src开始,一个带着function=login参数的请求就被发了出去。
这种请求方式有两个主要的缺点,一是只能发送GET请求,二是无法访问响应文本。因此只能用于浏览器与服务器间的单向通信。再多分析一下,图像ping实际上是利用了标签可以执行跨域请求的功能,换句话说,是Dom有跨域请求的功能,想明白这一点估计也就能想到,是不是其他的一些需要引用资源的标签是否也可以实现同样的功能,答案是确定的。类似于<img>
还有<link>
, <video>
,<audio>
,<object>
,<embed>
,<applet>
,<iframe>
等。
JSONP
JSONP
是”JSON with padding”的简写,是web服务上流行的一种JSON变体。
以一个场景为例,比如我们需要获取用户的基本信息,假如我们需要获取用户的用户名,性别和年龄三个字段。
以接口为http:127.0.0.2:8888/userInfo
,我们需要的数据为{'name':'Ajax','sex':'female','age':'18'}
。
JSONP
实际上是利用了浏览器允许跨域引用JavaScript资源。 我们需要首先在页面中准备好回调函数:
1 | function handleCallback(data) { |
然后创建script标签,并将我们需要请求的url后面跟上请求参数callback
等于我们预先准备好的回调函数handleCallback
,并将script标签插入文档中。
1 | var js = document.createElement('script'); |
script标签插入文档后,浏览器就会如同加载js代码一样,以get
请求的方式去获取src所链接的资源,当后台收到请求后,可以得到请求参数callback的值handleCallback
,后台需要将数据包装到handleCallback()
中进行返回,看起来和JSON一样,只是被包在一个函数调用里,同时需要Content-type设置为application/javascript
如:
1 | handleCallback({'name':'Ajax','sex':'female','age':'18'}) |
当浏览器收到数据后会理所应当的认为返回数据是一段js代码,运行这段代码,恰好就是调用了我们前面预先准备好的函数handleCallback
,将我们真正需要传输的数据作为参数传入了回调函数。 这样就完成了跨域加载数据,也是因为JSONP
是利用了script标签可以跨域加载资源的这一特点,让请求通过加载脚本的方式进行的,而加载脚本只有get
请求一种方式,所以JSONP也只能用使用get请求,并且要求返回数据封装成JavaScript的调用。
CORS
跨资源共享(CORS)全称Cross-Origin Resource Sharing
,规范定义了浏览器与服务器应该如何实现跨源通信。CORS背后的基本思想,就是使用自定义的HTTP头部让浏览器与服务器进行沟通,从而决定请求或响应是应该成功,还是应该失败。
比如一个简单的使用GET或POST发送的请求,它没有自定义的头部,而主体内容是text/plain
。在发送该请求时,会给它附加一个额外的Origin头部,其中包含请求页面的源信息(协议、域名和端口),以便服务器根据这个头部信息来决定是否给予响应。下面是Origin头部的一个示例:
1 | Origin: http://www.nczonline.net |
如果服务器认为这个请求可以接受,就在Access-Control-Allow-Origin
头部中回发相同的源信息(如果是公共资源,可以回发*
)。例如:
1 | Access-Control-Allow-Origin: http://www.nczonline.net |
如果没有这个头部,或者有这个头部,但源信息不匹配,浏览器则不会响应浏览器请求。
出于安全考虑,跨域xhr对象存在一些限制
- 不能使用setRequestHeader() 设置自定义头部
- 请求和响应都不携带cookie
- getAllResponseHeaders()方法始终返回空字符串
预检请求
CORS通过一种叫预检请求(preflighted request)的服务器验证机制,允许使用自定义头部,在发送跨域请求时会向服务器发送一个”预检”请求,这个请求使用OPTIONS方法发送并包含以下头部
- Origin:与请求相同
- Access-Control-Request-Method: 请求希望使用的方法
- Access-Control-Request-Headers:要使用的自定义头部列表(可选,以逗号分隔)
预检请求发送后,服务器可以确定是否允许这种类型的请求。服务器通过在响应中发送如下头部与浏览器沟通这些信息
- Access-Control-Allow-Origin: 与请求头中的Origin相同
- Access-Control-Allow-Methods: 允许的方法(逗号分隔的列表)
- Access-Control-Allow-Headers: 服务器允许的头部(逗号分隔的列表)
- Access-Control-Max-Age: 预存预检请求的秒数
预检请求返回后,结果会按照响应中指定的时间缓存一段时间,这段时间内这类请求不需要再额外发送一次HTTP请求。
凭据请求
默认情况下,跨源请求不提供凭证(cookie、HTTP认证和客户端SSL证书)。可以通过将xhr的withCredentials
设置为true来表明请求会发送凭证。如果服务器允许携带凭证的请求,那么可以在响应中包含如下HTTP头部:
1 | Access-Control-Allow-Credentials: true |
如果发送了凭据请求而服务器返回的响应中没有这个头部,则浏览器不会把响应交给JavaScript(responseText是空字符串,status是0,onerror()被调用。
服务器也可以在预检请求的响应中发送这个HTTP头部,已表明这个源允许发送凭据请求。
反向代理
用nginx
、apache
做反向代理,也是一种克服同源策略限制的方式。只需要修改nginx/apache
的配置即可解决跨域问题,对于一个服务,可以通过配置多个路径前缀来转发http/https
请求到多个目标服务。这个服务器上所有url都是相同的域名、协议和端口。因此,对于浏览器来说,这些url都是同源的,没有跨域限制。
我们拿使用nginx设置反向代理来举一个例子: 假设我们我们当前的站点是www.a.com
,我们需要访问的资源是: http://www.b.com/course/api/list
1 | server { |
上面这段nginx配置,前6行是一些为了开启www.a.com
站点的服务所写,后面的location
所包的一段文字就是开启反向代理的配置,在location
后面跟的是我们请求的前缀的监测的规则,在这里我们就直接写了/course/api/list
,这些配置设置完成后,假如我们想要在www.a.com
访问接口http://www.b.com/course/api/list
只需要在www.a.com
中访问 http://www.a.com/course/api/list
即可,当请求发出后,我们的本地服务器(nginx)发现这个接口与我们上面的反向代理的匹配规则能够匹配,就会把我们的请求自动转发到配置中的proxy_pass
上,也就是http://www.b.com/course/api/list
,并把请求的response取回来,跨域完成。因为我们的www.a.com
发出的请求的域名和协议和端口都和当前站一样,自然就不会有各种跨域的限制,不管是请求方式,还是cookie自然也就可以正常使用。
参考文献:
《JavaScript高级程序设计》浏览器的同源策略 https://developer.mozilla.org/zh-CN/docs/Web/Security/Same-origin_policy
http://hayageek.com/cross-domain-Ajax-request-jquery