分別使用 XHR、jQuery 和 Fetch 實現 AJAX
本文詳細講述如何使用原生 JS、jQuery 和 Fetch 來實現 AJAX。
AJAX 即 Asynchronous JavaScript and XML,異步的 JavaScript 和 XML。使用 AJAX 可以無刷新地向服務端發送請求接收服務端響應,并更新頁面。
一、原生 JS 實現 AJAX
JS 實現 AJAX 主要基于瀏覽器提供的 XMLHttpRequest(XHR)類,所有現代瀏覽器(IE7+、Firefox、Chrome、Safari 以及 Opera)均內建 XMLHttpRequest 對象。
1. 獲取XMLHttpRequest對象
// 獲取XMLHttpRequest對象
var xhr = new XMLHttpRequest();
如果需要兼容老版本的 IE (IE5, IE6) 瀏覽器,則可以使用 ActiveX 對象:
var xhr;
if (window.XMLHttpRequest) { // Mozilla, Safari...
xhr = new XMLHttpRequest();
} else if (window.ActiveXObject) { // IE
try {
xhr = new ActiveXObject('Msxml2.XMLHTTP');
} catch (e) {
try {
xhr = new ActiveXObject('Microsoft.XMLHTTP');
} catch (e) {}
}
}
2. 發送一個 HTTP 請求
接下來,我們需要打開一個URL,然后發送這個請求。分別要用到 XMLHttpRequest 的 open() 方法和 send() 方法。
// GET
var xhr;
if (window.XMLHttpRequest) { // Mozilla, Safari...
xhr = new XMLHttpRequest();
} else if (window.ActiveXObject) { // IE
try {
xhr = new ActiveXObject('Msxml2.XMLHTTP');
} catch (e) {
try {
xhr = new ActiveXObject('Microsoft.XMLHTTP');
} catch (e) {}
}
}
if (xhr) {
xhr.open('GET', '/api?username=admin&password=root', true);
xhr.send(null);
}
// POST
var xhr;
if (window.XMLHttpRequest) { // Mozilla, Safari...
xhr = new XMLHttpRequest();
} else if (window.ActiveXObject) { // IE
try {
xhr = new ActiveXObject('Msxml2.XMLHTTP');
} catch (e) {
try {
xhr = new ActiveXObject('Microsoft.XMLHTTP');
} catch (e) {}
}
}
if (xhr) {
xhr.open('POST', '/api', true);
// 設置 Content-Type 為 application/x-www-form-urlencoded
// 以表單的形式傳遞數據
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
xhr.send('username=admin&password=root');
}
open() 方法有三個參數:
-
open() 的第一個參數是 HTTP 請求方式 – GET,POST,HEAD 或任何服務器所支持的您想調用的方式。按照HTTP規范,該參數要大寫;否則,某些瀏覽器(如Firefox)可能無法處理請求。有關HTTP請求方法的詳細信息可參考 https://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html
-
第二個參數是請求頁面的 URL。由于同源策略(Same origin policy)該頁面不能為第三方域名的頁面。同時一定要保證在所有的頁面中都使用準確的域名,否則調用 open() 會得到 permission denied 的錯誤提示。
-
第三個參數設置請求是否為異步模式。如果是 TRUE ,JavaScript 函數將繼續執行,而不等待服務器響應。這就是 AJAX 中的 A。
如果第一個參數是 GET ,則可以直接將參數放在 url 后面,如: http://nodejh.com/api?name=admint&password=root 。
如果第一個參數是 POST ,則需要將參數寫在 send() 方法里面。send() 方法的參數可以是任何想送給服務器的數據。這時數據要以字符串的形式送給服務器,如: name=admint&password=root 。或者也可以傳遞 JSON 格式的數據:
// 設置 Content-Type 為 application/json
xhr.setRequestHeader('Content-Type', 'application/json');
// 傳遞 JSON 字符串
xhr.send(JSON.stringify({ username:'admin', password:'root' }));
如果不設置請求頭,原生 AJAX 會默認使用 Content-Type 是 text/plain;charset=UTF-8 的方式發送數據。
關于 Content-Type 更詳細的內容,將在以后的文章中解釋說明。
3. 處理服務器的響應
當發送請求時,我們需要指定如何處理服務器的響應,我們需要用到 onreadystatechange 屬性來檢測服務器的響應狀態。使用 onreadystatechange 有兩種方式,一是直接 onreadystatechange 屬性指定一個可調用的函數名,二是使用一個匿名函數:
// 方法一 指定可調用的函數
xhr.onreadystatechange = onReadyStateChange;
function onReadyStateChange() {
// do something
}
// 方法二 使用匿名函數
xhr.onreadystatechange = function(){
// do the thing
};
接下來我們需要在內部利用 readyState 屬性來獲取當前的狀態,當 readyState 的值為 4,就意味著一個完整的服務器響應已經收到了,接下來就可以處理該響應:
// readyState的取值如下
// 0 (未初始化)
// 1 (正在裝載)
// 2 (裝載完畢)
// 3 (交互中)
// 4 (完成)
if (xhr.readyState === 4) {
// everything is good, the response is received
} else {
// still not ready
}
完整代碼如下:
// POST
var xhr;
if (window.XMLHttpRequest) { // Mozilla, Safari...
xhr = new XMLHttpRequest();
} else if (window.ActiveXObject) { // IE
try {
xhr = new ActiveXObject('Msxml2.XMLHTTP');
} catch (e) {
try {
xhr = new ActiveXObject('Microsoft.XMLHTTP');
} catch (e) {}
}
}
if (xhr) {
xhr.onreadystatechange = onReadyStateChange;
xhr.open('POST', '/api', true);
// 設置 Content-Type 為 application/x-www-form-urlencoded
// 以表單的形式傳遞數據
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
xhr.send('username=admin&password=root');
}
// onreadystatechange 方法
function onReadyStateChange() {
// 該函數會被調用四次
console.log(xhr.readyState);
if (xhr.readyState === 4) {
// everything is good, the response is received
if (xhr.status === 200) {
console.log(xhr.responseText);
} else {
console.log('There was a problem with the request.');
}
} else {
// still not ready
console.log('still not ready...');
}
}
當然我們可以用onload來代替onreadystatechange等于4的情況,因為onload只在狀態為4的時候才被調用,代碼如下:
xhr.onload = function () { // 調用onload
if (xhr.status === 200) { // status為200表示請求成功
console.log('執行成功');
} else {
console.log('執行出錯');
}
}
然而需要注意的是,IE對 onload 屬性的支持并不友好。除了 onload 還有以下幾個屬性也可以用來監測響應狀態:
-
onloadstart
-
onprogress
-
onabort
-
ontimeout
-
onerror
-
onloadend
二、 jQuery 實現 AJAX
jQuery 作為一個使用人數最多的庫,其 AJAX 很好的封裝了原生 AJAX 的代碼,在兼容性和易用性方面都做了很大的提高,讓 AJAX 的調用變得非常簡單。下面便是一段簡單的 jQuery 的 AJAX 代碼:
$.ajax({
method: 'POST',
url: '/api',
data: { username: 'admin', password: 'root' }
})
.done(function(msg) {
alert( 'Data Saved: ' + msg );
});
對比原生 AJAX 的實現,使用 jQuery 就異常簡單了。當然我們平時用的最多的,是下面兩種更簡單的方式:
// GET
$.get('/api', function(res) {
// do something
});
// POST
var data = {
username: 'admin',
password: 'root'
};
$.post('/api', data, function(res) {
// do something
});
三、Fetch API
使用 jQuery 雖然可以大大簡化 XMLHttpRequest 的使用,但 XMLHttpRequest 本質上但并不是一個設計優良的 API:
-
不符合關注分離(Separation of Concerns)的原則
-
配置和調用方式非常混亂
-
使用事件機制來跟蹤狀態變化
-
基于事件的異步模型沒有現代的 Promise,generator/yield,async/await 友好
Fetch API 旨在修正上述缺陷,它提供了與 HTTP 語義相同的 JS 語法,簡單來說,它引入了 fetch() 這個實用的方法來獲取網絡資源。
Fetch 的瀏覽器兼容圖如下:
原生支持率并不高,幸運的是,引入下面這些 polyfill 后可以完美支持 IE8+:
-
由于 IE8 是 ES3,需要引入 ES5 的 polyfill: es5-shim, es5-sham
-
引入 Promise 的 polyfill: es6-promise
-
引入 fetch 探測庫: fetch-detector
-
引入 fetch 的 polyfill: fetch-ie8
-
可選:如果你還使用了 jsonp,引入 fetch-jsonp
-
可選:開啟 Babel 的 runtime 模式,現在就使用 async/await
1. 一個使用 Fetch 的例子
先看一個簡單的 Fetch API 的例子 :chestnut: :
fetch('/api').then(function(response) {
return response.json();
}).then(function(data) {
console.log(data);
}).catch(function(error) {
console.log('Oops, error: ', error);
});
使用 ES6 的箭頭函數后:
fetch('/api').then(response => response.json())
.then(data => console.log(data))
.catch(error => console.log('Oops, error: ', error))
可以看出使用Fetch后我們的代碼更加簡潔和語義化,鏈式調用的方式也使其更加流暢和清晰。但這種基于 Promise 的寫法還是有 Callback 的影子,我們還可以用 async/await 來做最終優化:
async function() {
try {
let response = await fetch(url);
let data = response.json();
console.log(data);
} catch (error) {
console.log('Oops, error: ', error);
}
}
使用 await 后,寫代碼就更跟同步代碼一樣。 await 后面可以跟 Promise 對象,表示等待 Promise resolve() 才會繼續向下執行,如果 Promise 被 reject() 或拋出異常則會被外面的 try...catch 捕獲。
Promise,generator/yield,await/async 都是現在和未來 JS 解決異步的標準做法,可以完美搭配使用。這也是使用標準 Promise 一大好處。
2. 使用 Fetch 的注意事項
-
Fetch 請求默認是不帶 cookie,需要設置 fetch(url, {credentials: 'include'}) `
-
服務器返回 400,500 錯誤碼時并不會 reject,只有網絡錯誤這些導致請求不能完成時,fetch 才會被 reject
接下來將上面基于 XMLHttpRequest 的 AJAX 用 Fetch 改寫:
var options = {
method: 'POST',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
},
body: JSON.stringify({ username: 'admin', password: 'root' }),
credentials: 'include'
};
fetch('/api', options).then(response => response.json())
.then(data => console.log(data))
.catch(error => console.log('Oops, error: ', error))
Github Issue: https://github.com/nodejh/nodejh.github.io/issues/15