/ AJAX

AJAX 及其跨域实现

Ajax 概念始于 2005 年的一篇文章,此前这种技术叫远程脚本(Remote Scripting)。早在 1998 年就有人用不同的手段实现了类似的技术(比如借助隐藏框架或内嵌框架)。再往前推,JavaScript 需要借助 Java applet 或者 Flash 等中间层向服务器发送请求。

1. XMLHttpRequest 对象

Ajax 的核心是 XMLHttpRequest (简称 XHR),微软最早在 IE5 中引入了 XHR 对象,其他浏览器后来都提供了相同的实现。

IE5 中,XHR 对象是通过 MSXML 库中的一个 ActiveX 对象实现的。IE 中可能会遇到三种不同版本的 XHR 对象,即 MSXML2.XMLHttp、MSXML2.XMLHttp.3.0、MSXML2.XMLHttp.6.0。IE7+ 及其他浏览器都支持原生的 XHR 对象。为兼容 IE6-,可以创建一个函数。

function createXHR() {
    if (typeof XMLHttpRequest != "undefined") {
        return new XMLHttpRequest();
    } else if (arguments.callee.activeXString != "undefined") {
        var v = ["MSXML2.XMLHttp.6.0", "MSXML2.XMLHttp.3.0", "MSXML2.XMLHttp"],
            i,len;

        for (i = 0, len= v.length; i < len; i++){
            try {
                new ActiveXObject(v[i]);
                arguments.callee.activeXString = v[i];
                break;
            } catch (ex) {
                // do nothing
            }
        }

        return new ActiveXObject(arguments.callee.activeXString);
    } else {
        throw new Error("No XHR object available.");
    }
}

IE7 以前的版本其实只需要考虑 IE6,可以精简一下这个函数。

function createXHR() {
    if (typeof XMLHttpRequest != "undefined") {
        return new XMLHttpRequest();
    } else {
        try {
            return new ActiveXObject("MSXML2.XMLHTTP.3.0");
        }
        catch(ex) {
            return null;
        }
    }
}

这样,就可以方便地创建一个跨浏览器的 XHR 对象了。

var xhr = createXHR();

1.1 开始 XHR 之旅

开始 XHR 之旅要调用的第一个方法是 open(),它接受 5 个参数——请求类型(GET、POST)、URL、是否异步发送、用户名、密码,后三个可选:
xhr.open(method, url [, async = true [, user = null [, password = null]]])
GET 请求

GET 请求常用于向服务器查询信息。查询字符串可以追加到 URL 末尾。为避免发生错误,传入 open() 方法的 URL 末尾的查询字符串必须编码,可以使用 encodeURICompontent() 方法。下面是一个向现有 URL 末尾添加查询字符串的辅助函数:

/**
 * 向URL末尾追加查询字符串
 * @param url {String} 要追加查询字符串的 URL
 * @param name {String} 追加查询的键
 * @param value {String} 追加查询的值
 * @returns {String} 返回追加查询字符串后的 URL
 */
function addURLParam(url, name, value) {
    url += ( url.indexOf("?") == -1 ? "?" : "&" );
    url += encodeURIComponent(name) + "=" + encodeURIComponent(value);
    return url;
}

受浏览器 URL 长度限制,GET 请求发送的数据有限。

POST 请求

POST 请求用于向服务器发送要保存的数据。POST 请求可以发送大量的数据,而且没有格式限制。不过 POST 请求消耗的资源多一些。仅看性能的话,发送相同的数据,GET 请求最多可比 POST 请求快两倍。

open() 方法的 URL 可以使用相对当前页面的路径,也可以使用同源绝对路径。调用 open() 后,就可以调用 send() 发送数据了。send() 方法有一个参数,即要发送的数据。这个参数对符合标准的浏览器来说是可选的,但为避免某些浏览器出问题,在没有要发送的数据时也要传入 null。

xhr.send([data = null]);

与使用 GET 请求直接在 URL 末尾追加查询字符串不同,使用 POST 方法时,可以通过 send() 发送数据。默认情况下,服务器对 POST 请求和提交 Web 表单的请求处理方式不同,服务器端必须有程序处理发过来的原始数据。

不过,可以使用 XHR 来模仿表单提交

  1. 首先将 Content-Type 头部信息设置为 application/x-www-form-urlencoded (即表单提交时的类型);
  2. 然后将数据序列化为类似 GET 请求查询字符串的格式,通过 send() 发送。
xhr.open("post", "url.php");
xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
xhr.send(serializeString);
对 PHP 而言,如果不设置 Content-Type 头部信息,发送给服务器的数据就不会出现在 $_POST 超全局变量中,只能借助 $HTTP_RAW_POST_DATA 访问发送的数据。

调用 send() 之后,请求会被发送到服务器。如果是同步请求,JavaScript 代码会等到服务器响应之后再继续执行。收到服务器响应后,响应的数据会自动填充 XHR 对象的属性:

  • status - HTTP 响应代码;
  • statusText - HTTP 响应状态说明;
  • responseText - 响应返回的文本;
  • responseXML - 如果响应内容类型是"text/xml"或"application/xml",该属性将包含响应数据的 XML 文章。
收到响应后,首先应该检查 status 属性,200 为成功的标志,此时,statusText、responseXML(设置了xml类型的情况下)也可以访问了。另外,304 状态代码表示资源没有修改,可以直接使用浏览器中缓存的版本。
var xhr = createXHR();
xhr.open("get", "url.txt", false);
xhr.open(null);
if( (xhr.status >= 200 && xhr.status < 300) || xhr.status == 304 ) {
    alert(xhr.responseText);
} else {
    alert("Error: " + xhr.status);
}
关于 HTTP 响应代码,参看 List of HTTP status codes。(注意,IE6 会将 204 设置为 1223,而 IE 原生 XHR 则将 204 规范化为 200。Opera 会将 204 报告为 0。)

上面是 XHR 同步请求的例子,如果是发送异步请求,则需要检测 readyState 属性。这个属性表示请求响应的当前活动阶段,可能的值为 0-4:

  • 0 - 未初始化,即尚未调用 open() 方法;
  • 1 - 启动,已经 open(),尚未 send();
  • 2 - 发送,已 send(),但还没有响应;
  • 3 - 接收,已经接收到部分响应数据;
  • 4 - 完成,已接收到全部数据并可在客户端使用。
readyState 属性值每次改变都会触发一次 readystatechange 事件,所以利用这个事件来检测变化后的 readyState 值。通常只要检测 4 就行,因为完成以后才可以对数据进行处理。为确保跨浏览器兼容,必须在 open() 之前调用 readychangestate 事件处理程序
var xhr = createXHR();
xhr.onreadystatechange = function() {
    if (xhr.readyState == 4) {
        if( (xhr.status >= 200 && xhr.status < 300) || xhr.status == 304 ) {
            alert(xhr.responseText);
        } else {
            alert("Error: " + xhr.status);
        }
    }
};
xhr.open("get", "url.txt", true);
xhr.send(null);
使用 DOM 0 级方法添加事件处理程序,不使用 this 关键字,都是出于浏览器兼容考虑

调用 abort() 方法可以停止 XHR 触发事件,而且不再允许访问与响应有关的对象属性。停止请求后,还应该解除 XHR 引用,释放内存。

1.2 HTTP 头部信息

使用 setRequestHeader() 方法可以设置自定义的请求头部信息。该方法必须在 open() 之后 send() 之前调用
xhr.setRequestHeader(header, value);
默认情况下,发送 XHR 请求时会发送下列头部信息:
  • Accept:浏览器能够处理的内容类型 MIME
  • Accept-Charset:浏览器能够显示的字符集
  • Accept-Encoding:浏览器能够处理的压缩编码
  • Accept-Language:浏览器当前语言设置
  • Connection:浏览器与服务器之间的链接类型
  • Cookie:当前页面设置的 Cookie
  • Host:发送请求的页面所在域
  • Referer:发出请求页面的 URL
  • User-Agent:用户代理字符串
不同浏览器发送的头部信息会有所不同,不过基本都包含上面列出的项。在发送自定头部信息时,最好不要使用浏览器正常使用的头部字段名称,否则可能会影响浏览器响应。

getResponseHeader() 方法可以获取服务器响应头部信息,传入的参数为头部字段名称。

xhr.getResponseHeader(header);

getAllResponseHeaders() 方法返回包含所有服务器响应信息的多行文本。这两个方法都应该放在 readystatechange 事件处理程序中使用。

2. XMLHttpRequest 2 级

XHR 已成为事实标准,被广泛应用,慢不止半拍的 W3C 也着手制定了相应标准以规范各浏览器行为。XMLHttpRequest 2 级对 1 级进行了扩展了,但目前浏览器支持有限,而且只是部分实现。

2.1 FormData

FormData 用来序列化表单或创建表单格式的数据。
fd = new FormData([form])
返回一个新的 FormData 对象,如果传递表单元素,则序列化表单数据。
fd.append(name, value [, filename])
FormData 对象插入键值对。
创建 FormData 实例后,可以将其直接传递给 XHR open() 方法。
var form = document.forms[0];
var data = new FormData(form);
data.append("other", "some text");
xhr.send(data);
使用 FormData 时不必再设置请求头部信息,XHR 对象能够识别传入的数据类型是 FormData 实例,并添加适当的头部信息。

支持 FormData 的浏览器有 FF4+、Safari 5+、Chrome、Android 3+ WebKit、IE10。

2.2 timeout 属性

timeout 属性表示请求在超过指定时间(毫秒)之后就终止。设定非 0 数值后,如果规定时间内没有接到响应,就会触发 timeout 事件,然后调用 timeout 事件处理程序。这个属性最早有 IE8 引入,现已被纳入标准。

支持超时设定的浏览器有 IE8+、Opera 12.15、FF20。(可能也有更早版本支持的,但无法一一测试)

2.3 overrideMimeType() 方法

overrideMimeType() 方法用于重写 XHR 响应的 MIME 类型,最早由 Firefox 引入。此方法必须在 send() 之前调用。支持的浏览器有 FF、Safari 4+、Opera 10.5+、Chrome。

2.4 ProgressEvent 进度事件

Event name Interface Dispatched when…
loadstart ProgressEvent 在接受到相应数据的第一个字节时触发
progress ProgressEvent 在接收和相应期间不断触发
abort ProgressEvent 调用 abort() 方法终止连接时触发
error ProgressEvent 请求失败时触发
load ProgressEvent 成功接受完整数据时触发
timeout ProgressEvent 超时事件
loadend ProgressEvent 请求完成或者触发 error、abort、load事件后触发
XHR 请求都从触发 loadstart 事件开始,接下来触发一个或多个 progress 事件,然后触发 error、abort、load 中的一个,最后以触发 loadend 事件结束。

IE8-9 只支持 load 事件,其他现代浏览器基本上都支持 loadend 以外的事件,最新版本(测试了 IE10、Chrome 26、FF20)中 loadend 事件也得到支持。

2.5 load 事件

Firefox 最早引入 load 事件以替代 readystatechange 事件。响应接收完成后将触发 load 事件,因此没必要再检查 readyState 属性了。onload 事件处理程序会接收指向 XHR 对象实例的 event 对象,可以访问到 XHR 对象的所有属性和方法。但不是所有浏览器都能这样实现,因此,最好还是在事件处理程序中直接使用 XHR 对象变量。

另外,只要接收到服务器响应,不管状态如何,浏览器都会触发 load 事件,因此,还要检查 status 事件,才能确定数据是否可用

var xhr = createXHR();
xhr.onload = function () {
    if( (xhr.status >= 200 && xhr.status < 300) || xhr.status == 304 ) {
        console.log(xhr.responseText);
    } else {
        console.log("Error: " + xhr.status);
    }
}
xhr.open("get", "url.txt", true);
xhr.send(null);

2.6 progress 事件

propress 事件也是 Firefox 最早引入的,在接收数据期间不断触发。onprogress 事件处理程序接收 target 属性为 XHR 对象的 event 对象,这个对象有三个属性:lengthComputable(表示进度信息是否可用的布尔值)、position(已经接收的字节数)、totalSize(预期字节数,根据 Content-Length 头确定)。可以利用这些信息创建进度指示器。

xhr.onprogress = function(event) {
    if (event.lengthComputable) {
        console.log("Received " + event.position + " of " + event.totalSize + " bytes.");
    }
}

同样,为保证正常执行,必须在 open() 之前添加 onprogress 事件处理程序

2.7 upload 属性

xhr.upload 返回与 XHR 对象关联的 XMLHttpRequestUpload 对象,用于收集文件上传信息。

2.8 withCredentials 属性

跨源请求默认不提供凭据(Cookie、HTTP 认证、客户端 SSL 证书),通过将 withCredentials 属性设置为 true,可以指定请求发送凭据。发送带凭据请求时,服务器端必须将 Access-Control-Allow-Credentials 设置为 true,否则,JavaScript 无法从浏览器获得响应数据,致使 responseText 为空字符串、status 为 0 ,并触发 error 事件。

支持这个属性的浏览器有 FF3.5+、Safari 4+、Chrome。

参考链接

3. Ajax 跨域请求

同源策略(Same-Origin Policy)可以一定程度保障安全,但也给正常跨源请求带来不便。为此,W3C 制定了 CORS (Cross-Origin Resource Sharing)标准,规范跨源请求时浏览器和服务器的沟通机制。CORS 使用自定义的 HTTP 头让浏览器和服务器进行沟通,从而决定请求或响应是否允许。

比如发送请求时,附加额外的 Origin 头部,包含请求页面的源信息(协议、主机、端口):

Origin: http://csspod.com

如果服务器接受请求,则应该在 Access-Control-Allow-Origin 头部中回发相同的源信息(如果是公共资源则可以回发"*",表示接受任何源的请求):

Access-Control-Allow-Origin: http://csspod.com

如果二者不匹配,浏览器就会驳回请求。其中,请求和响应都不包含 cookie 信息。

// Apache on .conf or .htaccess
Header set Access-Control-Allow-Origin "http://csspod.com"

// PHP
header("Access-Control-Allow-Origin: http://csspod.com");

// IIS7+ on web.config , 或者在 GUI 管理界面里 HTTP 响应头里添加
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
    <system.webServer>
        <httpProtocol>
            <customHeaders>
                <add name="Access-Control-Allow-Origin" value="http://csspod.com" />
            </customHeaders>
        </httpProtocol>
    </system.webServer>
</configuration>

响应头信息可以在程序中添加,也可以在Web 服务器(IIS、Apache 等)中添加,相关设置参见 enable cross-origin resource sharing

3.1 XDR:IE8-9 的 CORS 实现

IE8 引入了 XDomainRequest(XDR) 对象,用来实现跨源 Ajax 请求。XDR 对象的安全机制部分实现了 CORS 规范,XDR 与 XHR 的区别在于:
  • cookie 不发送,也不返回;
  • 只能设置头部信息的 Content-Type 字段;
  • 不能访问响应头部信息;
  • 只支持 GET 和 POST 请求。
XDR 对象的使用方法和 XHR 对象很相似,需要注意的是:
  • open() 方法只接收请求类型和 URL 两个参数,所有 XDR 请求都是异步执行;
  • 响应失败时触发 error 事件,但不提供任何错误信息;
  • ~~POST请求时,XDR 提供 contentType 属性用来表示发送数据格式,如 xdr.contentType="application/x-www-form-urlencoded";,这个属性应该设置在 open() 之后 send() 之前。~~XDR 在设计的时候支持 POST 请求设置 content-type,但在最终实现时并没有支持(见此文第 4 点)。

XDR 的更多细节参见:

3.2 其他浏览器的 CORS 实现

FF3.5+、WebKit 浏览器都通过 XHR 对象实现对 CORS 原生支持,只需要在 open() 方法中传入绝对 URL 即可。不过出于安全考虑,跨域 XHR 也有一些限制:
  • 不能使用 setRquestHeader() 设置自定义头部;
  • 不能发送和接收 cookie;
  • getAllResponseHeader() 总返回空字符串。
IE10 也实现了 XHR 对 CORS 的原生支持,详情见 CORS for XHR in IE10

3.3 跨浏览器的 CORS

了解了 IE 和其他浏览器的 CORS 实现以后,就可以得出跨浏览器的解决方案了:
/**
 * 创建跨浏览器 CORS 对象
 * @param method {String} get OR post
 * @param url {String}
 * @returns {XMLHttpRequest}
 */
function createCORSRequest(method,url) {
    var xhr = new XMLHttpRequest();
    if ("withCredentials" in xhr) {
        xhr.open(method, url, true);
    } else if (typeof XDomainRequest != "undefined") {
        var xhr = new XDomainRequest();
        xhr.open(method,url);
    } else {
        xhr = null;
    }
    return xhr;
}

var cors = createCORSRequest("get","url");
if (cors) {
cors.onload = function() {
console.log(cors.responseText);
}
cors.send();
}


两个对象共同的属性和方法都是可用的:abort()、onerror、onload、responseText、send()。下表是 CORS 浏览器支持详情。

3.4 Preflighted Requests

Preflighted requests 更多细节参见 MDN -HTTP access control (CORS) 中的相关部分。

3.5 其他跨域技术

图像 Ping:图像不受同源策略限制,可以用于跨域发送数据。
var img = new Image();
img.onload = img.onerror = function() {
    console.log("Sent.");
}

img.src = "http://example.com/test?ua=text";


图像 Ping 只能发送请求,无法访问服务器响应信息,只能实现浏览器到服务器的单向通信,通常用于记录用户行为,如页面点击、广告点击等。

JSONP

JSONP (JSON with padding) 由回调函数和数据组成。函数名字一般在请求中指定,数据就是传入回调函数的 JSON 数据。

http://domain/json/?callback=handle

JSONP 能直接访问响应文本,支持浏览器和服务器之间双向通信,不过,JSONP 从其他域中加载代码执行,存在一定的安全风险。