JavaScript Ajax + Promise

AJAX

在現代瀏覽器上寫AJAX主要依靠XMLHttpRequest對象:javascript

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寫法混在一塊兒,能夠這麼寫:html

var request;
if (window.XMLHttpRequest) {
    request = new XMLHttpRequest();
} else {
    request = new ActiveXObject('Microsoft.XMLHTTP');
}

經過檢測window對象是否有XMLHttpRequest屬性來肯定瀏覽器是否支持標準的XMLHttpRequest。注意,不要根據瀏覽器的navigator.userAgent來檢測瀏覽器是否支持某個JavaScript特性,一是由於這個字符串自己能夠僞造,二是經過IE版本判斷JavaScript特性將很是複雜。java

當建立了XMLHttpRequest對象後,要先設置onreadystatechange的回調函數。在回調函數中,一般咱們只需經過readyState === 4判斷請求是否完成,若是已完成,再根據status === 200判斷是不是一個成功的響應。ajax

XMLHttpRequest對象的open()方法有3個參數,第一個參數指定是GET仍是POST,第二個參數指定URL地址,第三個參數指定是否使用異步,默認是true,因此不用寫。注意,千萬不要把第三個參數指定爲false,不然瀏覽器將中止響應,直到AJAX請求完成。 最後調用send()方法才真正發送請求。GET請求不須要參數,POST請求須要把body部分以字符串或者FormData對象傳進去。json

安全限制JSONP

默認狀況下,JavaScript在發送AJAX請求時,URL的域名必須和當前頁面徹底一致。api

徹底一致的意思是,域名要相同 (www.example.comexample.com不一樣) ,協議要相同(httphttps不一樣),端口號要相同(默認是:80端口,它和:8080就不一樣)。有的瀏覽器口子鬆一點,容許端口不一樣,大多數瀏覽器都會嚴格遵照這個限制。跨域

JSONP,它有個限制,只能用GET請求,而且要求返回JavaScript。這種方式跨域其實是利用了瀏覽器容許跨域引用JavaScript資源:數組

<html><head>
    <script src="http://example.com/abc.js"></script>
    ...</head><body>...</body></html>

JSONP一般以函數調用的形式返回,例如,返回JavaScript內容以下:promise

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-Originhttp://my.com,或者是*,本次請求就能夠成功。

可見,跨域可否成功,取決於對方服務器是否願意給你設置一個正確的Access-Control-Allow-Origin,決定權始終在對方手中。

上面這種跨域請求,稱之爲「簡單請求」。簡單請求包括GET、HEAD和POST(POST的Content-Type類型 僅限application/x-www-form-urlencodedmultipart/form-datatext/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,不然,拋出一個錯誤。

因爲以POSTPUT方式傳送JSON格式的數據在REST中很常見,因此要跨域正確處理POSTPUT請求,服務器端必須正確響應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()函數只關心自身的邏輯,並不關心具體的resolvereject將如何處理結果。

有了執行函數,咱們就能夠用一個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是如何異步執行的:

'use strict';

// 清除log:
var logging = document.getElementById('test-promise-log');
while (logging.children.length > 1) {
    logging.removeChild(logging.children[logging.children.length - 1]);
}

// 輸出log到頁面:
function log(s) {
    var p = document.createElement('p');
    p.innerHTML = s;
    logging.appendChild(p);
}

new Promise(function (resolve, reject) {
    log('start new Promise...');
    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);
}).then(function (r) {
    log('Done: ' + r);
}).catch(function (reason) {
    log('Failed: ' + reason);
});

輸出結果爲:

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);

其中,job1job2job3都是Promise對象。

 

下面的例子演示瞭如何串行執行一系列須要異步計算得到結果的任務:

 
var logging = document.getElementById('test-promise2-log');
while (logging.children.length > 1) {
    logging.removeChild(logging.children[logging.children.length - 1]);
}

function log(s) {
    var p = document.createElement('p');
    p.innerHTML = s;
    logging.appendChild(p);
}

// 0.5秒後返回input*input的計算結果:
function multiply(input) {
    return new Promise(function (resolve, reject) {
        log('calculating ' + input + ' x ' + input + '...');
        setTimeout(resolve, 500, input * input);
    });
}

// 0.5秒後返回input+input的計算結果:
function add(input) {
    return new Promise(function (resolve, reject) {
        log('calculating ' + input + ' + ' + input + '...');
        setTimeout(resolve, 500, input + input);
    });
}

var p = new Promise(function (resolve, reject) {
    log('start new Promise...');
    resolve(123);
});

p.then(multiply)
 .then(add)
 .then(multiply)
 .then(add)
 .then(function (result) {
    log('Got value: ' + result);
});

start new Promise...

calculating 123 x 123...

calculating 15129 + 15129...

calculating 30258 x 30258...

calculating 915546564 + 915546564...

Got value: 1831093128

setTimeout能夠當作一個模擬網絡等異步執行的函數。如今,咱們把上一節的AJAX異步執行函數轉換爲Promise對象,看看用Promise如何簡化異步處理:

// ajax函數將返回Promise對象:
function ajax(method, url, data) {
    var request = new XMLHttpRequest();
    return new Promise(function (resolve, reject) {
        request.onreadystatechange = function () {
            if (request.readyState === 4) {
                if (request.status === 200) {
                    resolve(request.responseText);
                } else {
                    reject(request.status);
                }
            }
        };
        request.open(method, url);
        request.send(data);
    });
}

var log = document.getElementById('test-promise-ajax-result');
var p = ajax('GET', '/api/categories');
p.then(function (text) { // 若是AJAX成功,得到響應內容
    log.innerText = text;
}).catch(function (status) { // 若是AJAX失敗,得到響應代碼
    log.innerText = 'ERROR: ' + status;
});

除了串行執行若干異步任務外,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/plainapplication/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跨域加載數據。

相關文章
相關標籤/搜索