编程进阶网 编程进阶网
首页
  • 计算机原理
  • 操作系统
  • 网络协议
  • 数据库原理
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 性能优化
  • 编程原理
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 基础认知
  • 线性结构
  • 树与哈希
  • 工业级实现
  • 算法思想
  • 实战与综合
  • 算法题考核
  • C语言入门
  • C综合案例
  • C专栏博客
  • C标准集库
  • C++入门教程
  • C++综合案例
  • C++专栏博客
  • C++开发技巧
  • Java入门教程
  • Java综合案例
  • Java专栏博客
  • Go入门教程
  • Go综合案例
  • Go专栏博客
  • Go开发技巧
  • JavaScript入门
  • JavaScript高级
  • Android库解读
  • Android专栏
  • Android智能硬件
  • iOS ObjC入门
  • iOS Swift入门
  • iOS入门精通
  • Web之Html手册
  • Web之TypeScript
  • Web之Vue高级进阶
  • Linux之QML入门
  • Linux之QT核心库
  • Linux实践开发
  • Python教程
  • Shell&Bash教程
  • 工具脚本
  • 自动化脚本
  • 质量保障
  • 产品思考
  • 软实力
  • 开发流程
  • Git应用
  • 技术模版
  • 技术规范
  • Markdown
  • Mermaid
  • 开源协议
  • JSON工具
  • 文本工具
  • 图片处理
  • 文档转化
  • 代码压缩
  • 关于我
  • 自我精进
  • 职场管理
  • 职场面试
  • 心情杂货
  • 友情链接

杨充

专注编程 · 终身学习者
首页
  • 计算机原理
  • 操作系统
  • 网络协议
  • 数据库原理
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 性能优化
  • 编程原理
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 基础认知
  • 线性结构
  • 树与哈希
  • 工业级实现
  • 算法思想
  • 实战与综合
  • 算法题考核
  • C语言入门
  • C综合案例
  • C专栏博客
  • C标准集库
  • C++入门教程
  • C++综合案例
  • C++专栏博客
  • C++开发技巧
  • Java入门教程
  • Java综合案例
  • Java专栏博客
  • Go入门教程
  • Go综合案例
  • Go专栏博客
  • Go开发技巧
  • JavaScript入门
  • JavaScript高级
  • Android库解读
  • Android专栏
  • Android智能硬件
  • iOS ObjC入门
  • iOS Swift入门
  • iOS入门精通
  • Web之Html手册
  • Web之TypeScript
  • Web之Vue高级进阶
  • Linux之QML入门
  • Linux之QT核心库
  • Linux实践开发
  • Python教程
  • Shell&Bash教程
  • 工具脚本
  • 自动化脚本
  • 质量保障
  • 产品思考
  • 软实力
  • 开发流程
  • Git应用
  • 技术模版
  • 技术规范
  • Markdown
  • Mermaid
  • 开源协议
  • JSON工具
  • 文本工具
  • 图片处理
  • 文档转化
  • 代码压缩
  • 关于我
  • 自我精进
  • 职场管理
  • 职场面试
  • 心情杂货
  • 友情链接
  • README
  • C语言入门精通

  • Cpp入门到精通

  • Java入门精通

  • Go入门到精通

  • JavaScript入门

    • 基础入门

      • README
      • 入门介绍
      • 数据类型
      • 运算符
      • 函数
      • 面向对象
      • 标准库
      • 异步操作
      • 事件设计
      • 错误机制
      • 模块开发
      • 字符串处理
      • 迭代器与生成器
      • Symbol
      • DOM操作
      • 网络请求
        • 15.1 XMLHttpRequest
          • 15.1.1 基本用法
          • 15.1.2 POST 请求
          • 15.1.3 进度监控
          • 15.1.4 超时处理
          • 15.1.5 取消请求
        • 15.2 Fetch API
          • 15.2.1 基本用法
          • 15.2.2 请求配置
          • 15.2.3 Request 对象
          • 15.2.4 Response 对象
          • 15.2.5 取消请求(AbortController)
          • 15.2.6 FormData 上传
        • 15.3 流式响应
          • 15.3.1 ReadableStream
          • 15.3.2 流式 JSON 解析(如 ChatGPT 流式响应)
          • 15.3.3 TransformStream
        • 15.4 跨域资源共享(CORS)
          • 15.4.1 同源策略
          • 15.4.2 CORS 工作原理
          • 15.4.3 携带凭证(Cookies)
          • 15.4.4 CORS 响应头一览
          • 15.4.5 其他跨域方案
        • 15.5 Server-Sent Events(SSE)
          • 15.5.1 基本用法
          • 15.5.2 SSE vs Fetch Stream vs WebSocket
        • 15.6 WebSocket
          • 15.6.1 基本用法
          • 15.6.2 自动重连的 WebSocket
        • 15.7 请求封装
          • 15.7.1 通用 HTTP 客户端
          • 15.7.2 请求重试
          • 15.7.3 请求去重与缓存
        • 15.8 Beacon API
        • 15.9 性能相关 API
          • 15.9.1 请求优先级
          • 15.9.2 预加载资源
          • 15.9.3 Performance API
    • 综合案例

    • 专栏博客

  • CodeX
  • JavaScript入门
  • 基础入门
杨充
2026-04-13
目录

网络请求

# 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

# 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

# 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

# 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

# 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

# 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

# 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

# 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

# 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

# 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

# 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

# 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

# 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

# 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

# 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

# 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

预检请求(不满足简单请求条件时):

// 浏览器先发 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

# 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

# 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

# 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

# 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

# 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

# 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

# 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

# 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

# 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

# 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

# 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

# 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
上次更新: 2026/06/10, 11:13:41
DOM操作
README

← DOM操作 README→

最近更新
01
信号崩溃快速排查
06-15
02
CoreDump破案
06-15
03
perf火焰图实战
06-15
更多文章>
Theme by Vdoing | Copyright © 2019-2026 杨充 | MIT License | 桂ICP备2024034950号 | 桂公网安备45142202000030
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式