淺析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¶m2=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¶m2=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>
運行結果如下所示: