WEB 開發

XMLHttpRequest — JavaScript 發送 HTTP 請求 (I)

XMLHttpRequest (XHR), 是最常見的 JavaScript HTTP Client,
常見於 Web 應用、Debug、API 測試… 😆。
 
就算不會 JavaScript,閱讀本篇後,您將擁有基本實作能力,
不清楚如何執行 JavaScript 的話,可參閱 W3SChools,或利用 CodePen 進行練習 😁。
 
api-github
 


 

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,還能使用 PUTDELETEHEADOPTIONS !
並且預設使用了 非同步 (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 :
DocumentFragment-ex
 
 
2. 將含有多個 img 的 docFrag
一次新增至 HTML 的 body 中 :
DocumentFragment-ex-2
 
 
完成 ! (一切都是為了學習),
可前往 這裡 觀看成果 :
XMLHttpRequest Example
 


 

二進制回應

上方 處理回應,只說明使用 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 顯示:
its-you
 
 

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

 
並且能使用 BlobArrayBuffer
做為 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 字元列表 似乎有什麼巧合 🤔 ?
List-of-Unicode-characters
 
礙於篇幅,這邊就就不探討啦,
希望讓你對編碼有點概念 😀。
 


 

同源政策 (Same-origin policy)

若您嘗試將上述範例的 URI 替換為 絕對形式 (absolute-form),有很大的機會發生錯誤 !
例如,以 本地環境 對 https://example.com 送出請求:

...略...

xhr.open("POST", "https://example.com");

...略...

 
主控台 會出現:
cors-warning
 
這是因為 同源政策 (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 插入的:
cors-warning-2
 
 

限制 (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 則留言

  1. 請問您文中說XHR已經過時,那後續取代它的是什麼呢?
    既然AJAX是XHR的應用,那請問AJAX是否也過時了?

發表迴響