前言
XMLHttpRequest 是 JavaScript built-in 的一個 class,用於發送 HTTP 請求,俗稱 AJAX。
這幾年 XMLHttpRequest 已經逐漸被 Fetch 取代了,只剩下一個功能目前 Fetch 還做不到 -- 獲取上傳進度,因此 XMLHttpRequest 還是得學起來😑。
Simple Get Request & Response
首先實例化 XMLHttpRequest 對象
const request = new XMLHttpRequest();
接着設置 request method 和 URL
request.open('GET', 'https://192.168.1.152:44300/products');
接着監聽 request state change 事件
request.addEventListener('readystatechange', () => { if (request.readyState === 4) { console.log('status', request.status); console.log('response', request.response); } });
request.readyState 表示 request 當前的狀態,4 代表 response 已經下載完畢。通過 readystatechange event 可以監聽到 request.readyState 的變化。
接着發送 request
request.send();
效果
Request with Query Parameters
XMLHttpRequest 沒有 built-in 對 Query Parameters 的處理。
我們需要藉助 URLSearchParams。
const request = new XMLHttpRequest(); const searchParams = new URLSearchParams({ key1: 'value1', }); const queryString = '?' + searchParams.toString(); request.open('GET', 'https://192.168.1.152:44300/products' + queryString);
然後把 Query String 拼接到 request URL 就可以了。
Request with Header
通過 request.setRequestHeader 方法就可以添加 header 了。
const request = new XMLHttpRequest(); request.open('GET', 'https://192.168.1.152:44300/products'); request.setRequestHeader('Accept', 'application/json');
Auto Parse JSON Response
上面的例子,request.response 是一個 string 類型,我們需要手動 parse JSON。
request.addEventListener('readystatechange', () => { if (request.readyState === 4) { // status 是 number 類型 console.log('status', request.status); // 200 // response 是 string 類型 console.log('response', request.response); // '[{"name":"iPhone14"},{"name":"iPhone15"}]' const products = JSON.parse(request.response) as { name: string }[]; console.log(products[0].name); // 'iPhone14' } });
我們可以通過設置 request.responseType 讓 XMLHttpRequest 自動替我們 parse JSON。
request.responseType = 'json'; request.addEventListener('readystatechange', () => { if (request.readyState === 4) { // response 變成了 Array 類型 const products: { name: string }[] = request.response; console.log(products[0].name); // 'iPhone14' } });
Response Header
request.getResponseHeader 方法和 request.getAllResponseHeaders 方法
request.addEventListener('readystatechange', () => { if (request.readyState === 4) { console.log(request.getResponseHeader('Content-Type')); // 'application/json; charset=utf-8' console.log('response content type', request.getAllResponseHeaders()); // 'content-type: application/json; charset=utf-8' } });
Cross-Origin 跨域請求攜帶 Cookie
跨域主要是服務端的職責,不熟悉的可以看這篇 ASP.NET Core – CORS (Cross-Origin Resource Sharing)。
客戶端和跨域有關的是 Cookie,
它有兩種情況:
- 請求同跨 (same-origin)
默認情況下會攜帶 Cookie
如果不希望攜帶 Cookie 可以設置
request.withCredentials = false;
- 請求跨域 (cross-origin)
默認情況下不攜帶 Cookie
如果希望攜帶 Cookie 可以設置
request.withCredentials = true;
Request Error
Request Error 指的是請求失敗,通常是 offline 造成的。
status 400-5xx 這些可不算 Request Error 哦,因爲這些已經是有 response 成功了,只是在 status 被區分是有問題的。
要處理 Request Error,監聽 error 事件就可以了。
request.addEventListener('error', () => { console.log('Network error, possibly offline!'); });
注:request error 依然會發布 readystatechange 事件,request.readyState 依然等於 4,但是 status 會是 0。
Abort Request
我們可以在任意時間終止一個 request。
request.addEventListener('abort', () => { console.log('The request has been aborted.'); }); setTimeout(() => { request.abort(); }, 2000);
兩秒鐘後執行 request.abort 方法,終止請求,同時發佈 abort 事件。
另外,客戶端也會通知服務端,請求被終止了。
ASP.NET Core 有一個 cancellationToken,可以隨時查看是否客戶端已經終止請求, 如果已經終止,那就不需要繼續執行了。
注:request 被 abort 了依然會發布 readystatechange 事件,request.readyState 依然等於 4,但是 status 會是 0。
Request Timeout
我們可以設置請求的最長時間,如果在時間內沒有收到 response 或者 response body 來不及被下載完,這都算請求超時。
request.timeout = 5000; // unit 是 milliseconds
request.addEventListener('timeout', () => {
console.log('Timeout, the request is taking more than five seconds.');
});
監聽 timeout 事件可以處理超時請求。
注:request timeout 依然會發布 readystatechange 事件,request.readyState 依然等於 4,但是 status 會是 0。
Download File
除了請求 JSON 數據,偶爾我們也會需要下載文件。
download txt file
const request = new XMLHttpRequest(); request.open('GET', 'https://192.168.1.152:44300/data.txt'); request.responseType = 'arraybuffer'; request.addEventListener('readystatechange', () => { if (request.readyState === 4) { const memoryStream: ArrayBuffer = request.response; const bytes = new Uint8Array(memoryStream); const textDecoder = new TextDecoder(); const text = textDecoder.decode(bytes); console.log(text); // 'Hello World' } }); request.send();
關鍵是 responseType 設置成了 'arraybuffer'。
request.response 的類型變成了 ArrayBuffer,通過 Uint8Array 和 TextDecoder 從 ArrayBuffer 讀取 data.txt 的內容。
Download Video
Video 通常 size 比較大,用 ArrayBuffer 怕內存會不夠,所以用 Blob 會比較合適。
const request = new XMLHttpRequest(); request.open('GET', 'https://192.168.1.152:44300/video.mp4'); request.responseType = 'blob'; request.addEventListener('readystatechange', () => { if (request.readyState === 4) { const blob: Blob = request.response; console.log(blob.size / 1024); // 124,645 kb console.log(blob.type); // video/mp4 } }); request.send();
download progress
文件大下載慢,最好可以顯示進度條
request.addEventListener('progress', e => { const percentage = ((e.loaded / e.total) * 100).toFixed() + '%'; console.log(percentage); });
通過監聽 progress 事件,可以獲取到 total size 和已下載的 size。progress 事件會觸發多次。
partial data on downloading
如果把 responseType 設置成 'text',request.response 會在 progress 的過程中逐漸被添加。
request.responseType = 'text'; request.addEventListener('progress', () => { console.log(request.response); });
只有 'text' 纔會這樣,'blob', 'arraybuffer' 在 progress 階段 request.response 始終是 null。
所以,這個功能其實沒有什麼鳥用,因爲如果我們請求的是一個 video.mp4,使用 ‘text’ 的話,video 原本的 binary 會被強制轉換成 string,而這個過程很可能會破壞掉原本的 binary,導致 binary 無可還原,video 就毀了。
POST Request
POST 和 GET 大同小異
POST JSON Data
const request = new XMLHttpRequest(); request.open('POST', 'https://192.168.1.152:44300/products'); request.setRequestHeader('Accept', 'application/json'); request.setRequestHeader('Content-Type', 'application/json'); request.responseType = 'json'; request.addEventListener('readystatechange', () => { if (request.readyState === 4) { console.log(request.status); // 201 console.log(request.response); // { id: 1, name: 'iPhone12' } } }); const product = { name: 'iPhone12' }; const json = JSON.stringify(product); request.send(json);
JSON 格式需要添加 request header 'Content-Type',然後是 request.send 方法傳入要 post 的資料就可以了。
POST FormData
POST FormData 和 POST JSON data 大同小異
const productFormData = new FormData(); productFormData.set('name', 'iPhone12'); request.send(productFormData);
只是把 send 的資料改成 FormData 就可以了。
另外,FormData 不需要設置 request header 'Content-Type',遊覽器會依據 send 的 data 類型自動添加,JSON 之所以需要是因爲遊覽器把 JSON 視爲 text/plain。
POST Binary (Blob)
FormData 支持 Blob 類型的 value,所以我們可以使用 FormData 上傳二進制文件。
const productFormData = new FormData(); productFormData.set('name', 'iPhone12'); const productDocument = 'Product Detail'; const textEncoder = new TextEncoder(); const bytes = textEncoder.encode(productDocument); const blob = new Blob([bytes], { type: 'text/plain', }); productFormData.set('document', blob); request.send(productFormData);
或者直接發送 Blob 也是可以的。
const productDocument = 'Product Detail'; const textEncoder = new TextEncoder(); const bytes = textEncoder.encode(productDocument); const blob = new Blob([bytes], { type: 'text/plain', }); request.send(blob);
如果二進制沒有明確類型,type 就放 application/octet-stream。
upload progress
和 download 是同一個原理
request.addEventListener('progress', e => { const percentage = ((e.loaded / e.total) * 100).toFixed() + '%'; console.log(percentage); });
如果又有上傳又有下載,可以通過 request.readystate 做區分
request.addEventListener('progress', e => { if (request.readyState >= 3) { // 3 是 loading,4 是 done const downloadPercentage = ((e.loaded / e.total) * 100).toFixed() + '%'; } else { const uploadingPercentage = ((e.loaded / e.total) * 100).toFixed() + '%'; } });
Request ReadyState
0 是初始階段。
1 是設置了 request method 和 URL 之後
2.是 response header 下載完畢
3 是 downloading response body
4 是 response body 下載完畢