在這篇文章,我使用 Node.js作為後端。沒錯,這就可以全棧(瀏覽器和伺服器)JS了。Node.js 是很簡潔的,我鼓勵你能在 Github下載demo,並關注該項目。下面是伺服器端的代碼:
// app.jsvar app = http.createServer(function(req, res){ if(req.url.indexOf("/scripts/") >= 0){ render(req.url.slice(1), "application/javascript", httpHandler); } else if(req.headers['x-requested-with'] === 'XMLHttpRequest'){ // Send Ajax response } else{ render('views/index.html', 'text/html', httpHandler); }}
該程式碼片段通過檢測請求 URL,確定該app返回的相應內容。如果該請求來自 scripts 目錄,那麼伺服器將返回內容類型(content type)為 application/javascript 的相應檔案。如果要求標頭部的 x-requested-with 被設為 XMLHttpRequest,那麼該請求是 Ajax 請求,然後返回相應資料。除了以上兩種情況,伺服器將會返回 views/index.html。
下面我將會展開上一程式碼片段處理 Ajax 請求的注釋部分進行深入講解。在 Node.js端,我已處理了 render 和 httpHandler 的體力活:
// app.jsfunction render(path, contentType, fn) { fs.readFile(__dirname + '/' + path, 'utf-8', function (err, str) { fn(err, str, contentType); });}var httpHandler = function (err, str, contentType) { if (err) { res.writeHead(500, {'Content-Type': 'text/plain'}); res.end('An error has occured: ' + err.message); } else { res.writeHead(200, {'Content-Type': contentType}); res.end(str); }};
render 函數非同步讀取被請求檔案的內容。該函數向被作為回呼函數的 httpHandler 傳遞一個引用。
httpHandler 函數檢測 error 對象是否存在(如:被請求檔案不能被開啟,該對象就會存在)。另外,指定類型是好的做法,那麼伺服器返回的檔案內容就會擁有適當的 HTTP 狀態代碼(status code)和內容類型(content type)。
測試 API
讓我們為後端API編寫一些單元測試,從而確保它們能正確運行。對於這類測試,我會請求 supertest 和 mocha協助。
// test/app.request.jsit("responds with html", function(done){ request(app) .get("/") .expect("Content-Type", /html/) .expect(200, done);});it('responds with javascript', function (done) {request(app) .get('/scripts/index.js') .expect('Content-Type', /javascript/) .expect(200, done);});it('responds with json', function (done) {request(app) .get('/') .set('X-Requested-With', 'XMLHttpRequest') .expect('Content-Type', /json/) .expect(200, done);});
這些測試確保了我們的 app 對於不同請求能返回正確的內容類型(content type)和HTTP 狀態代碼(status code)。一旦你安裝了這些依賴,那麼你就能使用命令 npm test 運行這些測試。
介面
現在,讓我們看看使用者介面的 HTML 程式碼:
// views/index.html<h1>Vanilla Ajax without jQuery</h1><button id="retrieve" data-url="/">Retrieve</button><p id="results"></p>
上述的 HTML 程式碼看起來很簡潔。沒錯,正如你所看到的,所有令人興奮的事情都發生在 JavaScript。
onreadystate vs onload
如果你看過任何一本權威的、關於 Ajax 的書,你可能會發現 onreadystate 在書上隨處可見。該回呼函數需要通過嵌套的 ifs 或多個 case 陳述式完成,這使得難以記憶。讓我們再次回顧 onreadystate 和 onload 事件。
(function() { var retrieve = document.getElementById('retrieve'), results = document.getElementById('results'), toReadyStateDescription = function(state) { switch (state) { case 0: return 'UNSENT'; case 1: return 'OPENED'; case 2: return 'HEADERS_RECEIVED'; case 3: return 'LOADING'; case 4: return 'DONE'; default: return ''; } }; retrieve.addEventListener('click', function(e) { var oReq = new XMLHttpRequest(); oReq.onload = function() { console.log('Inside the onload event'); }; oReq.onreadystatechange = function() { console.log('Inside the onreadystatechange ev![此處輸入圖片的描述][1]ent with readyState: ' + toReadyStateDescription(oReq.readyState)); }; oReq.open('GET', e.target.dataset.url, true); oReq.send(); });}());
上述代碼會在 控制台(console) 輸出以下語句:
onreadystatechange 事件能在請求的任何過程中被觸發。如能在每個請求前、請求末。但根據文檔,onload 事件只會在請求成功後觸發。又因為 onload 事件是一個常見的 API,所以你能在很短時間內掌握它。onreadystatechange 事件可作為後備(原文是backwards compatible 向後相容?)方案。而 onload 事件應該是你的首選。而且 onload 事件與 jQuery 的 success 回呼函數類似,難道不是嗎?
###佈建要求頭部
jQuery 私下幫你佈建要求頭部了,所以後端能檢測這是一個 Ajax 請求。一般來說,後端並不關心 GET 請求是從哪而來,只要能返回正確的響應即可。當你相用同樣的 web API 返回 Ajax 或 HTML 時,這就派上用場了。讓我們看看如何通過原生 JavaScript 佈建要求頭部:
var oReq = new XMLHttpRequest();oReq.open('GET', e.target.dataset.url, true);oReq.setRequestHeader('X-Requested-With', 'XMLHttpRequest');oReq.send();
與此同時,我們在 Node.js 做一個檢測:
if (req.headers['x-requested-with'] === 'XMLHttpRequest') { res.writeHead(200, {'Content-Type': 'application/json'}); res.end(JSON.stringify({message: 'Hello World!'}));}
正如你所看到的,原生 Ajax 是一個靈活且現代化的前端 API。你可以利用要求標頭部做很多事情,其中一種是版本控制。例如,我想讓某個 web API 支援多個版本。但我又不想利用 URL,取而代之的是:通過佈建要求頭部,使用戶端能選擇它們想要的版本。所以,我們能這樣佈建要求頭部:
oReq.setRequestHeader('x-vanillaAjaxWithoutjQuery-version', '1.0');
然後,在後端寫上相應代碼:
if (req.headers['x-requested-with'] === 'XMLHttpRequest' && req.headers['x-vanillaajaxwithoutjquery-version'] === '1.0') { // Send Ajax response}
我們能利用 Node.js 為我們提供的 headers 對象進行相應檢測。而唯一需要注意的地方是:以小寫字母讀取它們。
響應類型
你可能想知道為什麼 responseText 返回的是字串,而不是能被我們操作的普通 JSON(Plain Old JSON)。原來是因為我沒有設定合適的 responseType 屬性。該 Ajax 屬性會很好地告訴前端 API 所期望伺服器返回的資料類型。所以,我們要好好利用它:
var oReq = new XMLHttpRequest();oReq.onload = function (e) { results.innerHTML = e.target.response.message;};oReq.open('GET', e.target.dataset.url, true);oReq.responseType = 'json';oReq.send();
哇,這樣我們就不必再對返回的純文字解析為 JSON 了,我們能告訴 API 我們期待接收的資料類型。該特性幾乎得到了所有最新主流瀏覽器的支援。當然,jQuery 會自動幫我們轉為適當的類型。但現在的原生 JavaScript 也具有方便的、完成同樣事件的方法。 原生 Ajax 已經支援很多其它響應類型,如 XML。
但遺憾的是,到 IE11 為止,Team Dev仍未對 xhr.responseType='json' 進行支援。雖然該特性目前在 Microsoft Edge 得到支援。但這個 Bug 提出幾乎兩年了。我堅信 Microsoft 團隊一直在努力地改進瀏覽器。讓我們期待 Microsoft Edge、aka Project Spartan 當初提出的承諾。
當然,你可以這個解決這個 IE 問題:
oReq.onload = function (e) { var xhr = e.target; if (xhr.responseType === 'json') { results.innerHTML = xhr.response.message; } else { results.innerHTML = JSON.parse(xhr.responseText).message; }};
避免緩衝
對 Ajax 請求進行緩衝的瀏覽器特性都快被我們忘記了。例如,IE 就預設是這樣。我還曾因此導致我的 Ajax 不執行而苦惱了幾個小時。幸運的是,jQuery 預設清除瀏覽器緩衝。當然,你能在純 Ajax 達到該目的,而且相當簡單:
var bustCache = '?' + new Date().getTime();oReq.open('GET', e.target.dataset.url + bustCache, true);
查看 jQuery 文檔,可知道 jQuery 在每個請求(GET)後面追加一個時間戳記作為查詢字串。這在某個程度上讓請求變得獨一無二,並避免瀏覽器緩衝。每當你觸發 HTTP Ajax 請求,你能看到類似如下請求:
OK!這就沒有戲劇性的事情發生了。
總結
我希望你能喜歡這篇關於原生 Ajax 的文章。Ajax 在過去某段時間裡,被人們看作是一種可怕的怪獸。而事實上,我們已經覆蓋了原生 Ajax 所有基礎知識。
最後,我會留給你一個簡潔的方式進行Ajax調用:
var oReq = new XMLHttpRequest();oReq.onload = function (e) { results.innerHTML = e.target.response.message;};oReq.open('GET', e.target.dataset.url + '?' + new Date().getTime(), true);oReq.responseType = 'json';oReq.send();