跳到主要内容

网络请求和远程资源

XMLHttpRequest 对象

基础

定义 XHR
  • xhr.open();
  • 三个参数依次为请求类型,URL,是否异步;
xhr.open("get", "example.php", false);
使用 xhr
  • 发送请求:xhr.send();
  • 响应请求;
    • responseText:响应体返回文本;
    • responseXML:XML DOM 文档;
    • status:HTTP 状态;
    • statusText:HTTP 状态描述;
  • 取消请求:xhr.abort();
xhr.send(null);
if ((xhr.status >= 200 && xhr.status < 300) || xhr.status == 304) {
alert(xhr.responseText);
} else {
alert("Request was unsuccessful: " + xhr.status);
}
xhr.abort();
异步处理
let xhr = new XMLHttpRequest();
xhr.onreadystatechange = function () {
// readyState 属性
// 0: 未初始化, 1: 已调用 open(), 2: 已调用 send(), 3: 接收部分响应, 4: 接收所有响应.
if (xhr.readyState == 4) {
if ((xhr.status >= 200 && xhr.status < 300) || xhr.status == 304) {
alert(xhr.responseText);
} else {
alert("Request was unsuccessful: " + xhr.status);
}
}
};
xhr.open("get", "example.txt", true);
xhr.send(null);

请求类型

get 请求
  • xhr.open() 使用 encodeURIComponent() 编码键值对;
xhr.open("get", "example.php?name1=value1&name2=value2", true);
post 请求
  • xhr.setRequestHeader() 设置头部类型;
  • xhr.send() 发送序列化后的表单数据;
xhr.open("post", "postexample.php", true);
xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
let form = document.getElementById("user-info");
xhr.send(serialize(form));
FormData 类型
  • xhr.send() 发送序列化后的表单数据;
  • 不需要显式设置头部类型;
let data = new FormData();
data.append("name", "Nicholas");
let data = new FormData(document.forms[0]);
xhr.open("post", "postexample.php", true);
let form = document.getElementById("user-info");
xhr.send(new FormData(form));
overrideMimeType()方法
  • 覆写 XHR 响应的 MIME 类型
  • send() 调用前使用
let xhr = new XMLHttpRequest();
xhr.open("get", "text.php", true);
xhr.overrideMimeType("text/xml");
xhr.send(null);

HTTP 头部

XHR 头部字段
头部字段描述
Accept内容类型
Accept-Charset字符集
Accept-Encoding编码类型
Accept-Language语言
Connection连接类型
Cookiecookie
Host页面所在域
Referer页面 URL
User-Agent用户代理字符串
操作头部字段
  • 设置:xhr.setRequestHeader();
  • 获得;
    • xhr.getResponseHeader();
    • xhr.getAllResponseHeaders();
xhr.open("get", "example.php", true);
xhr.setRequestHeader("MyHeader", "MyValue");
xhr.send(null);

let myHeader = xhr.getResponseHeader("MyHeader"); // 指定字段
let allHeaders xhr.getAllResponseHeaders(); // 所有字段

进度事件

相关事件
事件触发时机
loadstart接受到响应的第一个字节触发
progress接受响应期间反复触发
error请求出错触发
abort调用 abort() 触发
load成功接受响应后触发
loadenderror/abort/load 之后触发
load 事件
  • 接受 event 对象;
let xhr = new XMLHttpRequest();
xhr.onload = function (event) {
console.log(event);
};
xhr.open("get", "altevents.php", true);
xhr.send(null);
progress 事件
  • 接受 event 对象;
    • lengthComputable 属性:是否可用;
    • position:收到字节数;
    • totalSize:总字节数;
let xhr = new XMLHttpRequest();
xhr.onprogress = function (event) {
let divStatus = document.getElementById("status");
if (event.lengthComputable) {
divStatus.innerHTML =
"Received " + event.position + " of " + event.totalSize + " bytes";
} else;
};
xhr.open("get", "altevents.php", true);
xhr.send(null);
超时
  • 超时后触发 timeout 事件;
xhr.open("get", "timeout.php", true);
xhr.timeout = 1000; // 设置 1 秒超时
xhr.ontimeout = function () {
alert("Request did not return in a second.");
};
xhr.send(null);

Fetch API

基础

定义请求
  • fetch():返回 promise;
let r = fetch("/bar");
console.log(r);
fetch("bar.txt").then((response) => {
console.log(response);
});
读取响应
  • res.xxx();
fetch("bar.txt")
.then((response) => response.text())
.then((data) => console.log(data));
属性
  • response.status:状态码;
  • response.statusText:状态码文本;
  • url:请求 url;
fetch("/qux").then((response) => {
console.log(response.status); // 200
console.log(response.statusText); // OK
console.log(response.url) https://foo.com/bar/qux
});
拒绝 promise
  • 超时;
  • 网络错误:跨域。。。
fetch("/hangs-forever").then(
(response) => {
console.log(response);
},
(err) => {
console.log(err);
}
);
自定义选项
  • fetch() 只使用 URL 为 get 请求;
  • 第二个参数为 init 对象;
  • 配置请求具体参数;
自定义选项解释自定义选项解释
body请求体methodHTTP 请求方法
cache缓存策略mode请求模式
credentialscookie 配置redirect重定向配置
headers头部referrer指定 HTTP 的 Referer 头部 头部内容
integrity强制子资源完整性referrerPolicy指定 HTTP 的 Referer 头部
keepalive指示浏览器允许请求存在时间超出页面生命周期signalAbortController() 支持

常见 Fetch 请求模式

body 请求体
  • 添加 Content-Type 首部 application/x-www-form-urlencoded;
  • 使用 POST 请求;
  • 设置 body 属性;
let payload = "foo=bar&baz=qux";
let paramHeaders = new Headers({
"Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
});
fetch("/send-me-params", {
method: "POST",
body: payload,
headers: paramHeaders,
});
发送 json
  • 添加 Content-Type 首部 application/json;
  • 使用 POST 请求;
let jsonHeaders = new Headers({
"Content-Type": "application/json",
});
fetch("/send-me-json", {
method: "POST",
body: payload,
headers: jsonHeaders,
});
发送文件
  • 使用 POST 请求;
  • 无须显式指明 Content-Type 首部;
// 发送单个文件
let imageFormData = new FormData();
let imageInput = document.querySelector("input[type='file']");
imageFormData.append("image", imageInput.files[0]);
fetch("/img-upload", {
method: "POST",
body: imageFormData,
});
// 发送多个文件
let imageFormData = new FormData();
let imageInput = document.querySelector("input[type='file'][multiple]");
for (let i = 0; i < imageInput.files.length; ++i) {
imageFormData.append("image", imageInput.files[i]);
}
fetch("/img-upload", {
method: "POST",
body: imageFormData,
});
发送跨源请求
fetch("//cross-origin.com", { method: "no-cors" }).then((response) =>
console.log(response.type)
);
中断请求
  • 使用 AbortController() 和 signal;
let abortController = new AbortController();
fetch("wikipedia.zip", { signal: abortController.signal }).catch(() =>
console.log("aborted!")
);
setTimeout(() => abortController.abort());

Header 对象

操作 Header
// 字面量
let seed = [["foo", "bar"]];
let h = new Headers(seed);
// new
let h = new Headers();

// 设置键
h.set("foo", "bar");
// 检查键
console.log(h.has("foo")); // true
// 获取值
console.log(h.get("foo")); // bar
// 更新值
h.set("foo", "baz");
// 添加键
h.append("foo", "bar");
// 删除键
h.delete("foo");

// 迭代
console.log(...h.keys()); // foo, baz
console.log(...h.values()); // bar, qux
console.log(...h.entries()); // ['foo', 'bar'], ['baz', 'qux']

Request 对象

创建 Request 对象
  • new Request(url);
let r1 = new Request("https://foo.com");
let r2 = new Request("https://foo.com", { method: "POST" });
克隆 Request 对象
  • new Request(r):被克隆的请求体标记为已使用;
  • r.clone():被克隆的请求体不会标记为已使用;
  • 请求体若已被标记,不可被克隆;
let r1 = new Request("https://foo.com");
let r2 = new Request(r1);
console.log(r1.bodyUsed); // true
console.log(r2.bodyUsed); // false

let r1 = new Request("https://foo.com", { method: "POST", body: "foobar" });
let r2 = r1.clone();
console.log(r1.bodyUsed); // false
console.log(r2.bodyUsed); // false

let r = new Request("https://foo.com");
r.text();
r.clone(); // TypeError: Failed to execute 'clone' on 'Request': Request body is already used
new Request(r); // TypeError: Failed to construct 'Request': Cannot construct a Request with a Request object that has already been used.
fetch() 中使用 Request 对象
  • fetch(r);
  • fetch 无法使用请求体被使用的 Request;
let r = new Request("https://foo.com");
fetch(r, { method: "POST" });

let r = new Request("https://foo.com", { method: "POST", body: "foobar" });
r.text();
fetch(r); // TypeError: Cannot construct a Request with a Request object that has already been used.

// 使用 clone() 多次使用同一个 Request
let r = new Request("https://foo.com", { method: "POST", body: "foobar" });
// clone() 必须在 r 之前使用
fetch(r.clone());
fetch(r.clone());
fetch(r);

Response 对象

创建 Response 对象
  • new Response(body,init);
let r = new Response();
// 设置 body 和 init 对象
let r = new Response("foobar", {
status: 418,
statusText: "I'm a teapot",
});
// 生成网络错误的 Response
let r = Response.error();
响应信息
属性属性
headersHeader 对象statusTextstatus 属性描述
ok2XX 为 true, 其余为 falsetype响应类型
redirected是否经过重定向urlurl
statusHTTP 状态码
克隆 Response 对象
  • new Response(r):被克隆的请求体标记为已使用;
  • r.clone():被克隆的请求体不会标记为已使用;
  • 请求体若已被标记,不可被克隆;
  • response 只能读取一次;
let r1 = new Response("foobar");
let r2 = r1.clone();
console.log(r1.bodyUsed); // false
console.log(r2.bodyUsed); // false

let r = new Response("foobar");
r.clone(); // 没有错误
r.text(); // 设置 bodyUsed 为 true
r.clone(); // TypeError: Failed to execute 'clone' on 'Response': Response body is already used

let r = new Response("foobar");
r.text().then(console.log); // foobar
r.text().then(console.log); // TypeError: Failed to execute 'text' on 'Response': body stream is locked

Request, Response 及 Body 混入

Body
  • Request 和 Response 都使用了 Fetch API 的 Body 混入;
  • 两者具有只读的 body,bodyUsed 属性;
  • 两者具有对应的混入方法;
    • 将 ReadableStream 转存值缓冲区;
    • 将缓缓冲区转为为 js 数据类型;
Body.text()
  • 返回一个值为 UTF-8 字符串的 promise;
fetch("https://foo.com")
.then((response) => response.text())
.then(console.log);
Body.json()
  • 返回一个值为 JSON 的 promise;
let request = new Request("https://foo.com", {
method: "POST",
body: JSON.stringify({ bar: "baz" }),
});
request.json().then(console.log); // {bar: 'baz'}
Body.formData()
  • 返回一个值为序列化后的 FormData 的 promise;
fetch('https://foo.com/form-data')
.then((response) => response.formData())
.then((formData) => console.log(formData.get('foo'));
// bar
Body.arrayBuffer()
  • 返回一个值为 ArrayBuffer 的 promise;
let request = new Request("https://foo.com", {
method: "POST",
body: "abcdefg",
});
request.arrayBuffer().then((buf) => console.log(new Int8Array(buf))); // Int8Array(7) [97, 98, 99, 100, 101, 102, 103]
Body.blob()
  • 返回一个值为 Blob 的 promise;
fetch("https://foo.com")
.then((response) => response.blob())
.then(console.log);
// Blob(...) {size:..., type: "..."}
一次性流
  • body 混入构建在 ReadableStream;
  • 所有混入方法只能使用一次;

Beacon API

背景
  • 异步 xhr,fetch 无法在 unload 事件中发送请求;
  • 同步 xhr 可以,但堵塞主线程;
语法格式
  • navigator.sendBeacon():在页面已关闭的情况下依旧发送请求;
  • 发送 post 请求;
navigator.sendBeacon(
"https://example.com/analytics-reporting-url",
'{foo: "bar"}'
);

Web Socket

创建和关闭

API
let socket = new WebSocket("ws://www.example.com/server.php");
socket.close();
readyState 属性
  • WebSocket.OPENING (0):连接正在建立;
  • WebSocket.OPEN (1):连接已经建立;
  • WebSocket.CLOSING (2):连接正在关闭;
  • WebSocket.CLOSE (3):连接已经关闭;

发送和接受数据

发送数据
  • socket.send();
let socket = new WebSocket("ws://www.example.com/server.php");
socket.send(stringData);
socket.send(arrayBufferData.buffer);
socket.send(blobData);
接收数据
  • 接受数据触发 message 事件;
socket.onmessage = function (event) {
let data = event.data; // 获取数据
};

其他事件

let socket = new WebSocket("ws://www.example.com/server.php");
// open 连接成功时触发
socket.onopen = function () {
alert("Connection established.");
};
// error 连接报错时触发
socket.onerror = function () {
alert("Connection error.");
};
// close 连接关闭时触发
socket.onclose = function () {
alert("Connection closed.");
};

SSE

数据格式

字符编码
  • UTF-8;
message
  • 一次信息有若干 message 组成;
  • 使用 :表示注释;
  • message 之间 \n\n 间隔;
  • 单个 message 使用 \n 跨行;
: this is a test stream\n\n
data: some text\n\n

data: another message\n
data: with two lines \n\n
字段
  • data 表示数据内容;
  • id 字段表示标识符,用于重连机制;
  • event 字段表示自定义事件,默认为 message;
  • retry 字段指定浏览器重连时间间隔;
id: msg1\n
data: message\n\n

event: foo\n
data: a foo event\n\n

retry: 10000\n\n

客户端

  • new EventSource(url):建立 EventSource 对象;
    • 只能是 get 请求;
  • source.close():关闭 EventSource 对象;
const source = new EventSource(url);
// 链接建立, 触发 open 事件
source.addEventListener(
"open",
(event) => {
//...
},
false
);
// 服务器返回信息, 触发 message 事件
source.addEventListener(
"message",
(event) => {
const data = event.data;
// ...
},
false
);
// 链接出错, 触发 error 事件
source.addEventListener(
"error",
(event) => {
// ...
},
false
);

source.close();

服务端

  • 设置 http 头;
res.setHeader("Content-Type", "text/event-stream");
res.setHeader("Cache-Control", "no-cache");
// 发送数据
res.write(
`data: ${JSON.stringify({
status: "success",
content: [datasetID, modelID, [uvID]],
})}\n\n`
);
res.end();

最佳实践

fetch 封装

  • 超时控制 + 重试;
export const extendFetch = async (
url: string,
option: RequestInit,
retry = 3
) => {
let num = 1;
while (num <= retry) {
const timeout = (Math.floor(Math.random() * 2 ** num) + 1) * 1000;
const ab = new AbortController();
const id = setTimeout(() => {
ab.abort();
}, timeout);

try {
const res = await fetch(url, option, {
sign: ab.signal,
});
clearTimeout(id);
return res;
} catch (error) {
clearTimeout(id);
num++;
}
}
};