UNPKG

fetch-sdk

Version:

A fetch wrapper SDK with streaming support

1,194 lines (1,010 loc) 36.8 kB
# Fetch SDK 一个基于 Fetch API 的现代化 HTTP 客户端,提供简单易用的接口封装,支持请求拦截、流式数据和文件处理。 ## 特性 - ✨ 优雅的 API 设计,类似 axios 的使用体验 - 🚀 支持请求和响应拦截器 - 📦 自动 JSON 数据处理 - 🔄 支持请求取消 - 📊 支持上传/下载进度监控 - 📥 支持流式数据处理 - 🛡️ 完善的 TypeScript 支持 - 🔌 支持多实例创建 ## 快速开始 ### 安装 ```bash npm install fetch-sdk # 或 yarn add fetch-sdk ``` ### 基础使用 ```javascript import fetchClient from 'fetch-sdk'; // 创建实例(推荐方式) const service = fetchClient.create({ baseURL: 'https://api.example.com', // 可选: API的基础URL timeout: 5000, // 可选: 请求超时时间(毫秒) headers: { // 可选: 默认请求头 'Content-Type': 'application/json', // 默认值 'Accept': 'text/event-stream' // 可选: SSE 支持 }, withCredentials: true, // 可选: 跨域请求是否带凭证 validateStatus: (status) => status >= 200 && status < 500, // 可选: 状态码验证函数 stream: true // 可选: 启用流式处理 }); // 使用实例 try { // GET 请求: url 参数是必需的, options 对象是可选的 const data = await service.get('/users', { // '/users' 是必需的 params: { page: 1 }, // 可选: 查询参数 headers: { 'X-Token': 'xxx' } // 可选: 请求头 }); console.log('请求成功:', data); // POST 请求: url 参数是必需的, data 和 options 是可选的 const response = await service.post('/users', // '/users' 是必需的 { name: 'John', age: 30 }, // 可选: 请求体数据 { headers: { 'X-Custom': 'value' } } // 可选: 请求配置 ); // PUT 请求: url 参数是必需的, data 和 options 是可选的 await service.put('/users/1', // '/users/1' 是必需的 { name: 'Updated Name' }, // 可选: 请求体数据 { timeout: 3000 } // 可选: 请求配置 ); // DELETE 请求: url 参数是必需的, options 是可选的 await service.delete('/users/1', // '/users/1' 是必需的 { headers: { 'X-Token': 'xxx' } } // 可选: 请求配置 ); } catch (error) { console.error('请求失败:', error); } ``` ## 详细功能 ### 1. 实例配置 创建实例时可配置的选项(所有选项均为可选): | 配置项 | 类型 | 默认值 | 说明 | 示例 | |-------|------|-------|------|------| | baseURL | string | '' | 请求的基础URL | `'https://api.example.com'` | | timeout | number | 30000 | 请求超时时间(ms) | `5000` | | headers | object | `{'Content-Type': 'application/json'}` | 默认请求头 | `{ 'X-Token': 'xxx' }` | | validateStatus | function | `status => status >= 200 && status < 300` | 状态码校验 | `status => status < 500` | | withCredentials | boolean | false | 跨域请求是否带凭证 | `true` | | responseType | string | 'auto' | 响应数据类型 | `'json'` | ```javascript const service = fetchClient.create({ baseURL: 'https://api.example.com', timeout: 5000, headers: { 'Content-Type': 'application/json', 'X-Custom-Header': 'value' }, validateStatus: (status) => status < 500, withCredentials: true }); ``` ### 2. 请求方法 支持的请求方法及其使用: | 方法 | 参数 | 说明 | 示例 | |------|------|------|------| | get(url[, config]) | url: string, config?: RequestConfig | GET请求 | `service.get('/users', { params: { id: 1 } })` | | post(url[, data[, config]]) | url: string, data?: any, config?: RequestConfig | POST请求 | `service.post('/users', { name: 'John' })` | | put(url[, data[, config]]) | url: string, data?: any, config?: RequestConfig | PUT请求 | `service.put('/users/1', { name: 'John' })` | | delete(url[, config]) | url: string, config?: RequestConfig | DELETE请求 | `service.delete('/users/1')` | | request(url[, config]) | url: string, config?: RequestConfig | 通用请求方法 | `service.request('/api', { method: 'PATCH' })` | ```javascript // GET 请求示例 const getExample = async () => { // 1. 简单请求 const users = await service.get('/users'); // 2. 带查询参数 const user = await service.get('/users', { params: { id: 1, type: 'detail' } }); // 3. 带请求头 const data = await service.get('/data', { headers: { 'Authorization': 'Bearer token' } }); }; // POST 请求示例 const postExample = async () => { // 1. 发送 JSON 数据 await service.post('/users', { name: 'John', age: 30 }); // 2. 发送表单数据 const formData = new FormData(); formData.append('file', file); await service.post('/upload', formData); // 3. 发送 URL 编码数据 const params = new URLSearchParams(); params.append('name', 'John'); await service.post('/submit', params); }; ``` ### 3. 拦截器 拦截器配置及使用: ```javascript // 请求拦截器 service.interceptors.request.use( config => { // 请求前处理 config.headers['Token'] = getToken(); return config; }, error => { // 请求错误处理 return Promise.reject(error); } ); // 响应拦截器 service.interceptors.response.use( response => { // 统一处理响应 const { code, data, message } = response.data; if (code === 0) { return data; } throw new Error(message); }, error => { // 错误处理 if (error.response?.status === 401) { // 处理未授权 } return Promise.reject(error); } ); ``` ### 4. 文件处理 文件上传和下载功能: > **⚠️ 重要提示:** 使用 FormData 上传文件时,请勿手动设置 `'Content-Type': 'multipart/form-data'` 头部。浏览器需要自动添加带有 boundary 参数的完整 Content-Type 头,例如:`multipart/form-data; boundary=----WebKitFormBoundaryXYZ123`。手动设置会导致服务器无法正确解析请求,出现 "Failed to parse multipart request" 之类的错误。 #### 4.1 基础文件处理 ```javascript // 1. 文件上传 const uploadFile = async (file) => { const formData = new FormData(); formData.append('file', file); try { await service.post('/upload', formData, { // 注意:使用 FormData 上传文件时,不要手动设置 Content-Type // 让浏览器自动设置,包含必要的 boundary 参数 // 错误示例 ❌ // headers: { // 'Content-Type': 'multipart/form-data' // 这会导致请求失败 // }, // 正确示例 ✓ onProgress: ({ loaded, total, progress }) => { console.log(`上传进度: ${progress}%`); } }); } catch (error) { console.error('上传失败:', error); } }; // 2. 文件下载 const downloadFile = async () => { try { const blob = await service.download('/files/report.pdf', { filename: 'report.pdf', // 可选: 提供此参数将触发浏览器下载 mimeType: 'application/pdf', // 可选: 指定文件类型 onProgress: ({ progress }) => { // 可选: 下载进度回调 console.log(`下载进度: ${progress}%`); } }); // 如果不想自动下载,可以自行处理 blob return blob; } catch (error) { console.error('下载失败:', error); } }; ``` #### 4.2 断点续传 SDK 提供了文件上传和下载的断点续传功能,支持大文件传输时的断点恢复。 ##### 配置选项 | 选项 | 类型 | 默认值 | 是否必需 | 说明 | |------|------|--------|---------|------| | chunkSize | number | 1024 * 1024 | 否 | 分片大小(字节) | | onProgress | function | - | 否 | 进度回调函数 | | mimeType | string | 'application/octet-stream' | 否 | 文件类型 | | filename | string | - | 仅下载时为必需 | 保存的文件名 | | headers | object | {} | 否 | 自定义请求头 | | retryCount | number | 3 | 否 | 失败重试次数 | | retryDelay | number | 1000 | 否 | 重试间隔(毫秒) | ##### 断点续传上传方法参数 ```javascript /** * 断点续传文件上传 * @param {File|Blob} file - 要上传的文件对象 (必需) * @param {string} url - 上传地址 (必需) * @param {Object} options - 配置选项 (可选) * @returns {Promise<Object>} - 上传结果 */ uploadWithResume(file, url, options = {}) ``` ##### 上传断点续传示例 ```javascript // 断点续传上传示例 const handleUploadWithResume = async (file) => { try { await service.uploadWithResume(file, '/api/upload', { onProgress: ({ uploaded, total, progress }) => { console.log(`上传进度: ${progress}%`); } }); console.log('上传完成'); } catch (error) { console.error('上传暂停,已保存断点:', error.message); // 稍后可以使用相同的参数重新调用来继续上传 } }; ``` ##### 断点续传下载方法参数 ```javascript /** * 普通文件下载方法 * @param {string} url - 下载地址 (必需) * @param {Object} options - 配置选项 (可选) * @param {string} options.filename - 下载保存的文件名 (可选,提供此参数将自动触发浏览器下载) * @param {string} options.mimeType - 文件MIME类型 (可选,默认为'application/octet-stream') * @param {Function} options.onProgress - 下载进度回调 (可选) * @returns {Promise<Blob>} - 下载的文件Blob对象 */ download(url, options = {}) ``` ##### 下载断点续传示例 ```javascript // 断点续传下载示例 const handleDownloadWithResume = async () => { try { const blob = await service.downloadWithResume('/api/files/large.zip', { filename: 'large.zip', // 必需参数 mimeType: 'application/zip', // 可选参数 onProgress: ({ downloaded, total, progress }) => { console.log(`下载进度: ${progress}%`); } }); // 下载完成后处理文件 const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = 'large.zip'; a.click(); URL.revokeObjectURL(url); } catch (error) { console.error('下载暂停,已保存断点:', error.message); // 稍后可以使用相同的参数重新调用来继续下载 } }; ``` ##### 特性说明 断点续传功能特点: 1. 自动分片: - 默认分片大小为 1MB - 可通过配置自定义分片大小 - 支持超大文件传输 2. 进度保存: - 自动保存传输进度到 localStorage - 断点信息持久化 - 支持页面刷新后继续传输 3. 错误处理: - 网络错误自动保存断点 - 支持手动暂停/继续 - 提供详细的错误信息 4. 进度监控: - 实时进度回调 - 提供已传输大小和总大小信息 - 支持进度百分比计算 ##### 配置选项 | 选项 | 类型 | 默认值 | 说明 | |------|------|--------|------| | chunkSize | number | 1024 * 1024 | 分片大小(字节) | | onProgress | function | - | 进度回调函数 | | mimeType | string | 'application/octet-stream' | 文件类型 | | filename | string | - | 保存的文件名 | ##### 服务端配置要求 服务端需要支持以下功能: 1. 分片上传接口: ```javascript // 服务端接收分片示例(Node.js + Expressapp.post('/upload', (req, res) => { const uploadId = req.headers['x-upload-id']; const chunkIndex = req.headers['x-chunk-index']; const totalChunks = req.headers['x-total-chunks']; // 处理分片... }); ``` 2. 断点下载支持: ```javascript // 服务端支持断点下载示例 app.get('/download', (req, res) => { const range = req.headers.range; if (range) { // 处理断点下载请求... res.status(206); res.set('Accept-Ranges', 'bytes'); // 发送部分内容... } }); ``` ### 5. EventStream 响应处理 Server-Sent Events (SSE) 基本使用示例: > **注意:** 使用流式处理(`stream: true`)时,`responseType` 设置将被忽略,因为流数据始终以文本形式返回。如果需要 JSON 数据,需要手动解析。 ```javascript /** * EventStream 处理配置 * @param {boolean} stream - 启用流式处理 (必需,值为true) * @param {Object} headers - 请求头 (推荐) * @param {string} headers['Accept'] - 接受的内容类型 (推荐设置为'text/event-stream') */ ``` ```javascript const handleEventStream = async () => { // 创建流式请求 const stream = await service.request('/events', { stream: true, // 必需: 启用流式处理 headers: { 'Accept': 'text/event-stream' // 推荐: 声明接受SSE格式 }, // responseType: 'json', // 注意:此设置在流式处理时不生效 method: 'POST', // 可选: 默认为GET data: { // 可选: 请求体数据 message: 'Hello', type: 'chat' } }); try { while (true) { const chunk = await stream.read(); if (chunk === null) break; // 流结束 // 需要手动解析 JSON 字符串 try { const data = JSON.parse(chunk); console.log('收到 JSON 数据:', data); } catch (e) { console.log('收到普通文本:', chunk); } } } catch (error) { console.error('处理错误:', error); } finally { if (stream?.reader) { await stream.reader.cancel(); // 重要: 清理资源 } } }; // 使用工具函数处理 JSON 流 const handleJSONStream = async () => { const stream = await service.request('/api/chat', { stream: true, // 必需: 启用流式处理 method: 'POST', // 可选: 默认为GET data: { message: 'Hello' } // 可选: 请求体数据 }); const processJSON = (text) => { try { return JSON.parse(text); } catch (e) { return text; } }; try { while (true) { const chunk = await stream.read(); if (chunk === null) break; const data = processJSON(chunk); console.log('处理后的数据:', data); } } finally { if (stream?.reader) { await stream.reader.cancel(); // 重要: 清理资源 } } }; ``` 更多高级用法请参考文档底部的 [高级特性 - EventStream](#高级特性) 章节。 ### 跨域认证配置 跨域请求时的认证处理配置: | 配置项 | 类型 | 默认值 | 说明 | |-------|------|-------|------| | withCredentials | boolean | false | 跨域请求时是否携带认证信息(cookies、HTTP认证及客户端SSL证书等)| | credentials | string | 'same-origin' | 请求的凭据模式,可选值:'omit''same-origin''include' | ```javascript // 1. 使用 withCredentials const service = fetchClient.create({ baseURL: 'https://api.example.com', withCredentials: true, // 允许跨域请求携带 cookies // ... }); // 2. 使用 credentials const service = fetchClient.create({ baseURL: 'https://api.example.com', credentials: 'include', // 同 withCredentials: true // ... }); ``` 注意事项: 1. 当设置 `withCredentials: true` 时: - 服务端必须设置 `Access-Control-Allow-Credentials: true` - 服务端的 `Access-Control-Allow-Origin` 不能设置为 '*',必须指定具体域名 - 响应头中的 `Set-Cookie` 才会被浏览器接受并存储 2. credentials 可选值说明: - `'omit'`: 从不发送 cookies - `'same-origin'`: 只有当请求同源时才发送 cookies(默认值) - `'include'`: 总是发送 cookies,等同于 `withCredentials: true` 3. 安全考虑: - 启用此配置会增加 CSRF 攻击风险,建议同时实现 CSRF 令牌机制 - 仅在确实需要跨域认证时才启用此配置 - 建议配合 HTTPS 使用,确保数据传输安全 4. 使用场景: - 跨域登录认证 - 需要访问用户会话数据 - 多服务之间的认证信息共享 ```javascript // 完整配置示例 const service = fetchClient.create({ baseURL: 'https://api.example.com', withCredentials: true, headers: { 'Content-Type': 'application/json', 'X-Requested-With': 'XMLHttpRequest' // 用于识别 AJAX 请求 }, validateStatus: (status) => status >= 200 && status < 500, }); // 配合服务端设置示例(Node.js + Express) app.use(cors({ origin: 'http://localhost:8080', // 指定允许的源 credentials: true, // 允许携带认证信息 methods: ['GET', 'POST', 'PUT', 'DELETE'], allowedHeaders: ['Content-Type', 'X-Requested-With'] })); ``` ## API 文档 ### 请求方法 | 方法 | 参数 | 说明 | 示例 | |------|------|------|------| | get | (url, options?) | GET 请求,url为必需参数,options为可选 | `service.get('/users', { params: { page: 1 } })` | | post | (url, data?, options?) | POST 请求,url为必需参数,data和options为可选 | `service.post('/users', { name: 'John' })` | | put | (url, data?, options?) | PUT 请求,url为必需参数,data和options为可选 | `service.put('/users/1', { name: 'Updated' })` | | delete | (url, options?) | DELETE 请求,url为必需参数,options为可选 | `service.delete('/users/1')` | | request | (url, options?) | 自定义请求方法,url为必需参数,options为可选 | `service.request('/api', { method: 'PUT' })` | ### 请求配置 | 选项 | 类型 | 默认值 | 是否必需 | 说明 | 示例 | |------|------|--------|---------|------|------| | method | string | 'GET' | 否 | 请求方法 | `{ method: 'POST' }` | | headers | object | `{'Content-Type': 'application/json'}` | 否 | 自定义请求头 | `{ headers: { 'X-Token': 'xxx' } }` | | params | object | - | 否 | URL 查询参数 | `{ params: { id: 1 } }` | | data | any | - | 否 | 请求体数据 | `{ data: { name: 'test' } }` | | timeout | number | 30000 | 否 | 请求超时时间(ms) | `{ timeout: 5000 }` | | responseType | string | 'auto' | 否 | 响应数据类型('json'/'text'/'blob'/'arrayBuffer'/'formData'/'auto') | `{ responseType: 'json' }` | | signal | AbortSignal | - | 否 | 用于取消请求的信号 | `{ signal: controller.signal }` | | stream | boolean | false | 否 | 以流式方式处理响应 | `{ stream: true }` | | withCredentials | boolean | false | 否 | 是否携带凭证 | `{ withCredentials: true }` | | validateStatus | function | status => status >= 200 && status < 300 | 否 | 响应状态码校验 | `{ validateStatus: status => status < 500 }` | ### stream 配置详解 stream 配置用于处理流式响应数据,主要用于以下场景: 1. Server-Sent Events (SSE) 接收实时更新 2. 大文件下载时分块处理 3. 流式 API 响应(如 ChatGPT 流式响应) 使用示例: ```javascript // 1. 基础流式处理 const stream = await service.request('/stream-api', { stream: true, headers: { 'Accept': 'text/event-stream' } }); // 使用 iterateLines 按行读取(适用于 SSE) for await (const line of stream.iterateLines()) { if (line.startsWith('data: ')) { const data = JSON.parse(line.slice(6)); console.log('收到数据:', data); } } // 2. 带进度监控的流式处理 const stream = await service.request('/large-file', { stream: true, onProgress: ({ loaded, total, progress }) => { if (total) { console.log(`接收进度: ${progress}%`); } else { console.log(`已接收: ${loaded} bytes`); } } }); // 使用 read 方法逐块读取数据 let chunk; while ((chunk = await stream.read()) !== null) { console.log('收到数据块:', chunk); } // 3. 资源清理示例 let stream; try { stream = await service.request('/stream', { stream: true }); for await (const line of stream.iterateLines()) { console.log('接收到数据:', line); } } finally { // 确保释放资源 if (stream?.reader) { await stream.reader.cancel(); } } ``` 流式响应对象的属性和方法: | 属性/方法 | 类型 | 是否必需 | 说明 | |----------|------|---------|------| | read | async function | 必需使用此方法或iterateLines | 读取下一个数据块,返回 null 表示数据已读取完毕 | | iterateLines | async iterator | 必需使用此方法或read | 按行迭代数据,适合处理文本流和 SSE | | total | number | 自动提供 | 总数据大小(仅当服务器提供 content-length 时可用)| | loaded | number | 自动提供 | 当前已接收的数据大小 | | reader | ReadableStreamDefaultReader | 内部属性 | 底层读取器实例,用于手动控制和资源清理 | 注意事项: 1. 使用 `read()` 或 `iterateLines()` 方法是必需的: - 您必须选择其中一种方法来消费流 - 两种方法不能混用于同一流 2. 资源管理(强烈推荐): - 务必在 finally 中调用 `reader.cancel()` - 避免资源泄露 - 支持提前中断数据流 ## 错误处理 SDK 使用标准化的错误对象,包含以下属性: ```javascript try { await service.get('/api'); } catch (error) { // error.config - 请求配置信息 // error.request - 请求实例 // error.response - 响应对象(如果存在) // error.status - HTTP状态码(如果存在) // error.statusText - 状态描述(如果存在) console.log(error.message); // 错误消息 } ``` ### 请求取消 使用标准的 AbortController 来取消请求: ```javascript const controller = new AbortController(); service.get('/api/data', { signal: controller.signal }).catch(error => { if (error.name === 'AbortError') { console.log('请求已被取消'); } }); // 取消请求 controller.abort(); ``` #### 实际应用场景 1. 搜索场景下取消上一次请求: ```javascript let controller = null; const handleSearch = async (keyword) => { // 取消之前的请求 if (controller) { controller.abort(); } // 创建新的 controller controller = new AbortController(); try { const result = await service.get('/search', { params: { keyword }, signal: controller.signal }); return result; } catch (error) { if (error.name !== 'AbortError') { throw error; } } }; ``` 2. 组件卸载时取消请求: ```javascript import React, { useEffect } from 'react'; function DataComponent() { useEffect(() => { const controller = new AbortController(); const fetchData = async () => { try { const data = await service.get('/api/data', { signal: controller.signal }); // 处理数据 } catch (error) { if (error.name !== 'AbortError') { console.error('获取数据失败:', error); } } }; fetchData(); // 组件卸载时取消请求 return () => controller.abort(); }, []); return <div>...</div>; } ``` 3. 超时和取消的结合: ```javascript const timeoutRequest = async (url, timeout = 5000) => { const controller = new AbortController(); try { const response = await service.get(url, { signal: controller.signal, timeout }); return response; } catch (error) { if (error.name === 'AbortError') { throw new Error('请求超时或被取消'); } throw error; } }; ``` ## 最佳实践 ### 请求取消示例 ```javascript // 1. 引入CancelToken import fetchClient, { CancelToken } from 'fetch-sdk'; // 2. 创建AbortController const controller = new AbortController(); // 3. 发起可取消请求 const getUserRequest = service.get('/users', { signal: controller.signal }).catch(error => { if (error.name === 'AbortError') { console.log('请求已被取消'); } }); // 4. 取消请求 controller.abort(); // 5. 复用signal(可选) const getPostsRequest = service.get('/posts', { signal: controller.signal // 使用同一个signal }); ``` 1. 通用配置集中管理 ```javascript // api.js const client = new FetchClient('https://api.example.com', { timeout: 5000, validateStatus: status => status < 500, headers: { 'Accept': 'application/json', 'X-Client-Version': '1.0.0' } }); // 统一错误处理 client.addResponseInterceptor( response => response, error => { handleApiError(error); return Promise.reject(error); } ); export default client; ``` 2. 业务模块封装 ```javascript // userApi.js import client from './api'; export const userApi = { getProfile: () => client.get('/user/profile'), updateProfile: (data) => client.post('/user/profile', data), uploadAvatar: (file) => { const formData = new FormData(); formData.append('avatar', file); return client.post('/user/avatar', formData); } }; ``` 3. 请求取消处理 ```javascript // 搜索场景 let searchCancel; const search = async (keyword) => { // 取消上一次请求 if (searchCancel) { searchCancel('新搜索请求发起'); } const { token, cancel } = CancelToken.source(); searchCancel = cancel; try { const result = await service.get('/search', { params: { keyword }, cancelToken: token }); return result; } catch (error) { if (!isCancel(error)) { throw error; } } }; ``` 4. 流式数据优雅处理 ```javascript const handleStreamWithCleanup = async () => { let stream; try { stream = await service.request('/stream', { stream: true }); for await (const line of stream.iterateLines()) { processLine(line); } } catch (error) { console.error('Stream error:', error); } finally { // 确保资源释放 if (stream?.reader) { await stream.reader.cancel(); } } }; ``` ## 高级特性 ### EventStream 详细说明 1. 作用说明: - 告知服务器客户端期望接收 Server-Sent Events (SSE) 格式的数据流 - 服务器根据此头部判断是否使用 SSE 协议发送数据 - 建立长连接,保持数据流的持续推送 2. 是否必需: - 不是强制必需的,但强烈建议设置 - 不设置可能导致的问题: * 服务器可能返回普通 HTTP 响应而不是事件流 * 某些服务器会拒绝不带正确 Accept 头的 SSE 请求 * 无法享受浏览器对 SSE 的原生优化(如自动重连) 3. 使用场景: ```javascript // 推荐的完整配置 const service = fetchClient.create({ headers: { 'Accept': 'text/event-stream', // 声明接收 SSE 'Cache-Control': 'no-cache', // 禁用缓存 'Connection': 'keep-alive' // 保持连接 } }); // 最小配置(不推荐) const service = fetchClient.create({ // 不设置 Accept 头,依赖服务器默认行为 }); ``` 4. 服务器端对应配置: ```javascript // Node.js Express 示例 app.get('/events', (req, res) => { // 检查客户端是否支持 SSE if (req.headers.accept && req.headers.accept.includes('text/event-stream')) { res.setHeader('Content-Type', 'text/event-stream'); res.setHeader('Cache-Control', 'no-cache'); res.setHeader('Connection', 'keep-alive'); // 发送事件流 const sendEvent = () => { res.write(`data: ${JSON.stringify({ time: new Date() })}\n\n`); }; const timer = setInterval(sendEvent, 1000); req.on('close', () => clearInterval(timer)); } else { // 客户端不支持 SSE,返回普通响应 res.json({ error: 'SSE not supported' }); } }); ``` 5. 调试技巧: - 使用浏览器开发者工具查看 Network 面板 - 确认请求头中包含 `Accept: text/event-stream` - 检查响应头中的 `Content-Type: text/event-stream` - 观察连接是否保持打开状态 6. 常见问题处理: ```javascript // 处理连接断开 let retryCount = 0; const maxRetries = 3; const connectSSE = async () => { try { const stream = await service.request('/events', { stream: true, headers: { 'Accept': 'text/event-stream' } }); retryCount = 0; // 重置重试计数 for await (const line of stream.iterateLines()) { processEventData(line); } } catch (error) { if (retryCount < maxRetries) { retryCount++; console.log(`连接断开,${retryCount}秒后重试...`); setTimeout(connectSSE, retryCount * 1000); } else { console.error('多次重试失败,放弃连接'); } } }; ``` 7. 性能考虑: - SSE 连接会占用服务器资源,建议设置最大连接数 - 考虑在不需要时及时关闭连接 - 可以使用心跳机制检测连接状态 ```javascript // 心跳检测示例 let lastEventTime = Date.now(); const heartbeatInterval = setInterval(() => { const now = Date.now(); if (now - lastEventTime > 30000) { // 30秒无数据 controller.abort(); // 断开连接 clearInterval(heartbeatInterval); connectSSE(); // 重新连接 } }, 5000); ``` ### 高级用法示例 #### 1. EventStream 高级处理 1.1 使用 iterateLines 处理 SSE: ```javascript const handleSSEWithLines = async () => { const stream = await service.request('/events', { stream: true, headers: { 'Accept': 'text/event-stream' } }); try { for await (const line of stream.iterateLines()) { if (line.startsWith('data: ')) { console.log('收到数据:', JSON.parse(line.slice(6))); } } } catch (error) { console.error('处理错误:', error); } }; ``` 1.2 带缓冲的数据处理: ```javascript const handleStreamWithBuffer = async () => { const stream = await service.request('/events', { stream: true, headers: { 'Accept': 'text/event-stream' } }); try { let buffer = ''; while (true) { const chunk = await stream.read(); if (chunk === null) break; buffer += chunk; const lines = buffer.split('\n'); buffer = lines.pop() || ''; for (const line of lines) { if (line.startsWith('data: ')) { console.log('处理数据:', JSON.parse(line.slice(6))); } } } } catch (error) { console.error('处理错误:', error); } }; ``` 1.3 带超时和重试的事件流处理: ```javascript const handleStreamWithTimeout = async () => { const stream = await service.request('/events', { stream: true, headers: { 'Accept': 'text/event-stream' } }); try { while (true) { const timeoutPromise = new Promise((_, reject) => { setTimeout(() => reject(new Error('读取超时')), 5000); }); const chunk = await Promise.race([ stream.read(), timeoutPromise ]); if (chunk === null) break; console.log('收到数据:', chunk); } } catch (error) { if (error.message === '读取超时') { console.log('准备重试...'); } } }; ``` #### 2. 请求重试机制 ```javascript const request = async (url, options = {}, retries = 3) => { for (let i = 0; i < retries; i++) { try { return await service.request(url, options); } catch (error) { if (i === retries - 1) throw error; await new Promise(resolve => setTimeout(resolve, 1000 * (i + 1))); } } }; ``` #### 3. 批量请求处理 ```javascript const batchRequest = async (urls, concurrency = 3) => { const results = []; const queue = [...urls]; const workers = Array(concurrency).fill().map(async () => { while (queue.length) { const url = queue.shift(); const result = await service.get(url); results.push(result); } }); await Promise.all(workers); return results; }; ``` #### 4. 智能缓存 ```javascript const cacheMap = new Map(); const cachedRequest = async (url, options = {}, ttl = 60000) => { const key = `${url}-${JSON.stringify(options)}`; const cached = cacheMap.get(key); if (cached && Date.now() - cached.timestamp < ttl) { return cached.data; } const data = await service.request(url, options); cacheMap.set(key, { data, timestamp: Date.now() }); return data; }; ``` #### 5. WebSocket 降级方案 ```javascript class RealTimeClient { constructor(url) { this.url = url; this.fallbackToSSE = false; } async connect() { try { if (!this.fallbackToSSE) { // 尝试 WebSocket this.ws = new WebSocket(this.url); } else { // 降级到 SSE const stream = await service.request(this.url, { stream: true, headers: { 'Accept': 'text/event-stream' } }); this.handleSSE(stream); } } catch (error) { this.fallbackToSSE = true; this.connect(); } } } ``` #### 6. 文件上传断点续传 ```javascript const uploadWithResume = async (file, chunkSize = 1024 * 1024) => { const chunks = Math.ceil(file.size / chunkSize); let uploaded = 0; for (let i = 0; i < chunks; i++) { const chunk = file.slice(i * chunkSize, (i + 1) * chunkSize); const formData = new FormData(); formData.append('chunk', chunk); formData.append('index', i); await service.post('/upload', formData, { // 注意:使用 FormData 时,不要手动设置 Content-Type headers: { // 'Content-Type': 'multipart/form-data', // ❌ 不要设置这个 'X-Upload-Id': uploadId, 'X-Chunk-Index': i, 'X-Total-Chunks': chunks } }); uploaded += chunk.size; console.log(`上传进度: ${Math.round((uploaded / file.size) * 100)}%`); } }; ```