网络请求
XMLHttpRequest
- 发送 HTTP 请求;
- XMLHttpRequest MDN;
Fetch
Fetch
- XMLHttpRequest 的现代化替代;
Fetch API
Window.prototype.fetch(input: RequestInfo | URL, init?: RequestInit): Promise<Response>全局方法: 发起网络请求, 返回 Promise;- Request.prototype.method: string 实例属性: HTTP 请求方法;
- Request.prototype.url: string 实例属性: 请求 URL;
- Request.prototype.headers: Headers 实例属性: 请求头部;
- Request.prototype.body: BodyInit | null 实例属性: 请求体;
- Request.prototype.mode: RequestMode 实例属性: 请求模式;
- Request.prototype.credentials: RequestCredentials 实例属性: 凭证配置;
- Request.prototype.cache: RequestCache 实例属性: 缓存策略;
- RequestInit.body?: BodyInit | null: 请求体;
- RequestInit.method?: string: HTTP 请求方法;
- RequestInit.cache?: RequestCache: 缓存策略;
- RequestInit.mode?: RequestMode: 请求模式;
- RequestInit.credentials?: RequestCredentials: 凭证配置;
- RequestInit.redirect?: RequestRedirect: 重定向配置;
- RequestInit.headers?: HeadersInit: 请求头部;
- RequestInit.referrer?: string: 指定 HTTP 的 Referer 头部;
- RequestInit.referrerPolicy?: ReferrerPolicy: Referer 头部策略;
- RequestInit.integrity?: string: 强制子资源完整性;
- RequestInit.keepalive?: boolean: 允许请求存在时间超出页面生命周期;
- RequestInit.signal?: AbortSignal | null: AbortController 支持;
- Response.prototype.status: number 实例属性: 响应状态码;
- Response.prototype.statusText: string 实例属性: 响应状态码文本;
- Response.prototype.ok: boolean 实例属性: 响应是否成功 (状态码 200-299);
- Response.prototype.url: string 实例属性: 响应 URL;
- Response.prototype.headers: Headers 实例属性: 响应头部;
拒绝 promise
- 超时;
- 网络错误: 跨域...
常见 Fetch 请求模式
Get 请求
fetch("/send-me-params", {
method: "GET",
});
Body 请求体
- POST + Content-Type: application/x-www-form-urlencoded;
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
- POST + Content-Type: application/json;
let payload = JSON.stringify({ foo: "bar", baz: "qux" });
let jsonHeaders = new Headers({
"Content-Type": "application/json",
});
fetch("/send-me-json", {
method: "POST",
body: payload,
headers: jsonHeaders,
});
发送文件
- POST;
// 发送单个文件
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 对象
// 字面量
let seed = [["foo", "bar"]];
let h = new Headers(seed);
// 设置键
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 对象
- 网络资源请求;
- new Request(input, options);
let r2 = new Request("https://foo.com", { method: "POST" });
克隆 Request 对象
- new Request(r): 被克隆的请求体标记为已使用;
- r.clone(): 被克隆的请求体不会标记为已使用;
- Request 只能使用一次, 使用后不可被克隆, 不可使用;
Response 对象
Response 对象
- 网络资源响应;
- new Response(body, option);
let r = new Response("foobar", {
status: 418,
statusText: "I'm a teapot",
});
克隆 Response 对象
- 同 Request 对象;
Request, Response 及 Body 混入
Body
- Request 和 Response 都使用了 Fetch API 的 Body 混入;
- 两者具有只读的 body, bodyUsed 属性;
- 两者具有对应的混入方法;
- 将 ReadableStream 转存值缓冲区;
- 将缓缓冲区转为为 js 数据类型;
Body API
Body.prototype.text(): Promise<string>实例方法: 读取响应体为文本;Body.prototype.json(): Promise<any>实例方法: 读取响应体为 JSON;Body.prototype.blob(): Promise<Blob>实例方法: 读取响应体为 Blob;Body.prototype.arrayBuffer(): Promise<ArrayBuffer>实例方法: 读取响应体为 ArrayBuffer;Body.prototype.formData(): Promise<FormData>实例方法: 读取响应体为 FormData;
一次性流
- body 混入构建在 ReadableStream;
- 所有混入方法只能使用一次;
Beacon
Beacon
- 异步 xhr, fetch 无法在 unload 事件中发送请求;
- 同步 xhr 可以, 但堵塞主线程;
Beacon API
- Navigator: sendBeacon(url, data): 在页面已关闭的情况下依旧发送 post 请求;
navigator.sendBeacon("https://example.com/analytics-reporting-url", '{foo: "bar"}');
Web Socket
- WebSocket(url: string, protocols?: string | string[]): WebSocket 构造函数: 创建 WebSocket 连接;
- WebSocket.prototype.CONNECTING: number 常量: 连接正在建立 (0);
- WebSocket.prototype.OPEN: number 常量: 连接已经建立 (1);
- WebSocket.prototype.CLOSING: number 常量: 连接正在关闭 (2);
- WebSocket.prototype.CLOSE: number 常量: 连接已经关闭 (3);
- WebSocket.prototype.readyState: number 实例属性: WebSocket 连接状态;
- WebSocket.prototype.url: string 实例属性: WebSocket URL;
- WebSocket.prototype.protocol: string 实例属性: 服务器选择的协议;
- WebSocket.prototype.bufferedAmount: number 实例属性: 排队等待发送的字节数;
- WebSocket.prototype.extensions: string 实例属性: 服务器选择的扩展;
- WebSocket.prototype.send(data: string | ArrayBuffer | Blob): void 实例方法: 发送数据;
- WebSocket.prototype.close(code?: number, reason?: string): void 实例方法: 关闭 WebSocket 连接;
- WebSocket.prototype.onopen: ((this: WebSocket, ev: Event) => void) | null 实例属性: 连接成功时触发;
- WebSocket.prototype.onerror: ((this: WebSocket, ev: Event) => void) | null 实例属性: 连接报错时触发;
- WebSocket.prototype.onclose: ((this: WebSocket, ev: CloseEvent) => void) | null 实例属性: 连接关闭时触发;
- WebSocket.prototype.onmessage: ((this: WebSocket, ev: MessageEvent) => void) | null 实例属性: 接收消息时触发;
- MessageEvent.prototype.data: any 实例属性: 消息数据;
let socket = new WebSocket("ws://www.example.com/server.php");
socket.send(stringData);
socket.onmessage = function (event) {
let data = event.data; // 获取数据
};
SSE
SSE
- UTF-8;
- 一次信息有若干 message 组成;
- message 之间 \n\n 间隔, 单个 message 使用 \n 跨行;
- : 表示注释;
- data 表示数据内容;
- id 字段表示标识符, 用于重连机制;
- event 字段表示自定义事件, 默认为 message;
- retry 字段指定浏览器重连时间间隔;
: this is a test stream\n\n
data: some text\n\n
data: another message\n
data: with two lines \n\n
客户端
- EventSource(url: string, eventSourceInitDict?: EventSourceInit): EventSource 构造函数: 建立 EventSource 对象, 只能是 GET 请求;
- EventSource.prototype.url: string 实例属性: EventSource URL;
- EventSource.prototype.readyState: number 实例属性: 连接状态;
- EventSource.prototype.protocol: string 实例属性: 服务器使用的协议;
- EventSource.prototype.close(): void 实例方法: 关闭 EventSource 连接;
- EventSource.prototype.onopen: ((this: EventSource, ev: Event) => void) | null 实例属性: 连接建立时触发;
- EventSource.prototype.onmessage: ((this: EventSource, ev: MessageEvent) => void) | null 实例属性: 接收消息时触发;
- EventSource.prototype.onerror: ((this: EventSource, ev: Event) => void) | null 实例属性: 连接出错时触发;
const source = new EventSource(url);
source.addEventListener("message", (event) => {
//...
});
服务端
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++;
}
}
};