1.0.4 • Published 4 months ago
fetch-sdk v1.0.4
Fetch SDK
一个基于 Fetch API 的现代化 HTTP 客户端,提供简单易用的接口封装,支持请求拦截、流式数据和文件处理。
特性
- ✨ 优雅的 API 设计,类似 axios 的使用体验
- 🚀 支持请求和响应拦截器
- 📦 自动 JSON 数据处理
- 🔄 支持请求取消
- 📊 支持上传/下载进度监控
- 📥 支持流式数据处理
- 🛡️ 完善的 TypeScript 支持
- 🔌 支持多实例创建
快速开始
安装
npm install fetch-sdk
# 或
yarn add fetch-sdk
基础使用
import fetchClient from 'fetch-sdk';
// 创建实例(推荐方式)
const service = fetchClient.create({
baseURL: 'https://api.example.com',
timeout: 5000,
headers: {
'Content-Type': 'application/json',
'Accept': 'text/event-stream' // SSE 支持
},
withCredentials: true,
validateStatus: (status) => status >= 200 && status < 500,
stream: true // 启用流式处理
});
// 使用实例
try {
const data = await service.get('/users', {
params: { page: 1 },
headers: { 'X-Token': 'xxx' }
});
console.log('请求成功:', data);
} catch (error) {
console.error('请求失败:', error);
}
详细功能
1. 实例配置
创建实例时可配置的选项:
配置项 | 类型 | 默认值 | 说明 | 示例 |
---|---|---|---|---|
baseURL | string | '' | 请求的基础URL | 'https://api.example.com' |
timeout | number | - | 请求超时时间(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' |
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' }) |
// 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. 拦截器
拦截器配置及使用:
// 请求拦截器
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. 文件处理
文件上传和下载功能:
4.1 基础文件处理
// 1. 文件上传
const uploadFile = async (file) => {
const formData = new FormData();
formData.append('file', file);
try {
await service.post('/upload', formData, {
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 提供了文件上传和下载的断点续传功能,支持大文件传输时的断点恢复。
上传断点续传
// 断点续传上传示例
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);
// 稍后可以使用相同的参数重新调用来继续上传
}
};
下载断点续传
// 断点续传下载示例
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);
// 稍后可以使用相同的参数重新调用来继续下载
}
};
特性说明
断点续传功能特点:
自动分片:
- 默认分片大小为 1MB
- 可通过配置自定义分片大小
- 支持超大文件传输
进度保存:
- 自动保存传输进度到 localStorage
- 断点信息持久化
- 支持页面刷新后继续传输
错误处理:
- 网络错误自动保存断点
- 支持手动暂停/继续
- 提供详细的错误信息
进度监控:
- 实时进度回调
- 提供已传输大小和总大小信息
- 支持进度百分比计算
配置选项
选项 | 类型 | 默认值 | 说明 |
---|---|---|---|
chunkSize | number | 1024 * 1024 | 分片大小(字节) |
onProgress | function | - | 进度回调函数 |
mimeType | string | 'application/octet-stream' | 文件类型 |
filename | string | - | 保存的文件名 |
服务端配置要求
服务端需要支持以下功能:
- 分片上传接口:
// 服务端接收分片示例(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'];
// 处理分片...
});
- 断点下载支持:
// 服务端支持断点下载示例
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 数据,需要手动解析。
const handleEventStream = async () => {
const stream = await service.request('/events', {
stream: true,
headers: { 'Accept': 'text/event-stream' },
// responseType: 'json', // 注意:此设置在流式处理时不生效
method: 'POST',
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',
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' |
// 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
才会被浏览器接受并存储
credentials 可选值说明:
'omit'
: 从不发送 cookies'same-origin'
: 只有当请求同源时才发送 cookies(默认值)'include'
: 总是发送 cookies,等同于withCredentials: true
安全考虑:
- 启用此配置会增加 CSRF 攻击风险,建议同时实现 CSRF 令牌机制
- 仅在确实需要跨域认证时才启用此配置
- 建议配合 HTTPS 使用,确保数据传输安全
使用场景:
- 跨域登录认证
- 需要访问用户会话数据
- 多服务之间的认证信息共享
// 完整配置示例
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 请求 | service.get('/users', { params: { page: 1 } }) |
post | (url, data?, options?) | POST 请求 | service.post('/users', { name: 'John' }) |
request | (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 配置用于处理流式响应数据,主要用于以下场景:
- Server-Sent Events (SSE) 接收实时更新
- 大文件下载时分块处理
- 流式 API 响应(如 ChatGPT 流式响应)
使用示例:
// 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 | 读取下一个数据块,返回 null 表示数据已读取完毕 |
iterateLines | async iterator | 按行迭代数据,适合处理文本流和 SSE |
total | number | 总数据大小(仅当服务器提供 content-length 时可用) |
loaded | number | 当前已接收的数据大小 |
reader | ReadableStreamDefaultReader | 底层读取器实例,用于手动控制和资源清理 |
注意事项:
使用
read()
方法时:- 返回 null 表示数据流结束
- 返回的是解码后的文本数据
- 自动更新 loaded 属性和触发进度回调
使用
iterateLines()
时:- 自动处理行分割
- 适合处理 SSE 和文本流
- 会自动处理编码
资源管理:
- 务必在 finally 中调用 reader.cancel()
- 避免资源泄露
- 支持提前中断数据流
错误处理
SDK 使用标准化的错误对象,包含以下属性:
try {
await service.get('/api');
} catch (error) {
// error.config - 请求配置信息
// error.request - 请求实例
// error.response - 响应对象(如果存在)
// error.status - HTTP状态码(如果存在)
// error.statusText - 状态描述(如果存在)
console.log(error.message); // 错误消息
}
请求取消
使用标准的 AbortController 来取消请求:
const controller = new AbortController();
service.get('/api/data', {
signal: controller.signal
}).catch(error => {
if (error.name === 'AbortError') {
console.log('请求已被取消');
}
});
// 取消请求
controller.abort();
实际应用场景
- 搜索场景下取消上一次请求:
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;
}
}
};
- 组件卸载时取消请求:
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>;
}
- 超时和取消的结合:
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;
}
};
最佳实践
请求取消示例
// 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
});
- 通用配置集中管理
// 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;
- 业务模块封装
// 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);
}
};
- 请求取消处理
// 搜索场景
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;
}
}
};
- 流式数据优雅处理
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 详细说明
作用说明:
- 告知服务器客户端期望接收 Server-Sent Events (SSE) 格式的数据流
- 服务器根据此头部判断是否使用 SSE 协议发送数据
- 建立长连接,保持数据流的持续推送
是否必需:
- 不是强制必需的,但强烈建议设置
- 不设置可能导致的问题:
- 服务器可能返回普通 HTTP 响应而不是事件流
- 某些服务器会拒绝不带正确 Accept 头的 SSE 请求
- 无法享受浏览器对 SSE 的原生优化(如自动重连)
使用场景:
// 推荐的完整配置
const service = fetchClient.create({
headers: {
'Accept': 'text/event-stream', // 声明接收 SSE
'Cache-Control': 'no-cache', // 禁用缓存
'Connection': 'keep-alive' // 保持连接
}
});
// 最小配置(不推荐)
const service = fetchClient.create({
// 不设置 Accept 头,依赖服务器默认行为
});
- 服务器端对应配置:
// 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' });
}
});
调试技巧:
- 使用浏览器开发者工具查看 Network 面板
- 确认请求头中包含
Accept: text/event-stream
- 检查响应头中的
Content-Type: text/event-stream
- 观察连接是否保持打开状态
常见问题处理:
// 处理连接断开 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('多次重试失败,放弃连接'); } } };
性能考虑:
- SSE 连接会占用服务器资源,建议设置最大连接数
- 考虑在不需要时及时关闭连接
- 可以使用心跳机制检测连接状态
// 心跳检测示例 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:
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 带缓冲的数据处理:
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 带超时和重试的事件流处理:
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. 请求重试机制
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. 批量请求处理
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. 智能缓存
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 降级方案
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. 文件上传断点续传
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, {
headers: {
'X-Upload-Id': uploadId,
'X-Chunk-Index': i,
'X-Total-Chunks': chunks
}
});
uploaded += chunk.size;
console.log(`上传进度: ${Math.round((uploaded / file.size) * 100)}%`);
}
};