XMLHttpRequest (XHR), 是最常見的 JavaScript HTTP Client,
常見於 Web 應用、Debug、API 測試… 😆。
就算不會 JavaScript,閱讀本篇後,您將擁有基本實作能力,
不清楚如何執行 JavaScript 的話,可參閱 W3SChools,或利用 CodePen 進行練習 😁。
目錄
XMLHttpRequest
XMLHttpRequest (XHR) 是 JavaScript 著名的 古老 API,
頁面 能透過它操作 HTTP 請求,進行網路作業,
擷取資料的同時,卻 不需 進行 頁面重載 (page reload)!
這大大地增加 Web 效能 與 體驗,這種 非同步的 Web 應用架構,稱為 AJAX,
是 過去 許多 Web 應用的基石 (Ajax != XHR,僅是最常見的應用模式)。
「過去」,意味著 現在不推薦使用 !
「過去」,意味著 現在不推薦使用 !
「過去」,意味著 現在不推薦使用 !
XMLHttpRequest (XHR) 已經十幾年了,
其結構設計,已無法應對現今複雜的 Web 環境,
且容易陷入 回調函式地獄 (Callback Hell) 。
儘管如此,仍值得了解其用法,
對於理解 其他 API、維護舊系統 皆能有所幫助。
基本使用
以 XMLHttpRequest (XHR) 對 https://gank.io/api/random/data/福利/20,
送出 GET 請求,並將回應提供的 影像 URI,添加至頁面為例:
一、實例物件
首先,以 建構元 實例一個 XMLHttpRequest 物件:
var xhr = new XMLHttpRequest();
二、設定請求
使用 open()
方法,設定請求,
請求方法 除了 GET 與 POST,還能使用 PUT、DELETE、HEAD、OPTIONS !
並且預設使用了 非同步 (async) 的方式接收回應:
/*
* {string} 請求方法 (method)
* {string} 目標 url
* {boolean} 非同步 [async] -- 可選
* {string} 使用者 [user] -- 可選
* {string} 密碼 [password] -- 可選
*/
xhr.open("GET", "https://gank.io/api/random/data/福利/20");
非同步/異步 是指執行任務 (e.g., 請求) 卡住了不會傻傻地等,
而是先繼續做其他事,等回應或失敗了,再來處理後續事宜。
無論是何種 HTTP Client,預設幾乎都是 非同步,
才不會造成 瀏覽器 為等待 Server 回應,阻斷 (block) 執行,而造成 卡死。
三、請求表頭
再來可以透過 setRequestHeader()
方法,設置請求訊息的表頭欄位,
基於安全考量,這些表頭欄位: Forbidden_header_name 是禁止添加的喔,
會由 使用者代理 (e.g., 瀏覽器) 幫你搞定 !
四、監聽事件
使用 非同步,需監聽 load
事件,
讓 回應完成時 能執行相對應的函式 — 回調函式 (callback):
// 非同步取得回應
xhr.onload = function () {
.............
...處理回應...
.............
};
五、發送請求
一切準備就緒後,就送出請求啦 !
send
方法中的參數,是請求訊息的 酬載 (payload) 內容,
由於此次使用的是 GET 方法,不得 送出 酬載,因此設為空值:
xhr.send(null);
六、處理回應
剛剛的 監聽事件,尚未說明如何處理回應。
回應的文字 text,能透過 responseText
屬性 取得,
若 Server 回應的是 XML 或 XHTML,則使用 responseXML
屬性,
若回應的是 常見的 JSON,則可以使用 JSON.parse()
方法,解析 responseText
屬性。
// 取得回應的 Content-Type 表頭欄位
// 以決定如何處理回應
var type = xhr.getResponseHeader("Content-Type");
// 建構元 (strategy, response)
var handler;
// 使用簡易的 正規表達式,判斷媒體類型
if (type.match(/^application\/json/)) {
handler = new Handler(jsonHandler, JSON.parse(xhr.responseText));
} else if (type.match(/^application\/xml/)) {
handler = new Handler(textHandler, xhr.responseXML);
} else {
handler = new Handler(textHandler, xhr.responseText);
}
handler.handleResponse();
這裡為求簡潔省略 回應狀態碼的判斷,
實務中必不能省!
可透過簡單的小於、等於,
判斷是 2xx 的成功、3xx 的重新導向 或 4xx 客戶端錯誤… :
if (200 <= xhr.status && xhr.status <= 299)
也可以裝逼點,將 回應狀態碼 除以 100,
再用 地板函數 (floor) 將小數點去掉 :
if (Math.floor(xhr.status / 100) == 2) {
...成功...
} else {
...其他...
}
七、輸出結果
最後,隨便寫一個 DOM 操作,
將回應的 URI,置於 <img>
的 src 屬性,即可享用勝利的果實:
// 簡易處理 JSON 回應
function jsonHandler(response) {
let data = response.results;
// 建立緩衝的文件片段 docFrag
let docFrag = document.createDocumentFragment();
for (var i = 0, l = data.length; i < l; i++) {
var url = data[i].url;
var img = document.createElement("img");
img.src = url;
img.width = 300;
// 將 img 添加至 docFrag
docFrag.appendChild(img);
}
// 將含有多個 img 的 docFrag
// 一次新增至 HTML 的 body 中
document.body.appendChild(docFrag);
}
1. 將 img 添加至 docFrag :
2. 將含有多個 img 的 docFrag
一次新增至 HTML 的 body 中 :
完成 ! (一切都是為了學習),
可前往 這裡 觀看成果 :
二進制回應
上方 處理回應,只說明使用 responseText
來處理 回應文字,
於是,一個明顯的問題:
如果回應是 二進制 資料呢 ?
對此影像送出 GET 請求 (此處使用 相對 URL) 為例:
xhr.open("GET", "its-you.jpg");
overrideMimeType — 不建議
早期 XMLHttpRequest (XHR) 解析二進制檔案是痛苦的事,
於是 許多人 利用 overrideMimeType
方法,複寫 回應的 Content-Type 表頭欄位 (忽視回應的 內容類型),
將影像視為 Unicode 字串,並以轉換格式的方式處理:
(若看到 0xff 會頭暈的話,可參考 進制簡介 與 完整的程式碼 喔 😁)
// 複寫回應的 Content-Type
xhr.overrideMimeType('text/plain; charset=x-user-defined');
// 非同步取得回應
xhr.onload = function () {
let response = xhr.responseText;
let img = document.createElement("img");
let data = "";
for (var i = 0; i < response.length; i += 1) {
var u = response.charCodeAt(i) & 0xff;
data += String.fromCharCode(u);
}
img.src = "data:image/jpeg;base64," + window.btoa(data);
// 新增 img 元素至頁面
document.body.appendChild(img);
};
最後透過這種 <img src="data:image/jpeg;base64,/9j/4AA......>
,
將內容進行編碼的 Data URI 顯示:
Blob
上述方法雖然可行,但是並不通用,且帶來許多問題 (e.g., 效能、編碼),
處理二進制,許多人應該會直覺聯想到 — Blob !
Blob (Binary Large Object) 是 JavaScript 的二進位資料,
做為許多 JavaScript API 的資料交換機制,
操作 檔案的 File 物件,便是 Blob 的子型別 !
指定內容類型
要以 XHR 使用 blob,首先得在發送請求前,
將 XHR (Level 2) 的 responseType
屬性設置為「blob」:
xhr.responseType = 'blob';
處理回應
接下來就等著處理回應啦 !
需注意的是,一旦聲明 responseType
回應類型屬性,
即必須 改用 response
屬性 處理回應,而非 responseText 或 responseXML 屬性:
var blob = xhr.response;
建立 Blob URI
存取 Blob 內容,最簡單的方式就是使用 Blob URI (或稱 Object URI),
就像 http URI 是 https://xxxxxxxx ,Blob URI 長成 blob://xxxxxxxxx。
其生命週期很間單,一旦關閉 該頁面 blob URI 即失效 (內容可能暫存於記憶體或硬碟),
最重要的是,所有正規 URI 的地方都能使用它 (這招超方便),
於是我們把它設定給 img 和 a 標籤,不但能立馬顯示圖片,還能直接下載喔 !
// 建立 blob URI (Object URI)
var blobUri = window.URL.createObjectURL(blob);
img.src = blobUri;
link.href = blobUri;
Result:
更多 Blob 用法可參考 MDN Blob,以及 黑暗執行緒 HTML5筆記–Object URL。
ArrayBuffer
說到二進制,怎麼能忘了 ArrayBuffer !!
ArrayBuffer — — 陣列緩衝區,
是一 位元組 (bytes) 序列,能夠以不同的位元單位 「檢視/視圖 (view)」。
[i.e., 8位元、16位元、32位元 等 型別陣列 (Typed Array)]
與 Blob 相同,將 responseType
屬性設置為「arraybuffer」,
便能以 型別陣列 來操作位元組塊 !
xhr.responseType = 'arraybuffer';
當然,你能透過 FileReader 或 Blob 建構元,在 ArrayBuffer 與 Blob 之間進行轉換:
var blob = new Blob( [ buffer ], { type: "image/jpeg" } );
更多內容可參考 MDN ArrayBuffer。
POST 請求
截至目前為止,都是示範 GET 請求 (因為最簡單),
實際上,要送出 POST 請求 — — 也非常簡單 🤣,
僅需將 請求方法 換成 post,並於 send()
方法 置放資料酬載 (如果有的話) 。
使用其他 HTTP 請求方法 (e.g., PUT、DELETE、HEAD、OPTIONS) 也如出一轍,
但必須注意,該方法是否允許發送 酬載 (payload) 。 (保留)
Ex:
var payload = {name: "勝", city: "Taipei"};
var xhr = new XMLHttpRequest();
xhr.open("POST", "demo_test_post.asp");
xhr.setRequestHeader('Content-type', 'application/x-www-form-urlencoded; charset=UTF-8');
xhr.onload = function () {
document.body.innerHTML = xhr.responseText;
};
var encodedData = encodeFormData(payload);
xhr.send(encodedData);
Content-Type (內容類型)
application/x-www-form-urlencoded
別忘記啦 ! HTML5 表單 預設編碼方式 為 application/x-www-form-urlencoded,
POST 表單 在送出資料前,需設置 Content-Type 表頭,
否則 Server 可能不會鳥你 !
最重要的是 — 開發時,務必記得處理 百分比編碼 (Percent-Encoding),
你或許會想使用 犀牛本 作者 David 寫的簡易函式 — encodeFormData
:
function encodeFormData(data) {
if (!data) return ""; // Always return a string
var pairs = []; // To hold name=value pairs
for (var name in data) { // For each name
if (!data.hasOwnProperty(name)) continue; // Skip inherited
if (typeof data[name] === "function") continue; // Skip methods
var value = data[name].toString(); // Value as string
name = encodeURIComponent(name.replace(" ", "+")); // Encode name
value = encodeURIComponent(value.replace(" ", "+")); // Encode value
pairs.push(name + "=" + value); // Remember name=value pair
}
return pairs.join('&'); // Return joined pairs separated with &
}
Input:
var data = {name: "勝", city: "Taipei"};
Output:
name=%E4%B8%AD%E5%8B%9D&city=Taipei
可結合上例於 w3schools 測試,
詳細作法可參考: (5-3) GET vs. POST 保留 。
multipart/form-data
上傳檔案 用的 multipart/form-data 就更簡單啦 !
使用 FormData API 與它的 append()
方法 加入 File (或 Blob、buffer) 即可,
送出資料時,XHR 會幫你處理好 multipart/form-data 與 麻煩的 boundary:
var xhr = new XMLHttpRequest();
xhr.open("POST", "test.php");
xhr.onload = function () {
document.body.innerHTML = xhr.responseText;
};
// 實例 FormData 物件
var formData = new FormData();
// 新增 blob
formData.append("SNIS-928", blob);
// 送出 !
xhr.send(formData);
其他
POST、PUT 不僅僅用於 表單,Content-Type 需依 Server 實作方式而定,
例如,常見的 application/json 或 application/octet-stream,使用上也是如出一轍:
xhr.setRequestHeader('Content-type', 'application/json ; charset=UTF-8');
並且能使用 Blob 或 ArrayBuffer,
做為 POST、PUT 請求的 資料酬載 (payload) 送出喔 !
Ex:
var bytes = new Uint8Array(256);
// 初始化為 0、1、2、3...254、255
for (var i = 0, l = bytes.length; i < l; i++) {
bytes[i] = i & 0xff;
}
xhr.send(bytes.buffer);
送出的請求訊息:
POST /test.php HTTP/1.1
Host: localhost:8080 User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.12; rv:53.0) Gecko/20100101 Firefox/53.0 Content-Length: 256
!"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqr stuvwxyz{|}~ ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖ×ØÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ
咦,跟 Unicode 字元列表 似乎有什麼巧合 🤔 ?
礙於篇幅,這邊就就不探討啦,
希望讓你對編碼有點概念 😀。
同源政策 (Same-origin policy)
若您嘗試將上述範例的 URI 替換為 絕對形式 (absolute-form),有很大的機會發生錯誤 !
例如,以 本地環境 對 https://example.com 送出請求:
...略...
xhr.open("POST", "https://example.com");
...略...
主控台 會出現:
這是因為 同源政策 (Same-origin policy) 的關係 !
同源政策 是 User Agent (e.g., 瀏覽器) 對頁面安全 的 必要措施,
也是 JavaScript Request 與其他 HTTP Client 最不相同的地方,
沒有它的話,惡意 script 將能輕易竊取頁面資訊 或 注入攻擊 😨 !
因此,不建議以 客戶端 JavaScript 來寫 爬蟲 (crawler),
儘管可行 (e.g., extension),但相對其他方式麻煩,
你或許會想以 Node.js、python、Java、php 或 cmdline… 來實作。
來源 (Origin)
來源 (Origin) 是指資源下載處的 協定 (protocol) + 主機 (host) + 通訊埠 (port),
當兩資源具有相同的 來源 ,即稱 — — 同源 (Same-origin)。
同源 (Same-origin) 需要 三者一致,
即使 主機名稱 (e.g., example.com) 相同,而 通訊協定 (e.g., HTTP & HTTPS) 不同,
也屬於 非同源/跨來源 (cross-origin) !
MDN 提供了良好的同源範例 :
資源 (Resource)
同源政策 (Same-origin policy) 是對 跨來源資源 的互動限制,
例如,JavaScript 可以開啟、關閉某個 跨來源 視窗,卻不一定能操作、獲取其內容。
資源,可以是任意東西 (e.g., HTML、css、JavaScript、影音媒體),
且規則適用於 視窗 (window)、分頁 (tab) 或 頁框 (frame) 內。
例如,一個用 iframe 標籤,產生的頁框:
<iframe id="myFrame1" src="/" width="100%"></iframe>
此 iframe 與本頁面 同源 (Same-origin) (NotFalse.net),
因此執行以下 JavaScript ,將能印出頁面標題:
(僅為範例,未來可能更換主題 😅)
// 取得 myFrame1 的 Window 物件
var myFrame1 = document.getElementById("myFrame1").contentWindow;
// 先找出 class 為 site-title 的 標題 h1
// 再找出此 h1 的直系子元素 a
var titleLink = myFrame1.document.querySelector('h1.site-title > a');
// 取得文字內容
var title = titleLink.textContent;
console.log(title);
然而在 跨來源 的頁面 (e.g., example.com),
將 無法執行,就算此 iframe
元素 是由同份 JavaScript 插入的:
限制 (Constraint)
同源政策 (Same-origin policy) 主要的限制有三種:
跨來源嵌入(Cross-origin embedding)
跨來源讀取(Cross-origin read)
跨來源寫(Cross-origin writes)
這並不在本篇的討論範圍,
MDN 提供了 同源政策 (Same-origin policy),及其普遍解法: 跨來源 HTTP 請求 (CORS) 的詳細說明,
若您為網頁開發者,務必前往了解 (當然,還有其他解法: JSONP、iframe…) !
XHR & CORS
跨來源 HTTP 請求 (cross-origin HTTP request, CORS) 是最普遍的 — 方寬同源政策的方式,
全仰賴 Server 是否允許 CORS 並實作之,
Client 端的 XMLHttpRequest 已於 Level 2 規範中支援了 CORS,
因此 跨來源請求 的程式碼,完全不需更動 (Server 不支援就是不支援 😂) !
推薦閱讀:
1. 阮一峰的網路日誌 — 跨域資源共享 CORS 詳解,深入淺出的 CORS 的教學。
2. enable-cors,告訴你如何讓 Server 支援 CORS。
範例原始檔: Github,
下集: 發送 HTTP 請求 (II) jQuery Ajax。
在《XMLHttpRequest — JavaScript 發送 HTTP 請求 (I)》中有 8 則留言
好文
thank you very much for your explanation
My pleasure 😁.
是不是出現了番號R
XD
你不要這麼專業好不好 😂
請問您文中說XHR已經過時,那後續取代它的是什麼呢?
既然AJAX是XHR的應用,那請問AJAX是否也過時了?