跳到主要内容

网络请求

XMLHttpRequest

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++;
}
}
};