AJAX 在現代瀏覽器上寫AJAX主要依靠XMLHttpRequest對象: function success(text) { var textarea = document.getElementById('test-response-text'); textarea.value = text; } ...
AJAX
在現代瀏覽器上寫AJAX主要依靠XMLHttpRequest
對象:
function success(text) { var textarea = document.getElementById('test-response-text'); textarea.value = text; } function fail(code) { var textarea = document.getElementById('test-response-text'); textarea.value = 'Error code: ' + code; } var request = new XMLHttpRequest(); // 新建XMLHttpRequest對象 request.onreadystatechange = function () { // 狀態發生變化時,函數被回調 if (request.readyState === 4) { // 成功完成 // 判斷響應結果: if (request.status === 200) { // 成功,通過responseText拿到響應的文本: return success(request.responseText); } else { // 失敗,根據響應碼判斷失敗原因: return fail(request.status); } } else { // HTTP請求還在繼續... } } // 發送請求: request.open('GET', '/api/categories'); request.send(); alert('請求已發送,請等待響應...');
如果想把標準寫法和IE寫法混在一起,可以這麼寫:
var request; if (window.XMLHttpRequest) { request = new XMLHttpRequest(); } else { request = new ActiveXObject('Microsoft.XMLHTTP'); }
通過檢測window
對象是否有XMLHttpRequest
屬性來確定瀏覽器是否支持標準的XMLHttpRequest
。註意,不要根據瀏覽器的navigator.userAgent
來檢測瀏覽器是否支持某個JavaScript特性,一是因為這個字元串本身可以偽造,二是通過IE版本判斷JavaScript特性將非常複雜。
當創建了XMLHttpRequest
對象後,要先設置onreadystatechange
的回調函數。在回調函數中,通常我們只需通過readyState === 4
判斷請求是否完成,如果已完成,再根據status === 200
判斷是否是一個成功的響應。
XMLHttpRequest
對象的open()
方法有3個參數,第一個參數指定是GET
還是POST
,第二個參數指定URL地址,第三個參數指定是否使用非同步,預設是true
,所以不用寫。註意,千萬不要把第三個參數指定為false
,否則瀏覽器將停止響應,直到AJAX請求完成。 最後調用send()
方法才真正發送請求。GET
請求不需要參數,POST
請求需要把body部分以字元串或者FormData
對象傳進去。
安全限制JSONP
預設情況下,JavaScript在發送AJAX請求時,URL的功能變數名稱必須和當前頁面完全一致。
完全一致的意思是,功能變數名稱要相同 (www.example.com
和example.com
不同) ,協議要相同(http
和https
不同),埠號要相同(預設是:80
埠,它和:8080
就不同)。有的瀏覽器口子松一點,允許埠不同,大多數瀏覽器都會嚴格遵守這個限制。
JSONP,它有個限制,只能用GET請求,並且要求返回JavaScript。這種方式跨域實際上是利用了瀏覽器允許跨域引用JavaScript資源:
<html><head> <script src="http://example.com/abc.js"></script> ...</head><body>...</body></html>
JSONP通常以函數調用的形式返回,例如,返回JavaScript內容如下:
foo('data');
這樣一來,我們如果在頁面中先準備好foo()
函數,然後給頁面動態加一個<script>
節點,相當於動態讀取外域的JavaScript資源,最後就等著接收回調了。
以163的股票查詢URL為例,對於URL:http://api.money.126.net/data/feed/0000001,1399001?callback=refreshPrice,你將得到如下返回:
refreshPrice({"0000001":{"code": "0000001", ... });
因此我們需要首先在頁面中準備好回調函數:
function refreshPrice(data) { var p = document.getElementById('test-jsonp'); p.innerHTML = '當前價格:' + data['0000001'].name +': ' + data['0000001'].price + ';' + data['1399001'].name + ': ' + data['1399001'].price; }
最後用getPrice()
函數觸發:
function getPrice() { var js = document.createElement('script'), head = document.getElementsByTagName('head')[0]; js.src = 'http://api.money.126.net/data/feed/0000001,1399001?callback=refreshPrice'; head.appendChild(js); }
就完成了跨域載入數據。
CORS
如果瀏覽器支持HTML5,那麼就可以一勞永逸地使用新的跨域策略:CORS了。
CORS全稱Cross-Origin Resource Sharing,是HTML5規範定義的如何跨域訪問資源。
瞭解CORS前,我們先搞明白概念:
Origin表示本域,也就是瀏覽器當前頁面的域。當JavaScript向外域(如sina.com)發起請求後,瀏覽器收到響應後,首先檢查Access-Control-Allow-Origin
是否包含本域,如果是,則此次跨域請求成功,如果不是,則請求失敗,JavaScript將無法獲取到響應的任何數據。
用一個圖來表示就是:
假設本域是my.com
,外域是sina.com
,只要響應頭Access-Control-Allow-Origin
為http://my.com
,或者是*
,本次請求就可以成功。
可見,跨域能否成功,取決於對方伺服器是否願意給你設置一個正確的Access-Control-Allow-Origin
,決定權始終在對方手中。
上面這種跨域請求,稱之為“簡單請求”。簡單請求包括GET、HEAD和POST(POST的Content-Type類型 僅限application/x-www-form-urlencoded
、multipart/form-data
和text/plain
),並且不能出現任何自定義頭(例如,X-Custom: 12345
),通常能滿足90%的需求。
無論你是否需要用JavaScript通過CORS跨域請求資源,你都要瞭解CORS的原理。最新的瀏覽器全面支持HTML5。在引用外域資源時,除了JavaScript和CSS外,都要驗證CORS。例如,當你引用了某個第三方CDN上的字體文件時:
/* CSS */@font-face { font-family: 'FontAwesome'; src: url('http://cdn.com/fonts/fontawesome.ttf') format('truetype');}
如果該CDN服務商未正確設置Access-Control-Allow-Origin
,那麼瀏覽器無法載入字體資源。
對於PUT、DELETE以及其他類型如application/json
的POST請求,在發送AJAX請求之前,瀏覽器會先發送一個OPTIONS
請求(稱為preflighted請求)到這個URL上,詢問目標伺服器是否接受:
OPTIONS /path/to/resource HTTP/1.1 Host: bar.com Origin: http://my.com Access-Control-Request-Method: POST
伺服器必須響應並明確指出允許的Method:
HTTP/1.1 200 OK Access-Control-Allow-Origin: http://my.com Access-Control-Allow-Methods: POST, GET, PUT, OPTIONS Access-Control-Max-Age: 86400
瀏覽器確認伺服器響應的Access-Control-Allow-Methods
頭確實包含將要發送的AJAX請求的Method,才會繼續發送AJAX,否則,拋出一個錯誤。
由於以POST
、PUT
方式傳送JSON格式的數據在REST中很常見,所以要跨域正確處理POST
和PUT
請求,伺服器端必須正確響應OPTIONS
請求。
Promise
在JavaScript的世界中,所有代碼都是單線程執行的。
由於這個“缺陷”,導致JavaScript的所有網路操作,瀏覽器事件,都必須是非同步執行。非同步執行可以用回調函數實現:
function callback() { console.log('Done'); } console.log('before setTimeout()'); setTimeout(callback, 1000); // 1秒鐘後調用callback函數console.log('after setTimeout()');
觀察上述代碼執行,在Chrome的控制台輸出可以看到:
before setTimeout()
after setTimeout()
(等待1秒後)
Done
可見,非同步操作會在將來的某個時間點觸發一個函數調用。
AJAX就是典型的非同步操作。以上一節的代碼為例:
request.onreadystatechange = function () { if (request.readyState === 4) { if (request.status === 200) { return success(request.responseText); } else { return fail(request.status); } } }
把回調函數success(request.responseText)
和fail(request.status)
寫到一個AJAX操作里很正常,但是不好看,而且不利於代碼復用。
有沒有更好的寫法?比如寫成這樣:
var ajax = ajaxGet('http://...'); ajax.ifSuccess(success) .ifFail(fail);
這種鏈式寫法的好處在於,先統一執行AJAX邏輯,不關心如何處理結果,然後,根據結果是成功還是失敗,在將來的某個時候調用success
函數或fail
函數。
古人雲:“君子一諾千金”,這種“承諾將來會執行”的對象在JavaScript中稱為Promise對象。
Promise有各種開源實現,在ES6中被統一規範,由瀏覽器直接支持。
new Promise(function () {}); alert("支持Promise");
先看一個最簡單的Promise例子:生成一個0-2之間的隨機數,如果小於1,則等待一段時間後返回成功,否則返回失敗:
function test(resolve, reject) { var timeOut = Math.random() * 2; log('set timeout to: ' + timeOut + ' seconds.'); setTimeout(function () { if (timeOut < 1) { log('call resolve()...'); resolve('200 OK'); } else { log('call reject()...'); reject('timeout in ' + timeOut + ' seconds.'); } }, timeOut * 1000); }
這個test()
函數有兩個參數,這兩個參數都是函數,如果執行成功,我們將調用resolve('200 OK')
,如果執行失敗,我們將調用reject('timeout in ' + timeOut + ' seconds.')
。可以看出,test()
函數只關心自身的邏輯,並不關心具體的resolve
和reject
將如何處理結果。
有了執行函數,我們就可以用一個Promise對象來執行它,併在將來某個時刻獲得成功或失敗的結果:
var p1 = new Promise(test); var p2 = p1.then(function (result) { console.log('成功:' + result); }); var p3 = p2.catch(function (reason) { console.log('失敗:' + reason); });
變數p1
是一個Promise對象,它負責執行test
函數。由於test
函數在內部是非同步執行的,當test
函數執行成功時,我們告訴Promise對象:
// 如果成功,執行這個函數: p1.then(function (result) { console.log('成功:' + result); });
當test
函數執行失敗時,我們告訴Promise對象:
p2.catch(function (reason) { console.log('失敗:' + reason); });
Promise對象可以串聯起來,所以上述代碼可以簡化為:
new Promise(test).then(function (result) { console.log('成功:' + result); }).catch(function (reason) { console.log('失敗:' + reason); });
實際測試一下,看看Promise是如何非同步執行的:
輸出結果為:
start new Promise...
set timeout to: 1.7587399336588674 seconds.
call reject()...
Failed: timeout in 1.7587399336588674 seconds.
可見Promise最大的好處是在非同步執行的流程中,把執行代碼和處理結果的代碼清晰地分離了:
Promise還可以做更多的事情,比如,有若幹個非同步任務,需要先做任務1,如果成功後再做任務2,任何任務失敗則不再繼續並執行錯誤處理函數。
要串列執行這樣的非同步任務,不用Promise需要寫一層一層的嵌套代碼。有了Promise,我們只需要簡單地寫:
job1.then(job2).then(job3).catch(handleError);
其中,job1
、job2
和job3
都是Promise對象。
下麵的例子演示瞭如何串列執行一系列需要非同步計算獲得結果的任務:
start new Promise...
calculating 123 x 123...
calculating 15129 + 15129...
calculating 30258 x 30258...
calculating 915546564 + 915546564...
Got value: 1831093128
setTimeout
可以看成一個模擬網路等非同步執行的函數。現在,我們把上一節的AJAX非同步執行函數轉換為Promise對象,看看用Promise如何簡化非同步處理:
除了串列執行若幹非同步任務外,Promise還可以並行執行非同步任務。
試想一個頁面聊天系統,我們需要從兩個不同的URL分別獲得用戶的個人信息和好友列表,這兩個任務是可以並行執行的,用Promise.all()
實現如下:
var p1 = new Promise(function (resolve, reject) { setTimeout(resolve, 500, 'P1'); });
var p2 = new Promise(function (resolve, reject) { setTimeout(resolve, 600, 'P2'); }); // 同時執行p1和p2,併在它們都完成後執行then: Promise.all([p1, p2]).then(function (results) { console.log(results); // 獲得一個Array: ['P1', 'P2'] });
有些時候,多個非同步任務是為了容錯。比如,同時向兩個URL讀取用戶的個人信息,只需要獲得先返回的結果即可。這種情況下,用Promise.race()
實現:
var p1 = new Promise(function (resolve, reject) { setTimeout(resolve, 500, 'P1'); });
var p2 = new Promise(function (resolve, reject) { setTimeout(resolve, 600, 'P2'); }); Promise.race([p1, p2]).then(function (result) { console.log(result); // 'P1' });
由於p1
執行較快,Promise的then()
將獲得結果'P1'
。p2
仍在繼續執行,但執行結果將被丟棄。
如果我們組合使用Promise,就可以把很多非同步任務以並行和串列的方式組合起來執行。
jQuery ajax
jQuery在全局對象jQuery
(也就是$
)綁定了ajax()
函數,可以處理AJAX請求。ajax(url, settings)
函數需要接收一個URL和一個可選的settings
對象,常用的選項如下:
-
async:是否非同步執行AJAX請求,預設為
true
,千萬不要指定為false
; -
method:發送的Method,預設為
'GET'
,可指定為'POST'
、'PUT'
等; -
contentType:發送POST請求的格式,預設值為
'application/x-www-form-urlencoded; charset=UTF-8'
,也可以指定為text/plain
、application/json
; -
data:發送的數據,可以是字元串、數組或object。如果是GET請求,data將被轉換成query附加到URL上,如果是POST請求,根據contentType把data序列化成合適的格式;
-
headers:發送的額外的HTTP頭,必須是一個object;
-
dataType:接收的數據格式,可以指定為
'html'
、'xml'
、'json'
、'text'
等,預設情況下根據響應的Content-Type
猜測。
下麵的例子發送一個GET請求,並返回一個JSON格式的數據:
var jqxhr = $.ajax('/api/categories', { dataType: 'json' });// 請求已經發送了
不過,如何用回調函數處理返回的數據和出錯時的響應呢?
function ajaxLog(s) { var txt = $('#test-response-text'); txt.val(txt.val() + '\n' + s); } $('#test-response-text').val(''); var jqxhr = $.ajax('/api/categories', { dataType: 'json' }).done(function (data) { ajaxLog('成功, 收到的數據: ' + JSON.stringify(data)); }).fail(function (xhr, status) { ajaxLog('失敗: ' + xhr.status + ', 原因: ' + status); }).always(function () { ajaxLog('請求完成: 無論成功或失敗都會調用'); });
get
對常用的AJAX操作,jQuery提供了一些輔助方法。由於GET請求最常見,所以jQuery提供了get()
方法,可以這麼寫:
var jqxhr = $.get('/path/to/resource', {
name: 'Bob Lee', check: 1
});
第二個參數如果是object,jQuery自動把它變成query string然後加到URL後面,實際的URL是:
/path/to/resource?name=Bob%20Lee&check=1
這樣我們就不用關心如何用URL編碼並構造一個query string了。
post
post()
和get()
類似,但是傳入的第二個參數預設被序列化為application/x-www-form-urlencoded
:
var jqxhr = $.post('/path/to/resource', {
name: 'Bob Lee',
check: 1
});
實際構造的數據name=Bob%20Lee&check=1
作為POST的body被髮送。
getJSON
由於JSON用得越來越普遍,所以jQuery也提供了getJSON()
方法來快速通過GET獲取一個JSON對象:
var jqxhr = $.getJSON('/path/to/resource', { name: 'Bob Lee', check: 1}).done(function (data) { // data已經被解析為JSON對象了 });
安全限制
jQuery的AJAX完全封裝的是JavaScript的AJAX操作,所以它的安全限制和前面講的用JavaScript寫AJAX完全一樣。
如果需要使用JSONP,可以在ajax()
中設置jsonp: 'callback'
,讓jQuery實現JSONP跨域載入數據。