浅析XMLHttpRequest

在Ajax技术出现之前,客户端浏览器与服务器之间的交互是非常传统的方式,每一次,浏览器向服务器发送一个请求,服务器接受并处理,返回相对应的处理结果给浏览器,浏览器接收服务器的返回结果,重新加载新的结果,这样的交互方式方式,用户需要花费一定的时间来每一次等待页面的重新加载,以求获取服务器的响应,如果网络不给力或者加载的对象比较大,需要花费一定的时间,那么,用户就并需花费大量的时间在等待上面。

为了避免这种无谓的等待跟提高用户的操作体验,微软第一个站出来,开发了XMLHttpRequest Object,用以实现浏览器与服务器之间的异步通信,进行数据交互,很快,这种方法被大量的采用和广泛的应用,现在所有主流的浏览器都支持了这样的交互方式,通过XMLHttpRequest Object.

Microsoft最初开发的XMLHttpRequest是基于ActiveXObject控件的,与其它的主流浏览器不同(其它的浏览器都是内置本地Javascript支持XMLHttpRequest Object),所以在具体的跨浏览器开发的时候,需要特别留意这一点。尽管在具体的实现细节上,旧的IE浏览器(IE7之前)与其它的主流浏览器不同,但是庆幸的是大家基于这个XMLHttpRequest Object与服务器进行交互的方式确实基本相同,都是采用相同的方法跟属性,这也给我们跨浏览器操作带了极大的便利性。

这里我们简单的介绍一下XMLHttpRequest Object的一些属性,方法,以及如何利用这个Object实现与浏览器的异步操作。

旧版本IE下创建XMLHttpRequest Object

在IE7之前,XMLHttpRequest Object是通过ActiveXObject来实现,方法可以参考如下:

function getXMLHttpRequest() {
    var versions = ['MS2XML.XMLHTTP.6.0', 'MS2XML.XMLHTTP.3.0', 'MS2XML.XMLHTTP', 'Microsoft.XMLHTTP'];
    for (var i = 0; i < versions.length; i++) {
        try {
            return new ActiveXObject(versions[i]);
        } catch (e) {
            continue;
        }
    }
};

IE7以及其它现代浏览器下创建XMLHttpRequest Object

在IE7+以及其它的现代浏览器中,可以简单地使用以下的语句来创建XMLHttpRequest Object

var xhr = new XMLHttpRequest();

跨浏览器实现

综上所述,我们可以用以下的方法来实现跨浏览器创建XMLHttpRequest Object

function getXMLHttpRequest() {
    if (typeof XMLHttpRequest !== 'undefined') {
        return new XMLHttpRequest();
    } else {
        var versions = ['MS2XML.XMLHTTP.6.0', 'MS2XML.XMLHTTP.3.0', 'MS2XML.XMLHTTP', 'Microsoft.XMLHTTP'];

        for (var i = 0; i < versions.length; i++) {
            try {
                return new ActiveXObject(versions[i]);
            } catch (e) {
                continue;
            }
        }
    }
};

XMLHttpRequest与服务器通信三部曲

XMLHttpRequest Object实现与服务器的通信交互,主要是通过以下的三个步骤来实现:

 - 创建XMLHttpRequest Object

 - XMLHttpRequest.open(Method, URL, Asyn),该方法有三个参数,第一个是request method,主要是通过GET/POST两种方式,第二个参数是请求的URL,但是必须是与当前的页面处于相同的Domain,第三个是布尔变量,true表示有异步请求,false表示为同步请求,客户端必须等待服务器返回加载完毕之后,才能继续之下往下的操作

 - XMLHttpRequest.send(data),该方法有一个参数,如果没有参数传递给服务器,设置为null

 XMLHttpRequest Response

当XMLHttpRequest发送请求上服务器,服务器响应并处理完成之后,就会把处理的结果返回给浏览器,我们可以通过XMLHttpRequest Object的一些方法和属性来获取返回的操作结果。

我们可以通过XMLHttpRequest的status, statusText, readyState, responseText以及responseXML属性来查看返回的状态跟结果。

当我们发送请求上服务器之后,我们可以通过readyState的属性来监听当前的状态,readyState总过有以下5ge状态:

 - 0 : 还没有进行任何的初始化动作,open method还没有被调用

 - 1 : open method被调用,但是请求还没有send出去

 - 2 : 调用send method发送请求

 - 3 : 数据加载当中

 - 4 : 请求完成

当readyState在不同的状态之间切换的时候,会触发onreadystatechange事件,我们可以通过绑定这个事件,对请求的响应状态进行实时的监控:

window.onload = function () {
    var xhr = getXMLHttpRequest();
    xhr.onreadystatechange = function () {
        if (xhr.readyState === 4) {

        }
    }
};

通常我们最为关心就是当readyState为4的情况,此时我们可以通过查看当前的HTTP status code,来判定请求是否成功,以下是我们较为常用的status code

 - 200 <= xhr.status < 300,当satus code在这个区间的时候,表示请求成功

 - 304,这个代码表示not modified since last request, the response will get from browser personal cache,依然表示一个成功的请求

 - 另外有一种情况我们需要留意,当我们请求一个本地文件(protocol为file://)的时候,此时的status code返回的是undefined

 - 另外一个比较特殊的情况是,当Safari浏览器,the response is not modified since last request,这种情况下它返回的并不是304,而是一个undefined

因此我们可以通过以下的代码还检验一个HTTP请求是否成功:

function httpSuccess(xhr) {
    return (200 <= xhr.status < 300) || xhr.status === 304 || 
            (window.location.host.protocol === 'file:' && xhr.status === undefined) || 
            (userAgent.indexOf('Safari') !== -1 && xhr.status === undefined);
}

我们一般不通过statusText属性来判断当前的请求是否成功,因为不同的浏览器有不同实现,对于相同的结果,可能返回不同的描述。

我们可以通过responseText跟responseXML这两个属性来获取当前返回的内容,无论content-type为何值,我们都可以通过responseText来获取当前的结果,但是responseXML为null,如果当前的content-type不是text/xml或者application/xml.

window.onload = function () {
    var xhr = getXMLHttpRequest();
    xhr.onreadystatechange = function () {
        if (xhr.readyState === 4) {
            if (httpSuccess(xhr)) {
                console.debug(xhr.responseText);
            }
        }
    }
};

序列化请求数据

当我们发送一个请求上服务器的时候,我们通常会向服务器发送额外的请求数据,这个时候我们就需要先将请求数据进行格式化,把它转变成服务器可以处理的形式,通常我们把这个过程称之为序列化。

在客户端,我们通常是以以下的两种形式向服务器提交请求参数:

 - JSON格式 : {'userName' : 'AndyLuo', 'title' : 'Software Engineering'}

 - 表单数据 : [userNameElem, titleElem]

通过序列化我们最终需要把它们转换成诸如 https://www.someurl.com?name1=value1&name2=value2&name3=value3的形式

function serialize(data) {
    var rtnValue = '';
    if (Object.prototype.toString.call(data) === '[Object Array]') {
        // handle form elements case
        for (var i = 0; i < data.length; i++) {
            var elem = data[i];
            rtnValue = addUrlParameter('', elem.name, elem.value);
        }
    } else {
        for (var k in data) {
            rtnValue = addUrlParameter('', k, data[k]);
        }
    }
    
    return rtnValue;
}

function addUrlParameter(url, name, value) {
    if (url.indexOf('?') == -1) {
        url += '?';
    } else {
        url += '&';
    }
    
    url += encodeURIComponent(name) + '=' + encodeURIComponent(value);
    
    return url;
}

HTTP Header

我们可以通过xhr.setRequestHeader(hdrName, hdrValue)来订制header value,也可以通过xhr.getResponseHeader(hdrName)以及xhr.getAllResponseHeaders()来获取服务器响应的header头部信息。

xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');

xhr.setRequestHeader('Content-Type', 'application/xml');

xhr.setRequestHeader('userName', 'AndyLuo');

xhr.getResponseHeader('userName');

xhr.getALLResponseHeaders();

另外,xhr还提供了一个非常有用的方法overwriteMimeType,我们可以通过修改MIME类型以获得正确的返回,比如,当前的服务器返回的是一个XML数据,但是它的content-type却是设置成了text/plain,这种情况之下,responseXML将为null,我们就可以通过overwriteMimeType('text/xml')来对返回的content type进行修改以得到我们预期的结果。

GET/ POST 方式请求数据

window.onload = function () {
    var xhr = getXMLHttpRequest();
    xhr.onreadystatechange = function () {
        if (xhr.readyState === 4) {
            if (httpSuccess(xhr)) {
                console.debug(xhr.responseText);
            }
        }
    }
    
    xhr.open('GET', '/someurl/somepage?param1=value1&param2=value2', true);
    xhr.send(null);
};
window.onload = function () {
    var xhr = getXMLHttpRequest();
    xhr.onreadystatechange = function () {
        if (xhr.readyState === 4) {
            if (httpSuccess(xhr)) {
                console.debug(xhr.responseText);
            }
        }
    }
    
    xhr.open('POST', '/someurl/somepage', true);
    xhr.send('param1=value1&param2=value2');
};

 XMLHttpRequest Level 2

 

随着XMLHttpRequest技术的不同发展,W3C起草了XMLHttpRequest Level 2 Spec,给XMLHttpRequest带了给多特性和可能性,由于尚处于起草阶段,各个浏览器对它的支持也是很有限。

XMLHttpRequest Level 2的其中一个亮点之一就是引入FormData对象,再POST方法请求数据的时候不,可以方便的对表单数据进行操作,其具体的用法有以下两种方式:

 - FormData.append(name, value)

 - new FormData(formElement)

var formData = new FormData();
formData.append('userName', 'AndyLuo');
xhr.send(formData);

xhr.send(new Formdata(document.forms[0]));

另一个值得一提的是,Level 2引进了以下的event事件:

 - loadStart : 当客户端接收到第一个字节的时候,触发此事件

 - progress : 当客户端持续接收到一个或者多个数据的时候,触发此事件

 - error : 当处理请求出现错误的时候

 - abort : 当取消当前请求的时候

 - load : 请求完成的时候

 - loadEnd : 请求结束的时候触发此事件

AJAX using XMLHttpRequest

前面我们提到了传统的浏览器服务器数据交互的模式,用户提交一个请求,等待服务器处理,服务器处理完请求返回数据给浏览器,浏览器重新加载页面显示结果。这样的交互模式并非十分友好,有的时候我们仅仅需要服务返回一点点的信息,但是我们还是一样要经历一系列的动作和等待,而且这这个这个过程中,我们除了等待什么事情也做不了,对于当前的操作页面也完全失去了控制。

我们希望有这样一种方式,当我们需要服务器信息的时候,我们点击页面中的某个按钮或者链接,向服务器提出数据请求,然后我们保留在当前页面继续下面的操作,当服务器返回数据的时候,我们可以很方便的把数据更新到当前页面合适的位置,这个时候,AJAX就应运而生了。

AJAX是Asynchronize JavaScript and XML的缩写,是一种实现客户端与浏览器实现异步操作的技术,底层实现方式就是利用XMLHttpRequest Object.

由于AJAX的应用非常广泛,为了简化我们代码的开发,我们可以把它开发成为一个通用的module,后续工作中,我们只需要通过这个module就可以很方便的实现AJAX的操作,具体如下所示:

 

function ajax (options) {
    options = {
        url : options.url || '',
        method : options.method || 'POST',
        type : options.type || 'xml',
        asyn : options.asyn || true,
        timeout : options.timeout || '',
        onSuccess : options.onSuccess || function () {},
        onError : options.onError || function () {},
        onComplete : options.onComplete || function () {},
        onTimeout : options.onTimeout || function () {},
        data : options.data || {}
    };
    
    var requestDone = false;
    
    try {
        parseInt(timeout);
        setTimeout(function() {
            requestDone = true;
            options.onTimeout();
        }, timeout * 1000);
    } catch (e) {}
    
    var xhr = createXHR();
    xhr.onreadystatechange = function () {
        if (xhr.readyState === 4 && !requestDone) {
            if (httpSuccess(xhr)) {
                options.onSuccess(httpData(xhr, options.type));
            } else {
                options.onError(httpData(xhr, options.type));
            }
            
            options.onComplete();
            
            xhr = null;
        }
    };
    
    if (options.method.toLowerCase() === 'post') {
        xhr.open(options.method, options.url, options.aysn);
        xhr.send(serialize(options.data));
    } else {
        options.url = addURLParameters(options.url, serialize(options.data));
        xhr.open(options.method, options.url, options.aysn);
        xhr.send(null);
    }
    
};

function createXHR() {
    if (typeof XMLHttpRequest !== undefined) {
        return new XMLHttpRequest();
    } else {
        var versions = ['MS2XML.XMLHTTP.6.0', 'MS2XML.XMLHTTP.3.0', 'MS2XML.XMLHTTP', 'Microsoft.XMLHTTP'];
        
        for (var i = 0; i < versions.length; i++) {
            try {
                return new ActiveXObject(versions[i]);
            } catch (e) {
                continue;
            }
        }
    }
};

function httpSuccess (xhr) {
    try {
        return (200 <= xhr.status < 300) 
            || (xhr.status === 304)
            || (!xhr.status && location.protocol === 'file:')
            || (window.userAgent.indexOf('Safari') !== -1 && typeof xhr.status === undefined);
    } catch (e) {
        return false;
    }
    
    return false;
};

function httpData (xhr, type) {
    var contentType = xhr.getResponseHeader('Content-Type');
    var isXMLType = !type && contentType && contentType.indexOf('xml') >= 0;
    var data = (type === 'xml') || isXMLType ? xhr.responseXML : xhr.responseText;
    if (type === 'script') {
        eval.call(window, data);
    }
    
    return data;
};

function serialize(data) {
    var results = [];
    if (Object.prototype.toString.call(data) === '[Object Array]') {
        for (var i = 0; i < data.length; i++) {
            data.push(encodeURIComponent(data[i].name) + '=' + encodeURIComponent(data[i].value));
        }
    } else {
        for (var key in data) {
            data.push(encodeURIComponent(key) + '=' + encodeURIComponent(data[key]));
        }
    }
    
    return results.join('&');
}

function addURLParameters(url, paramStr) {
    if (url.indexOf('?') === -1) {
        url += '?';
    } else {
        url += '&';
    }
    
    return url + paramStr;
}

下面是一个简单的使用例子:

 

<!DOCTYPE html>
<html>
    <head>
        <title>AJAX DEMO</title>
        <script type='text/javascript' src='ajax.js'></script>
    </head>
    <body>
        <div id='weather'>
            What's the weather like today?
            <input type='button' id='queryBtn' name='queryBtn' value='Query' />
        </div>
        <div id='console'>
            Today's Weather:<span id='result'></span>
        </div>
        <script type="text/javascript">
            window.onload = function () {
                var queryBtn = document.getElementById('queryBtn');
                queryBtn.addEventListener('click', function() {
                    ajax({
                        url : '<replace your domain url here>',
                        type : 'text',
                        onSuccess : function (data) {
                            var result = document.getElementById('result');
                            result.innerHTML = data;
                        },
                        onError : function (data) {
                            console.debug('fail');
                        }
                    });
                }, false);
            };
        </script>
    </body>
</html>

 

运行结果如下所示:

 

posted @ 2016-01-27 18:37  AndyCBLuo  阅读(5581)  评论(0编辑  收藏  举报