# Fetch SDK

一个基于 Fetch API 的现代化 HTTP 客户端，提供简单易用的接口封装，支持请求拦截、流式数据和文件处理。

## 特性

- ✨ 优雅的 API 设计，类似 axios 的使用体验
- 🚀 支持请求和响应拦截器
- 📦 自动 JSON 数据处理
- 🔄 支持请求取消
- 📊 支持上传/下载进度监控
- 📥 支持流式数据处理
- 🛡️ 完善的 TypeScript 支持
- 🔌 支持多实例创建

## 快速开始

### 安装

```bash
npm install fetch-sdk
# 或
yarn add fetch-sdk
```

### 基础使用

```javascript
import fetchClient from 'fetch-sdk';

// 方式1: 直接调用 (类似 axios)
const users = await fetchClient({
    url: '/users',
    method: 'GET'
});

// 方式2: 创建实例（推荐方式）
const service = fetchClient.create({
    baseURL: 'https://api.example.com',     // 可选: API的基础URL
    timeout: 5000,                          // 可选: 请求超时时间(毫秒)
    headers: {                              // 可选: 默认请求头
        'Content-Type': 'application/json'  // 默认值
    },
    withCredentials: true,                  // 可选: 跨域请求是否带凭证
    timeout: 5000                           // 可选: 请求超时时间(毫秒)
});

// 方式3: 通过实例直接调用 (类似 axios)
const users = await service({
    url: '/users',
    method: 'GET'
});

// 使用实例
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(config) | config: RequestConfig | 通用请求方法，接受完整的配置对象 | `service.request({ url: '/api', method: 'POST', data: { name: 'John' } })` |
| request(url[, config]) | url: string, config?: RequestConfig | 通用请求方法 | `service.request('/api', { method: 'PATCH' })` |
| instance(config) | config: RequestConfig | 实例直接调用，接受完整的配置对象 (类似 axios) | `service({ url: '/api', method: 'POST', data: { name: 'John' } })` |

```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);
};

// 使用 request 方法传入配置对象 (类似 axios)
const requestExample = async () => {
    // 1. GET 请求
    const users = await service.request({
        url: '/users',
        method: 'GET',
        params: { page: 1 }
    });

    // 2. POST 请求
    const newUser = await service.request({
        url: '/users',
        method: 'POST',
        data: { name: 'John', age: 30 }
    });

    // 3. PUT 请求
    const updatedUser = await service.request({
        url: '/users/1',
        method: 'PUT',
        data: { name: 'Updated Name' }
    });

    // 4. DELETE 请求
    await service.request({
        url: '/users/1',
        method: 'DELETE'
    });
};
```

### 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 + 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');
        // 发送部分内容...
    }
});
```



### 跨域认证配置

跨域请求时的认证处理配置：

| 配置项 | 类型 | 默认值 | 说明 |
|-------|------|-------|------|
| 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)}%`);
    }
};
```