JavaScript Ajax + Promise


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.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內容如下:

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跨域加載數據。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM