fetch-sdk
Version:
A fetch wrapper SDK with streaming support
1,194 lines (1,010 loc) • 36.8 kB
Markdown
# 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 + Express)
app.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)}%`);
}
};
```