JavaScript – XMLHttpRequest
前言
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');
接着监听 load 事件
request.addEventListener('load', () => { console.log('status', request.status); console.log('response', request.response); });
load 事件会在 response 下载完毕后发布。
接着发送 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('load', () => { // 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('load', () => { // response 变成了 Array 类型 const products: { name: string }[] = request.response; console.log(products[0].name); // 'iPhone14' });
Response Header
request.getResponseHeader 方法和 request.getAllResponseHeaders 方法
request.addEventListener('load', () => { 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' });
如果遇到重复的 header 那么它会合并起来
key 是 'cutom',value 是 'a, b'
Cross-Origin 跨域请求携带 Cookie
跨域主要是服务端的职责,不熟悉的可以看这篇 ASP.NET Core – CORS (Cross-Origin Resource Sharing)。
客户端和跨域有关的是 Cookie。
同跨 (same-origin) 请求,XMLHttpRequest 会自动携带 Cookie。
跨域 (cross-origin) 请求,默认不携带 Cookie,如果我们希望它携带 Cookie 可以设置 withCredentials。
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!'); });
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 Timeout
我们可以设置请求的最长时间,如果在时间内没有收到 response 或者 response body 来不及被下载完,这都算请求超时。
request.timeout = 5000; // unit 是 milliseconds
request.addEventListener('timeout', () => {
console.log('Timeout, the request is taking more than five seconds.');
});
监听 timeout 事件可以处理超时请求。
load 和 loaded 事件
load 事件在请求失败 (error, abort, timeout) 时是不会发布的。
loaded 事件则无论请求成功还是失败都会发布。
注:response status 400-599 依然算是请求成功哦。
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('load', () => { 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('load', () => { 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('load', () => { 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 方法传入要发送的 JSON 数据就可以了。
POST FormData or FormUrlEncoded (multipart/form-data or application/x-www-form-urlencoded)
POST FormData or FormUrlEncoded 和 POST JSON data 大同小异
// POST multipart/form-data const productFormData = new FormData(); productFormData.set('name', 'iPhone12'); request.send(productFormData); // POST application/x-www-form-urlencoded const productFormUrlEncoded = new URLSearchParams({ name: 'iPhone12', }); request.send(productFormUrlEncoded);
只是把 send 的数据从 JSON 换成 FormData or URLSearchParams 就可以了。
另外,FormData or FormUrlEncoded 不需要设置 request header 'Content-Type',游览器会依据发送的数据类型自动添加,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', // 如果二进制没有明确类型,type 就放 application/octet-stream }); request.send(blob);
upload progress
和 download 是同一个原理
request.upload.addEventListener('progress', e => { const percentage = ((e.loaded / e.total) * 100).toFixed() + '%'; console.log(percentage); });
和 download 不同的是,它监听的对象是 request.upload 而不是 request。
Request ReadyState
request.readyState 反应了当前 Reqest 的阶段。readystatechange 事件会在 Request 有动静的时候发布,比如 state change 的时候或者 state Loading 下载到新数据的时候。
0 是初始阶段。
1 是设置了 request method 和 URL 之后
2.是 response header 下载完毕
3 是 downloading response body
4 是 response body 下载完毕
request.addEventListener('readystatechange', () => {
console.log(request.readyState);
});
有 3 个知识点:
- request 发出后,无论成功还是失败 (error, abort, time),readyState 最终都会变成 DONE。
- readystatechange 会在 LOADING 阶段 (也就下载文件的时候) 发布多次。
- response header 会先被下载,然后发布 readystatechange HEADERS_RECEIVED。
如果我们想最快的获取 response header 资料,可以把监听从 load 事件换到 readystatechange 事件。
request.addEventListener('readystatechange', () => { if (request.readyState === 2) { console.log(request.getResponseHeader('Content-Length')); } else if (request.readyState === 4 && request.status !== 0) { console.log(request.status); console.log(request.response); } });
readState === 4 是一定会发布的,所以如果我们只想要监听成功的请求,这里需要过滤 status !== 0。