UNPKG

fetch-sdk

Version:

A simple and lightweight fetch wrapper SDK

772 lines (640 loc) 23 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' // 默认值 }, withCredentials: true, // 可选: 跨域请求是否带凭证 timeout: 5000 // 可选: 请求超时时间(毫秒) }); // 使用实例 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 是可选的 // 注意:对象会自动序列化为JSON字符串 const response = await service.post('/users', // '/users' 是必需的 { name: 'John', age: 30 }, // 对象会自动序列化为JSON字符串 { headers: { 'X-Custom': 'value' } } // 可选: 请求配置 ); // PUT 请求: url 参数是必需的, data 和 options 是可选的 await service.put('/users/1', // '/users/1' 是必需的 { name: 'Updated Name' }, // 对象会自动序列化为JSON字符串 { headers: { }, 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' }` | | withCredentials | boolean | false | 跨域请求是否带凭证 | `true` | ```javascript const service = fetchClient.create({ baseURL: 'https://api.example.com', timeout: 5000, headers: { 'Content-Type': 'application/json', 'X-Custom-Header': 'value' }, 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请求,对象数据会自动序列化为JSON | `service.post('/users', { name: 'John' })` | | put(url[, data[, config]]) | url: string, data?: any, config?: RequestConfig | PUT请求,对象数据会自动序列化为JSON | `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', // 可选: 提供此参数将触发浏览器下载 onProgress: ({ progress }) => { // 可选: 下载进度回调 console.log(`下载进度: ${progress}%`); } }); // 如果不想自动下载,可以自行处理 blob return blob; } catch (error) { console.error('下载失败:', error); } }; ``` #### 4.2 断点续传 SDK 提供了文件上传和下载的断点续传功能,支持大文件传输时的断点恢复。 ##### 断点续传上传方法参数 ```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 {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', // 必需参数 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 | - | 进度回调函数 | | 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'); // 发送部分内容... } }); ``` ### 跨域认证配置 跨域请求时的认证处理配置: | 配置项 | 类型 | 默认值 | 说明 | |-------|------|-------|------| | 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 请求 } }); // 配合服务端设置示例(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 }` | | signal | AbortSignal | - | 否 | 用于取消请求的信号 | `{ signal: controller.signal }` | | withCredentials | boolean | false | 否 | 是否携带凭证 | `{ withCredentials: true }` | ## 错误处理 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, 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; } } }; ``` #### 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; }; ``` #### 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)}%`); } }; ```