Ajax与跨域

发布时间:2017/02/13

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// 新建XMLHttpRequest对象
var xhr = new XMLHttpRequest();

// 注册请求状态发生变化的回调,状态发生变化时,函数会被调用
xhr.onreadystatechange = function () {
if (xhr.readyState === 4) { // readyState为4代表请求成功完成
// 在这里可以执行请求完成的操作
console.log(xhr.status); // status:响应的HTTP状态
console.log(xhr.statusText); // statusText:响应的HTTP状态描述
console.log(xhr.responseText); // responseText:响应体返回的文本
console.log(xhr.responseXML); // responseXML:如果响应的内容是"text/xml"或"application/xml",字段的值为包含响应数据的XML DOM文档
console.log(xhr.getResponseHeader("MyHeader")); // getResponseHeader()方法从xhr对象获取响应头部,只要传入获取头部的名称即可
console.log(xhr.getAllResponseHeaders()); // getAllResponseHeaders()方法会返回包含所有响应头部的字符串
} else {
// HTTP请求还在继续...
}
}

// open()方法支持3个参数:请求类型("get","post","put"等)、请求URL,以及表示请求是否异步的布尔值。
// URL可以是相对地址也可以是绝对地址,查询字符串中的每个名和值都必须使用encodeURIComponent()编码,所有的名/值都必须以&分割
xhr.open('GET', '/api?name1=value1&name2=value2', true);

// setRequestHeader方法可以设置请求头,接收两个参数:头部字段的名词和值。为了确保请求头被发送,必须在open()之后、send()之前调用setRequestHeader()
xhr.setRequestHeader('MyHeader', 'MyValue');

// 定义好请求后,必须使用send()方法发送请求
// send()方法接收一个参数,作为请求体发送的数据。
// 如果模拟表单提交,第一步需要设置请求头 Content-Type为"application/x-www-formurlencoded",数据需要和url的search部分一样组织
// 如果发送JSON数据,需要设置请求头 Content-Type为"application/json",数据为json格式的字符串
xhr.send(null);

// -----------------------------------------------
// 在收到响应之前如果想要取消异步请求,可以调用abort()方法
xhr.abort();

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
2
3
4
5
6
7
8
9
// 通过直接给FormData构造函数传入一个表单元素,也可以将表单中的数据作为键值对填充进去
var data = new FormData(document.forms[0]);

// 创建与表单类似格式的数据然后通过XHR发送
var data = new FormData();
data.append('name', 'Nicholas'); // append()方法接收两个参数:键和值

// 有了FormData实例,可以直接传给xhr对象的send()方法
xhr.send(data); // xhr为XMLHttpRequest的实例

使用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,我们知道,一个网页可以从任何网页中加载图像,没有跨域的限制,因此我们就可以动态的创建图像,使用它们的onloadonerrer事件处理程序来确定是否请求是否完成。

这种方式多用于与服务器进行简单、单向的跨域通信的一种方式(比如日志上报)。请求的数据是通过查询字符串形式发送的,而响应通常是一个位图图片,或者204的状态码,通过图像ping,浏览器得不到任何具体的数据。

1
2
3
4
5
var img = new Image();
img.onload = img.onerror = function(){
console.log('请求结束');
};
img.src = 'http:127.0.0.2:8888/report?function=login';

这里创建了一个image的实例,然后注册onloadonerror的监听,这样无论结果如何,只要请求完成,就能得到通知。当我们为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
2
3
function handleCallback(data) {
console.log('My name is ' + data.name + ',i am' + data.age + 'years old');
}

然后创建script标签,并将我们需要请求的url后面跟上请求参数callback等于我们预先准备好的回调函数handleCallback,并将script标签插入文档中。

1
2
3
4
var js = document.createElement('script');
js.src = 'http:127.0.0.2:8888/userInfo?callback=handleCallback';
head = document.getElementsByTagName('head')[0];
head.appendChild(js);

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头部,已表明这个源允许发送凭据请求。

反向代理

nginxapache做反向代理,也是一种克服同源策略限制的方式。只需要修改nginx/apache的配置即可解决跨域问题,对于一个服务,可以通过配置多个路径前缀来转发http/https请求到多个目标服务。这个服务器上所有url都是相同的域名、协议和端口。因此,对于浏览器来说,这些url都是同源的,没有跨域限制。

我们拿使用nginx设置反向代理来举一个例子: 假设我们我们当前的站点是www.a.com,我们需要访问的资源是: http://www.b.com/course/api/list

1
2
3
4
5
6
7
8
9
10
11
12
13
14
server {
listen 80;
charset utf-8;
server_name www.a.com;
server_name_in_redirect off;
root /a;
location /course/api/list {
proxy_pass http://www.b.com/course/api/list;
proxy_redirect off;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}

上面这段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

https://www.w3.org/TR/cors/

http://www.liaoxuefeng.com/wiki/001434446689867b27157e896e74d51a89c25cc8b43bdb3000/001434499861493e7c35be5e0864769a2c06afb4754acc6000

http://blog.jobbole.com/90975/

最后更新:2022/06/30