网络请求
# 15.网络请求
网络请求是前端与服务器通信的核心能力。从早期的 XMLHttpRequest 到现代的 Fetch API,再到实时通信的 WebSocket 和 Server-Sent Events,JavaScript 提供了完整的网络通信方案。本章系统讲解各种网络请求方式、跨域处理以及高级应用模式。
# 15.1 XMLHttpRequest
# 15.1.1 基本用法
XMLHttpRequest(XHR)是最传统的异步请求方式,虽然 Fetch 已成为主流,但理解 XHR 仍然重要:
// 基本 GET 请求
const xhr = new XMLHttpRequest();
xhr.open('GET', '/api/users', true); // true = 异步
xhr.setRequestHeader('Accept', 'application/json');
xhr.onreadystatechange = function() {
// readyState 状态值:
// 0: UNSENT - 未调用 open
// 1: OPENED - 已调用 open
// 2: HEADERS_RECEIVED - 已接收响应头
// 3: LOADING - 正在接收响应体
// 4: DONE - 请求完成
if (xhr.readyState === 4) {
if (xhr.status >= 200 && xhr.status < 300) {
const data = JSON.parse(xhr.responseText);
console.log(data);
} else {
console.error('请求失败:', xhr.status);
}
}
};
xhr.onerror = function() {
console.error('网络错误');
};
xhr.send();
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# 15.1.2 POST 请求
const xhr = new XMLHttpRequest();
xhr.open('POST', '/api/users');
xhr.setRequestHeader('Content-Type', 'application/json');
xhr.onload = function() {
if (xhr.status === 201) {
console.log('创建成功:', JSON.parse(xhr.responseText));
}
};
xhr.send(JSON.stringify({
name: 'Alice',
email: 'alice@example.com'
}));
1
2
3
4
5
6
7
8
9
10
11
12
13
14
2
3
4
5
6
7
8
9
10
11
12
13
14
# 15.1.3 进度监控
XHR 的一个优势是内置进度事件,Fetch 原生不支持上传进度:
const xhr = new XMLHttpRequest();
xhr.open('POST', '/api/upload');
// 上传进度
xhr.upload.onprogress = function(e) {
if (e.lengthComputable) {
const percent = (e.loaded / e.total * 100).toFixed(1);
console.log(`上传进度: ${percent}%`);
}
};
// 下载进度
xhr.onprogress = function(e) {
if (e.lengthComputable) {
const percent = (e.loaded / e.total * 100).toFixed(1);
console.log(`下载进度: ${percent}%`);
}
};
xhr.upload.onload = () => console.log('上传完成');
xhr.onload = () => console.log('请求完成');
const formData = new FormData();
formData.append('file', fileInput.files[0]);
xhr.send(formData);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# 15.1.4 超时处理
const xhr = new XMLHttpRequest();
xhr.open('GET', '/api/data');
xhr.timeout = 5000; // 5 秒超时
xhr.ontimeout = function() {
console.error('请求超时');
};
xhr.send();
1
2
3
4
5
6
7
8
9
2
3
4
5
6
7
8
9
# 15.1.5 取消请求
const xhr = new XMLHttpRequest();
xhr.open('GET', '/api/data');
xhr.send();
// 取消
xhr.abort();
xhr.onabort = () => console.log('请求已取消');
1
2
3
4
5
6
7
2
3
4
5
6
7
# 15.2 Fetch API
# 15.2.1 基本用法
Fetch 是现代浏览器提供的网络请求 API,基于 Promise,语法更简洁:
// GET 请求
const response = await fetch('/api/users');
// 注意:HTTP 错误状态码(如 404、500)不会触发 reject
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const data = await response.json();
console.log(data);
1
2
3
4
5
6
7
8
9
10
2
3
4
5
6
7
8
9
10
# 15.2.2 请求配置
// POST 请求
const response = await fetch('/api/users', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer token123'
},
body: JSON.stringify({ name: 'Alice', age: 25 }),
});
// PUT 请求
await fetch('/api/users/1', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name: 'Bob' }),
});
// DELETE 请求
await fetch('/api/users/1', { method: 'DELETE' });
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 15.2.3 Request 对象
const request = new Request('/api/users', {
method: 'POST',
headers: new Headers({
'Content-Type': 'application/json',
}),
body: JSON.stringify({ name: 'Alice' }),
mode: 'cors', // 'cors' | 'no-cors' | 'same-origin'
credentials: 'include', // 'omit' | 'same-origin' | 'include'
cache: 'no-cache', // 'default' | 'no-cache' | 'reload' | 'force-cache'
redirect: 'follow', // 'follow' | 'error' | 'manual'
referrerPolicy: 'no-referrer',
signal: controller.signal, // AbortController 信号
keepalive: true, // 页面卸载时保持请求
priority: 'high', // 'high' | 'low' | 'auto'
});
const response = await fetch(request);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 15.2.4 Response 对象
const response = await fetch('/api/data');
// 响应元数据
response.status; // 200
response.statusText; // 'OK'
response.ok; // true(status 200-299)
response.url; // 最终 URL(跟随重定向后)
response.redirected; // 是否经过重定向
response.type; // 'basic' | 'cors' | 'opaque'
// 响应头
response.headers.get('Content-Type');
response.headers.has('X-Custom-Header');
for (const [name, value] of response.headers) {
console.log(`${name}: ${value}`);
}
// 读取响应体(只能读取一次)
const json = await response.json(); // 解析 JSON
const text = await response.text(); // 纯文本
const blob = await response.blob(); // 二进制 Blob
const buffer = await response.arrayBuffer(); // ArrayBuffer
const formData = await response.formData(); // FormData
// 克隆响应(可以多次读取)
const cloned = response.clone();
const json = await response.json();
const text = await cloned.text();
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
# 15.2.5 取消请求(AbortController)
const controller = new AbortController();
// 传入 signal
const fetchPromise = fetch('/api/data', {
signal: controller.signal
});
// 5 秒后取消
setTimeout(() => controller.abort(), 5000);
try {
const response = await fetchPromise;
const data = await response.json();
} catch (error) {
if (error.name === 'AbortError') {
console.log('请求已取消');
} else {
throw error;
}
}
// 超时封装
function fetchWithTimeout(url, options = {}, timeout = 5000) {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), timeout);
return fetch(url, {
...options,
signal: options.signal
? AbortSignal.any([options.signal, controller.signal])
: controller.signal,
}).finally(() => clearTimeout(timeoutId));
}
// AbortSignal.timeout(更简洁,ES2022+)
fetch('/api/data', {
signal: AbortSignal.timeout(5000)
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
# 15.2.6 FormData 上传
// 表单数据上传
const formData = new FormData();
formData.append('name', 'Alice');
formData.append('avatar', fileInput.files[0]);
formData.append('tags', JSON.stringify(['developer', 'designer']));
// 不要手动设置 Content-Type,浏览器会自动设置 multipart/form-data 及 boundary
const response = await fetch('/api/upload', {
method: 'POST',
body: formData,
});
// 从 form 元素创建
const form = document.querySelector('#myForm');
const data = new FormData(form);
await fetch('/api/submit', { method: 'POST', body: data });
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 15.3 流式响应
# 15.3.1 ReadableStream
Fetch 的响应体是 ReadableStream,可以逐块读取大文件或流式数据:
const response = await fetch('/api/large-file');
const reader = response.body.getReader();
const contentLength = +response.headers.get('Content-Length');
let receivedLength = 0;
const chunks = [];
while (true) {
const { done, value } = await reader.read();
if (done) break;
chunks.push(value);
receivedLength += value.length;
console.log(`下载进度: ${(receivedLength / contentLength * 100).toFixed(1)}%`);
}
// 合并 chunks
const blob = new Blob(chunks);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 15.3.2 流式 JSON 解析(如 ChatGPT 流式响应)
async function streamChat(prompt) {
const response = await fetch('/api/chat', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ prompt, stream: true }),
});
const reader = response.body.getReader();
const decoder = new TextDecoder();
let buffer = '';
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split('\n');
buffer = lines.pop(); // 保留不完整的行
for (const line of lines) {
if (line.startsWith('data: ')) {
const data = line.slice(6);
if (data === '[DONE]') return;
try {
const parsed = JSON.parse(data);
const content = parsed.choices[0].delta.content;
if (content) {
process.stdout.write(content); // 逐字输出
}
} catch (e) {
// 忽略解析错误
}
}
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
# 15.3.3 TransformStream
// 创建一个文本行分割的转换流
function createLineSplitter() {
let buffer = '';
return new TransformStream({
transform(chunk, controller) {
buffer += chunk;
const lines = buffer.split('\n');
buffer = lines.pop();
for (const line of lines) {
controller.enqueue(line);
}
},
flush(controller) {
if (buffer) {
controller.enqueue(buffer);
}
}
});
}
// 使用
const response = await fetch('/api/logs');
const lineReader = response.body
.pipeThrough(new TextDecoderStream())
.pipeThrough(createLineSplitter());
for await (const line of lineReader) {
console.log('Line:', line);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
# 15.4 跨域资源共享(CORS)
# 15.4.1 同源策略
浏览器的同源策略限制从一个源加载的文档或脚本与另一个源的资源交互。同源要求协议、域名、端口三者完全相同。
https://example.com/page → https://example.com/api ✅ 同源
https://example.com/page → https://api.example.com/ ❌ 子域名不同
https://example.com/page → http://example.com/api ❌ 协议不同
https://example.com/page → https://example.com:8080/ ❌ 端口不同
1
2
3
4
2
3
4
# 15.4.2 CORS 工作原理
简单请求(满足以下全部条件):
- 方法:GET、HEAD、POST
- 头部仅包含:Accept、Accept-Language、Content-Language、Content-Type(仅 text/plain、multipart/form-data、application/x-www-form-urlencoded)
浏览器发送:
GET /api/users HTTP/1.1
Origin: https://frontend.com
服务器响应:
HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://frontend.com
1
2
3
4
5
6
7
2
3
4
5
6
7
预检请求(不满足简单请求条件时):
// 浏览器先发 OPTIONS 预检
OPTIONS /api/users HTTP/1.1
Origin: https://frontend.com
Access-Control-Request-Method: PUT
Access-Control-Request-Headers: Content-Type, Authorization
// 服务器响应预检
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://frontend.com
Access-Control-Allow-Methods: GET, POST, PUT, DELETE
Access-Control-Allow-Headers: Content-Type, Authorization
Access-Control-Max-Age: 86400
Access-Control-Allow-Credentials: true
// 预检通过后,发送实际请求
PUT /api/users/1 HTTP/1.1
Origin: https://frontend.com
Content-Type: application/json
Authorization: Bearer token123
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 15.4.3 携带凭证(Cookies)
// 前端
fetch('https://api.example.com/data', {
credentials: 'include' // 携带跨域 cookies
});
// 服务端必须设置:
// Access-Control-Allow-Origin: https://frontend.com(不能是 *)
// Access-Control-Allow-Credentials: true
1
2
3
4
5
6
7
8
2
3
4
5
6
7
8
# 15.4.4 CORS 响应头一览
| 响应头 | 说明 |
|---|---|
Access-Control-Allow-Origin | 允许的源(* 或具体域名) |
Access-Control-Allow-Methods | 允许的 HTTP 方法 |
Access-Control-Allow-Headers | 允许的请求头 |
Access-Control-Allow-Credentials | 是否允许携带凭证 |
Access-Control-Expose-Headers | 允许前端读取的响应头 |
Access-Control-Max-Age | 预检缓存时间(秒) |
# 15.4.5 其他跨域方案
// 1. JSONP(仅 GET,已过时)
function jsonp(url, callbackName) {
return new Promise((resolve) => {
const script = document.createElement('script');
window[callbackName] = (data) => {
resolve(data);
script.remove();
delete window[callbackName];
};
script.src = `${url}?callback=${callbackName}`;
document.body.appendChild(script);
});
}
// 2. 代理服务器(开发环境常用)
// vite.config.js
export default {
server: {
proxy: {
'/api': {
target: 'https://api.example.com',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, '')
}
}
}
};
// 3. postMessage(跨窗口通信)
// 父页面
const iframe = document.querySelector('iframe');
iframe.contentWindow.postMessage({ type: 'request', data: 'hello' }, 'https://other.com');
window.addEventListener('message', (e) => {
if (e.origin !== 'https://other.com') return;
console.log('收到响应:', e.data);
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
# 15.5 Server-Sent Events(SSE)
# 15.5.1 基本用法
SSE 是服务器向客户端推送事件的单向通道,基于 HTTP:
const source = new EventSource('/api/events');
// 接收默认事件
source.onmessage = (event) => {
console.log('收到消息:', event.data);
console.log('事件ID:', event.lastEventId);
};
// 接收命名事件
source.addEventListener('user-login', (event) => {
const user = JSON.parse(event.data);
console.log('用户登录:', user.name);
});
source.addEventListener('notification', (event) => {
console.log('通知:', event.data);
});
// 连接状态
source.onopen = () => console.log('SSE 连接已建立');
source.onerror = (e) => {
if (source.readyState === EventSource.CLOSED) {
console.log('SSE 连接已关闭');
}
};
// readyState: 0 = CONNECTING, 1 = OPEN, 2 = CLOSED
// 关闭连接
source.close();
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
# 15.5.2 SSE vs Fetch Stream vs WebSocket
| 特性 | SSE | Fetch Stream | WebSocket |
|---|---|---|---|
| 方向 | 服务器→客户端 | 服务器→客户端 | 双向 |
| 协议 | HTTP | HTTP | WS/WSS |
| 自动重连 | 内置 | 需手动 | 需手动 |
| 二进制数据 | 不支持 | 支持 | 支持 |
| 事件类型 | 内置支持 | 需手动解析 | 需手动 |
| 适用场景 | 通知推送、股票行情 | 大文件、AI 流式输出 | 聊天、游戏 |
# 15.6 WebSocket
# 15.6.1 基本用法
const ws = new WebSocket('wss://echo.websocket.org');
ws.onopen = () => {
console.log('WebSocket 连接已建立');
ws.send('Hello Server!');
ws.send(JSON.stringify({ type: 'chat', message: 'Hi' }));
};
ws.onmessage = (event) => {
console.log('收到消息:', event.data);
// 二进制数据
if (event.data instanceof Blob) {
// 处理 Blob
} else if (event.data instanceof ArrayBuffer) {
// 处理 ArrayBuffer
} else {
// 文本消息
const data = JSON.parse(event.data);
}
};
ws.onerror = (event) => {
console.error('WebSocket 错误');
};
ws.onclose = (event) => {
console.log(`连接关闭: code=${event.code}, reason=${event.reason}`);
// event.wasClean: 是否正常关闭
};
// 发送二进制数据
ws.binaryType = 'arraybuffer'; // 或 'blob'
ws.send(new ArrayBuffer(8));
// 检查状态
// ws.readyState: 0=CONNECTING, 1=OPEN, 2=CLOSING, 3=CLOSED
// 关闭连接
ws.close(1000, 'Normal closure');
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
# 15.6.2 自动重连的 WebSocket
class ReconnectingWebSocket {
#url;
#ws = null;
#reconnectAttempts = 0;
#maxReconnectAttempts = 10;
#reconnectInterval = 1000;
#handlers = { message: [], open: [], close: [], error: [] };
constructor(url) {
this.#url = url;
this.#connect();
}
#connect() {
this.#ws = new WebSocket(this.#url);
this.#ws.onopen = (e) => {
this.#reconnectAttempts = 0;
this.#handlers.open.forEach(h => h(e));
};
this.#ws.onmessage = (e) => {
this.#handlers.message.forEach(h => h(e));
};
this.#ws.onclose = (e) => {
this.#handlers.close.forEach(h => h(e));
if (!e.wasClean) {
this.#reconnect();
}
};
this.#ws.onerror = (e) => {
this.#handlers.error.forEach(h => h(e));
};
}
#reconnect() {
if (this.#reconnectAttempts >= this.#maxReconnectAttempts) {
console.error('达到最大重连次数');
return;
}
this.#reconnectAttempts++;
const delay = this.#reconnectInterval * Math.pow(2, this.#reconnectAttempts - 1);
console.log(`${delay}ms 后重连(第 ${this.#reconnectAttempts} 次)`);
setTimeout(() => this.#connect(), delay);
}
on(event, handler) {
this.#handlers[event]?.push(handler);
}
send(data) {
if (this.#ws.readyState === WebSocket.OPEN) {
this.#ws.send(typeof data === 'string' ? data : JSON.stringify(data));
}
}
close() {
this.#maxReconnectAttempts = 0; // 阻止重连
this.#ws.close(1000, 'Manual close');
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
# 15.7 请求封装
# 15.7.1 通用 HTTP 客户端
class HttpClient {
#baseURL;
#defaultHeaders;
#interceptors = { request: [], response: [] };
constructor(baseURL = '', defaultHeaders = {}) {
this.#baseURL = baseURL;
this.#defaultHeaders = {
'Content-Type': 'application/json',
...defaultHeaders,
};
}
// 拦截器
useRequestInterceptor(fn) {
this.#interceptors.request.push(fn);
}
useResponseInterceptor(fn) {
this.#interceptors.response.push(fn);
}
async #request(url, options = {}) {
let config = {
...options,
headers: { ...this.#defaultHeaders, ...options.headers },
};
// 执行请求拦截器
for (const interceptor of this.#interceptors.request) {
config = await interceptor(config);
}
const fullURL = this.#baseURL + url;
let response = await fetch(fullURL, config);
// 执行响应拦截器
for (const interceptor of this.#interceptors.response) {
response = await interceptor(response);
}
if (!response.ok) {
const error = new Error(`HTTP ${response.status}`);
error.response = response;
throw error;
}
const contentType = response.headers.get('Content-Type') || '';
if (contentType.includes('application/json')) {
return response.json();
}
return response.text();
}
get(url, options) {
return this.#request(url, { ...options, method: 'GET' });
}
post(url, data, options) {
return this.#request(url, {
...options,
method: 'POST',
body: JSON.stringify(data),
});
}
put(url, data, options) {
return this.#request(url, {
...options,
method: 'PUT',
body: JSON.stringify(data),
});
}
delete(url, options) {
return this.#request(url, { ...options, method: 'DELETE' });
}
}
// 使用
const api = new HttpClient('https://api.example.com');
// 添加认证拦截器
api.useRequestInterceptor(async (config) => {
const token = localStorage.getItem('token');
if (token) {
config.headers = {
...config.headers,
Authorization: `Bearer ${token}`,
};
}
return config;
});
// 添加错误处理拦截器
api.useResponseInterceptor(async (response) => {
if (response.status === 401) {
// 跳转登录页
window.location.href = '/login';
}
return response;
});
const users = await api.get('/users');
await api.post('/users', { name: 'Alice' });
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
# 15.7.2 请求重试
async function fetchWithRetry(url, options = {}, maxRetries = 3) {
let lastError;
for (let attempt = 0; attempt <= maxRetries; attempt++) {
try {
const response = await fetch(url, options);
// 5xx 错误可以重试
if (response.status >= 500 && attempt < maxRetries) {
throw new Error(`Server error: ${response.status}`);
}
return response;
} catch (error) {
lastError = error;
if (attempt < maxRetries) {
// 指数退避
const delay = Math.min(1000 * Math.pow(2, attempt), 10000);
const jitter = Math.random() * 1000;
await new Promise(r => setTimeout(r, delay + jitter));
console.log(`重试第 ${attempt + 1} 次...`);
}
}
}
throw lastError;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
# 15.7.3 请求去重与缓存
class RequestDedup {
#pending = new Map();
async fetch(url, options = {}) {
const key = `${options.method || 'GET'}:${url}`;
// 如果有相同请求正在进行,返回同一个 Promise
if (this.#pending.has(key)) {
return this.#pending.get(key);
}
const promise = fetch(url, options)
.then(res => res.json())
.finally(() => this.#pending.delete(key));
this.#pending.set(key, promise);
return promise;
}
}
// 带 TTL 的请求缓存
class CachedFetcher {
#cache = new Map();
async get(url, ttl = 60000) {
const cached = this.#cache.get(url);
if (cached && Date.now() - cached.timestamp < ttl) {
return cached.data;
}
const response = await fetch(url);
const data = await response.json();
this.#cache.set(url, { data, timestamp: Date.now() });
return data;
}
invalidate(url) {
this.#cache.delete(url);
}
clear() {
this.#cache.clear();
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
# 15.8 Beacon API
sendBeacon 用于在页面卸载时发送数据,保证数据不丢失:
// 页面关闭时发送统计数据
window.addEventListener('visibilitychange', () => {
if (document.visibilityState === 'hidden') {
const data = JSON.stringify({
event: 'page_leave',
duration: performance.now(),
url: location.href,
});
// sendBeacon 不受页面卸载影响
navigator.sendBeacon('/api/analytics', data);
}
});
// 也支持 Blob 和 FormData
const blob = new Blob([JSON.stringify(data)], { type: 'application/json' });
navigator.sendBeacon('/api/log', blob);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 15.9 性能相关 API
# 15.9.1 请求优先级
// Fetch Priority API
fetch('/api/critical-data', { priority: 'high' });
fetch('/api/prefetch-data', { priority: 'low' });
// HTML 元素优先级
// <img fetchpriority="high" src="hero.jpg">
// <link rel="preload" href="font.woff2" fetchpriority="high" as="font">
1
2
3
4
5
6
7
2
3
4
5
6
7
# 15.9.2 预加载资源
// 预连接
const link1 = document.createElement('link');
link1.rel = 'preconnect';
link1.href = 'https://api.example.com';
document.head.appendChild(link1);
// 预获取
const link2 = document.createElement('link');
link2.rel = 'prefetch';
link2.href = '/api/next-page-data';
document.head.appendChild(link2);
// DNS 预解析
const link3 = document.createElement('link');
link3.rel = 'dns-prefetch';
link3.href = 'https://cdn.example.com';
document.head.appendChild(link3);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 15.9.3 Performance API
// 测量请求性能
const entries = performance.getEntriesByType('resource');
entries.forEach(entry => {
console.log(`${entry.name}:`);
console.log(` DNS: ${entry.domainLookupEnd - entry.domainLookupStart}ms`);
console.log(` TCP: ${entry.connectEnd - entry.connectStart}ms`);
console.log(` TTFB: ${entry.responseStart - entry.requestStart}ms`);
console.log(` Download: ${entry.responseEnd - entry.responseStart}ms`);
console.log(` Total: ${entry.duration}ms`);
});
// 自定义性能标记
performance.mark('fetch-start');
const data = await fetch('/api/data').then(r => r.json());
performance.mark('fetch-end');
performance.measure('fetch-duration', 'fetch-start', 'fetch-end');
const measure = performance.getEntriesByName('fetch-duration')[0];
console.log(`请求耗时: ${measure.duration}ms`);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
上次更新: 2026/06/10, 11:13:41