何米酥`s Blog

js高程读书笔记 第21章 Ajax与Comet

September 06, 2017

本章内容

  • 使用XMLHttpRequest对象
  • 使用XMLHttpRequest事件
  • 跨域Ajax通信的限制

XMLHttpReques对象

支持IE7以前的版本,创建XMLHttpRequest对象

function createXHR(){
    if (typeof XMLHttpRequest != "undefined"){
        return new XMLHttpRequest();
    } else if (typeof ActiveXObject != "undefined"){
        if (typeof arguments.callee.activeXString != "string"){
            var versions = ["MSXML2.XMLHttp.6.0", "MSXML2.XMLHttp.3.0",
                            "MSXML2.XMLHttp"],
                i, len;
    
            for (i=0,len=versions.length; i < len; i++){
                try {
                    new ActiveXObject(versions[i]);
                    arguments.callee.activeXString = versions[i];
                    break;
                } catch (ex){
                    //skip
                }
            }
        }
     
        return new ActiveXObject(arguments.callee.activeXString);
    } else {
        throw new Error("No XHR object available.");
    }
}

var xhr = createXHR();        
xhr.open("get", "example.txt", false);
xhr.send(null);

if ((xhr.status >= 200 && xhr.status < 300) || xhr.status == 304){
    alert(xhr.statusText);
    alert(xhr.responseText);
} else {
    alert("Request was unsuccessful: " + xhr.status);
}

至此,可以用以下代码创建XHR对象了

var xhr = createXHR();

XHR的用法

使用XHR对象时,要调用的第一个方法是open(),它接受3个参数:要发送的请求的类型、请求的URL和表示是否异步发送请求的布尔值。

xhr.open("get", "example.php", "false");
  • URL相对于执行代码的当前页面;
  • 调用open()方法并不会真正发送请求

要发送特定的请求,必须调用send()方法。

请求是同步的,JavaScript代码会等到服务器响应之后再继续执行。在收到响应后,响应的数据会自动填充xhr对象的属性:

  • responseText: 作为响应主体被返回的文本。
  • responseXML: 如果响应的内容类型是”text/xml”或”application/xml”,这个属性中将保存包含着响应数据的XML DOM文档。
  • status: 响应的HTTP状态。
  • statusText: HTTP状态的说明。

状态码为304表示请求的资源并没有被修改,可以直接使用浏览器中缓存的版本。

所以应该向下列代码检查状态码:

if ((xhr.status >= 200 && xhr.status < 300) || xhr.status == 304){
    alert(xhr.statusText);
    alert(xhr.responseText);
} else {
    alert("Request was unsuccessful: " + xhr.status);
}

如果要发送异步请求,可以检测XHR对象的readyState属性,该属性表示请求/响应过程的当前活动阶段:

  • 0: 未初始化。尚未调用open()方法
  • 1: 启动。已经调用open()方法但尚未调用send()方法
  • 2:发送。已经调用send()方法但尚未接收到响应。
  • 3:接受。已经接受到部分响应数据。
  • 4:完成。已经接受到全部响应数据,而且可以在客户端使用了。

readyState属性的值由一个值变成另一个值,都触发一次readystatechange事件。

var xhr = createXHR();        
xhr.onreadystatechange = function(event){
    if (xhr.readyState == 4){
        if ((xhr.status >= 200 && xhr.status < 300) || xhr.status == 304){
            alert(xhr.responseText);
        } else {
            alert("Request was unsuccessful: " + xhr.status);
        }
    }
};
xhr.open("get", "example.txt", true);
xhr.send(null);

这个例子在onreadystatechange事件处理程序中使用了xhr对象,没有使用this对象,原因是onreadystatechange事件处理程序的作用域问题。如果使用this对象,在有的浏览器中会导致函数执行失败,或者导致错误发生。因此使用实际的XHR对象实例变量是较为可靠的一种方式。

HTTP头部信息

默认情况下,在发送XHR请求的同时,还会发送下列头部信息。

  • Accept: 浏览器能够处理的内容类型
  • Accept-Charset:浏览器能够显示的字符集。
  • Accept-Encoding:浏览器能够处理的压缩编码。
  • Accept-Language:浏览器当前设置的语言
  • Connection:浏览器与服务器之间连接的类型
  • Host:发出请求的页面所在的域。
  • Referer:发出请求的页面URI(正确拼法是Referrer)
  • User-Agent:浏览器的用户代理字符串
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("Request was unsuccessful: " + xhr.status);
        }
    }
};
xhr.open("get", "example.php", true);
xhr.setRequestHeader("MyHeader", "MyValue");
xhr.send(null);

使用setRequestHeader()方法可以设置自定义的请求头部信息。

调用xhr对象的getResponseHeader()方法并传入头部字段名称,可以取得相应的相应头部信息。而调用getAllResponseHeaders()方法可以取得一个包含所有头部信息的长字符串。

var myHeader = xhr.getResponseHeader("MyHeader");
var allHeaders = xhr.getAllRespnseHeaders();

GET请求

xhr.open("get", "example.php?name1=value1&name2=value2", true);

这样经常会发生一个错误,就是查询字符串的格式有问题。

查询字符串中每个参数的名值对都必须使用encodeURIComponent()进行编码,然后才能放到URL末尾。

下面这个函数可以辅助向现有URL的末尾添加查询字符串参数。

function addURLParam(url, name, value){
    url += (url.indexOf("?")) == -1? "?" : "&";
    url += encodeURIComponent(name) + "=" + encodeURIComponent(value);
    return url;
}

POST请求

发送post请求,要讲content-type头部信息设置application/x-www-form-urlencoded,也就是表单提交时内容类型,其次是以适当创建一个字符串。

function serialize(form){        
    var parts = new Array();
    var field = null;
    
    for (var i=0, len=form.elements.length; i < len; i++){
        field = form.elements[i];
    
        switch(field.type){
            case "select-one":
            case "select-multiple":
                for (var j=0, optLen = field.options.length; j < optLen; j++){
                    var option = field.options[j];
                    if (option.selected){
                        var optValue = "";
                        if (option.hasAttribute){
                            optValue = (option.hasAttribute("value") ? 
                                        option.value : option.text);
                        } else {
                            optValue = (option.attributes["value"].specified ? 
                                        option.value : option.text);
                        }
                        parts.push(encodeURIComponent(field.name) + "=" + 
                                   encodeURIComponent(optValue));
                    }
                }
                break;
                
            case undefined:     //fieldset
            case "file":        //file input
            case "submit":      //submit button
            case "reset":       //reset button
            case "button":      //custom button
                break;
                
            case "radio":       //radio button
            case "checkbox":    //checkbox
                if (!field.checked){
                    break;
                }
                /* falls through */
                            
            default:
                parts.push(encodeURIComponent(field.name) + "=" + 
                    encodeURIComponent(field.value));
        }
    }        
    return parts.join("&");
}

function submitData(){
    var xhr = createXHR();        
    xhr.onreadystatechange = function(event){
        if (xhr.readyState == 4){
            if ((xhr.status >= 200 && xhr.status < 300) || xhr.status == 304){
                alert(xhr.responseText);
            } else {
                alert("Request was unsuccessful: " + xhr.status);
            }
        }
    };
    
    xhr.open("post", "postexample.php", true);
    xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
    var form = document.getElementById("user-info");            
    xhr.send(serialize(form));
}

文件postexample.php可以通过_$POST获取数据

<?php
    header("Content-Type: text/plain");   
    echo <<<EOF
Name: {$_POST['user-name']}
Email: {$_POST['user-email']}
EOF;
?>

text/html与text/plain区别如下:

  1. text/html是html格式的正文,text/html的意思是将文件的content-type设置为text/html的形式
  2. text/plain是无格式正文,text/plain的意思是将文件设置为纯文本的形式,浏览器在获取到这种文件时并不会对其进行处理。
  3. text/xml忽略xml头所指定编码格式而默认采用us-ascii编码
  4. application/xml会根据xml头指定的编码格式来编码

如果不设置Content-Type头部信息,那么发送给服务器的数据就不会出现在$_POST超级全局变量中。

XMLHttpRequest 2级

FromData

FromData为序列化表单以及创建与表单格式相同的数据提供便利。

下面代码创建了一个FromData对象,并向其中添加了一些数据。

var data = new FromData();
data.append("name", "Nicholas");

可以用FromData代替序列化函数

function submitData(){
    var xhr = createXHR();        
    xhr.onreadystatechange = function(event){
        if (xhr.readyState == 4){
            if ((xhr.status >= 200 && xhr.status < 300) || xhr.status == 304){
                alert(xhr.responseText);
            } else {
                alert("Request was unsuccessful: " + xhr.status);
            }
        }
    };
    
    xhr.open("post", "postexample.php", true);
    var form = document.getElementById("user-info");            
    xhr.send(new FormData(form));
}

同时可以不必明确地在XHR对象上设置请求头部。

超时设定

IE8为XHR对象添加了一个timeout属性,表示请求在等待响应多少毫秒之后就终止。如果在规定的时间内浏览器还没有接收到响应,那么就会触发timeout事件,进而会调用ontimeout事件处理程序。这项功能已经被收入到XMLHttpRequest2规范中。

var xhr = createXHR();        
xhr.onreadystatechange = function(event){
    try {
        if (xhr.readyState == 4){
            if ((xhr.status >= 200 && xhr.status < 300) || xhr.status == 304){
                alert(xhr.responseText);
            } else {
                alert("Request was unsuccessful: " + xhr.status);
            }
        }
    } catch (ex){
        //assume handled by ontimeout
    }
};

xhr.open("get", "timeout.php", true);
xhr.timeout = 1000;
xhr.ontimeout = function(){
    alert("Request did not return in a second.");
};        
xhr.send(null);

overrideMimeType()方法

用于重写XHR响应的MIME类型,比如服务器返回的MIME类型是text/plain,但数据中实际包含的是XML。根据MIME类型,即使数据是XML,responseXML属性中还是null。通过这个overrideMimeType()方法,可以保证把响应当做XML而非纯文本来处理。

var xhr = createXHR();
xhr.open("get", "text.php", true);
xhr.overrideMimeType("text/xml");
xhr.send(null);

进度事件 Progress Events

  • loadstrart: 在接收到响应数据的第一个字节时触发。
  • progress: 在接收响应期间持续不断地触发。
  • error: 在请求发生错误时触发
  • abort: 在因为调用abort()方法而终止连接时触发。
  • load: 在接收到完整的响应数据时触发。
  • loadend: 在通信完成或者触发error、abort或load事件后触发。

load事件

用于替代readystatechange事件。onload事件处理程序会接收到一个event对象,其target属性就指向XHR对象实例,因而可以访问到XHR对象的所有方法和属性。

window.onload = function(){
    var xhr = createXHR();        
    xhr.onload = function(event){
        if ((xhr.status >= 200 && xhr.status < 300) || 
                xhr.status == 304){
            alert(xhr.responseText);
        } else {
            alert("Request was unsuccessful: " + xhr.status);
        }
    };
    xhr.open("get", "altevents.php", true);

    xhr.send(null);
};

并非所有的浏览器都为这个事件实现了适当的事件对象,所以还是采用XHR对象变量。

progress事件

这个事件会在浏览器接收新数据期间周期性地触发。 onprogress事件处理程序会接收到一个event对象,其target属性是XHR对象,但包含着三个额外的属性:lengthComputable、position和totalSize。第一个表示进度信息是否可用的布尔值,第二个表示已经接受的字节数,第三个表示根据Content_Length响应头部确定的预期字节数。

window.onload = function(){
    var xhr = createXHR();        
    xhr.onload = function(event){
        if ((xhr.status >= 200 && xhr.status < 300) || 
                xhr.status == 304){
            console.log(xhr.responseText);
        } else {
            console.log("Request was unsuccessful: " + xhr.status);
        }
    };
    xhr.onprogress = function(event){
        var divStatus = document.getElementById("status");
        if (event.lengthComputable){
            divStatus.innerHTML = "Received " + event.position + " of " + event.totalSize + " bytes";
        }
    };
    xhr.open("get", "altevents.php", true);

    xhr.send(null);
};

跨资源共享 CORS

在发送请求时,需要给它附加一个额外的Origin头部,其中包含请求页面的源信息,以便服务器根据这个头部信息来决定是否给予响应。

如果服务器认为这个请求可以接受,就在Access-Control_Allow-Origin头部中发相同的源信息。

IE对CORS的实现

  • cookie不会随请求发送,也不会随响应返回
  • 只能设置请求头部信息中的Content-Type字段
  • 不能访问响应头部信息
  • 只支持GET和POST请求

所有的XDR(XDomainRequest)类型都是异步执行的,不能用它来创建同步请求。请求返回之后,会触发load事件,响应的数据也会保存在responseText属性中。

var xdr = new XDomainRequest();
xdr.onload = function(){
    alert(xdr.responseText);
};
xdr.onerror = function(){
    alert("Error!");
};

//you'll need to replace this URL with something that works
xdr.open("get", "http://www.somewhere-else.com/xdr.php");
xdr.send(null);    

其他浏览器对CORS的实现

只需要在open()方法中传入绝对URL即可。

限制:

  • 不能使用setRequestHeader()设置自定义头部
  • 不能发送和接受cookie
  • 调用getAllResponseHeaders()方法总会返回空字符串

Preflighted Reqeusts

CORS通过一种叫做PreFlighted Requests的透明服务器验证机制支持使用自定义头部、get和post之外的方法以及不同类型的主体内容。

带凭据的请求

默认情况下,跨源请求不提供凭据(cookie、HTTP认证及客户端SSL证明等)。通过将withCredentials属性设置为true,可以指定某个请求应该发送凭据。

服务器如果接受带凭据的请求,会响应

Access-Control-Allow-Credentials: true

如果发送的是带凭据的请求,但服务器的响应中没有包含这个头部,那么浏览器就不会把响应交给JavaScript。

跨浏览器的CORS

function createCORSRequest(method, url){
    var xhr = new XMLHttpRequest();
    if ("withCredentials" in xhr){
        xhr.open(method, url, true);
    } else if (typeof XDomainRequest != "undefined"){
        xhr = new XDomainRequest();
        xhr.open(method, url);
    } else {
        xhr = null;
    }
    return xhr;
}

var request = createCORSRequest("get", "http://www.somewhere-else.com/xdr.php");
if (request){
    request.onload = function(){
        //do something with request.responseText
    };
    request.send();
}

其他跨域技术

图像ping

var img = new Image();
img.onload = img.onerror = function(){
    alert("Done!");
};
img.src = "http://www.example.com/test?name=Nicholas"; 

图像ping最常用于跟踪用户点击页面或动态广告曝光次数。图像ping有两个主要的缺点:

  • 只能发送get请求
  • 无法访问服务器的响应文本。

因此,图像ping只能用于浏览器与服务器的单向通信。

JSONP (JSON with padding)

包含在函数调用中的JSON

像这样 callback({ "name": "Nicholas";});

JSONP由两部分组成:回调函数和数据。回调函数是当响应到来时应该在页面中调用的函数。回调函数的名称一般是在请求中指定的。而数据就是传入回调函数中的JSON数据。

function handleResponse(response){
    alert("You're at IP address " + response.ip + ", which is in " + response.city + ", " + response.region_name);
}

var script = document.createElement("script");
script.src = "http://freegeoip.net/json/?callback=handleResponse";
document.body.insertBefore(script, document.body.firstChild);
function createStreamingClient(url, progress, finished){        
            
    var xhr = new XMLHttpRequest(),
        received = 0;
        
    xhr.open("get", url, true);
    xhr.onreadystatechange = function(){
        var result;
        
        if (xhr.readyState == 3){
        
            //get only the new data and adjust counter
            result = xhr.responseText.substring(received);
            received += result.length;
            
            //call the progress callback
            progress(result);
            
        } else if (xhr.readyState == 4){
            finished(xhr.responseText);
        }
    };
    xhr.send(null);
    return xhr;
}

var client = createStreamingClient("streaming.php", function(data){
                alert("Received: " + data);
             }, function(data){
                alert("Done!");
             });

通过侦听readystatechange事件及检测readyState的值是否为3,就可以利用XHR对象实现HTTP流。在上述这些浏览器中,随着不断从服务器接收数据,readyState的值会周期性地变为3。当readyState值变为3时,responseText属性中就会保存接收到的所有数据。

createSteamingClient()函数接收三个参数:要连接的URL,在接收到数据时调用的函数以及关闭连接时调用的函数。 只要readystatechange事件发生,而且readyState值为3,就对responseText进行分割以取得最新数据。这里的received变量用于记录已经处理了多少个字符,每次readyState值为3时都递增。然后,通过progress回调函数来处理传入的新数据。而当readyState值为4时,则执行finished回调函数,传入响应返回的全部内容。

服务器发送事件

SSE(Server-Sent Events) SSE API用于创建到服务器的单向链接,服务器通过这个连接可以发送任意数量的数据。服务器响应的MIME类型必须是text/event-stream,而且是浏览器中javascript API能解析格式输出。

  1. SSE API
var source = new EventSource("myevents.php");

实例有个readyState属性,值为0表示正连接到服务器,值为1表示打开了连接,值为2表示关闭了连接。

事件:

  • open: 建立连接时触发。
  • message: 从服务器接收到新事件时触发
  • error: 无法建立连接时触发。
source.onmessage = function(event) {
    var data = event.data;
    //处理数据
}

服务器返回的数据保存在event.data中。

默认情况下,EventSource对象会保持与服务器的活动连接。如果连接断开,还会重新连接。这就意味着SSE适合长轮询和HTTP流。

关闭连接使用source.close();

  1. 事件流

服务器事件会通过一个持久的HTTP响应发送,这个响应的MIME类型为text/event-stream。响应的格式是纯文本,最简单的情况是每个数据项都带有前缀data:

data:foo

data: bar

data: foo data: bar

第一第二个事件流返回的event.data值分别为foo bar。 第三个返回foo/bar。:后面需要添加空格。

通过id: 前缀可以给特定的事件指定一个关联的ID。这个ID位于前后都可以

data: foo id: 1

Web Sockets

全双工,双向通信。

URL模式由http://变为ws://,https://变为wss://

  1. Web Socket API

实例一个WebSocket对象并传入要连接的URL:

var socket = new WebSocket("ws://www.example.com/server.php");

readyState属性:

  • 0 正在建立连接
  • 1 已建立连接
  • 2 正在关闭连接
  • 3 已经关闭连接

要关闭连接,可以在任何时候调用close方法

  1. 发送和接受数据

使用send()方法并传入任意字符。

var socket = new WebSocket("ws://www.example.com/server.php");
socket.send("Hello World!");

Web Sockets 只能发送纯文本数据,所以对于复杂的结构需要进行序列化。

var message = {
    time: new Date();
    text: "Hello World";
    clientId: "asdfp";
}

socket.send(JSON.stringify(message));
  1. 其他事件
  • open: 成功建立连接时触发。
  • error: 在发生错误时触发,连接不能持续。
  • close: 在连接关闭时触发。

这三个事件中,只有close事件的event对象有额外的信息。 wasClean、code和reason。 分别表示:布尔值(是否明确关闭连接)、服务器返回的数值状态码和服务器发回的消息。

安全

CSRF(Cross-Site Request Forgery)

  • 要求以SSL连接来访问可以通过XHR请求的资源
  • 要求每一次请求都要附带经过相应算法计算得到的验证码。

小结

Ajax是无需刷新页面就能够从服务器取得数据的一种方法。

  • 负责Ajax运作的核心对象是XMLHttpRequest(XHR)对象
  • XHR对象由微软最早在IE5中引入,用于通过JavaScript从服务器取得XML数据

Profile picture

Written by 何坤舆 EFE