npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

fetch-sdk

v1.1.2

Published

A simple and lightweight fetch wrapper SDK

Readme

Fetch SDK

一个基于 Fetch API 的现代化 HTTP 客户端,提供简单易用的接口封装,支持请求拦截、流式数据和文件处理。

特性

  • ✨ 优雅的 API 设计,类似 axios 的使用体验
  • 🚀 支持请求和响应拦截器
  • 📦 自动 JSON 数据处理
  • 🔄 支持请求取消
  • 📊 支持上传/下载进度监控
  • 📥 支持流式数据处理
  • 🛡️ 完善的 TypeScript 支持
  • 🔌 支持多实例创建

快速开始

安装

npm install fetch-sdk
# 或
yarn add fetch-sdk

基础使用

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 |

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' } }) |

// 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. 拦截器

拦截器配置及使用:

// 请求拦截器
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 基础文件处理

// 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 提供了文件上传和下载的断点续传功能,支持大文件传输时的断点恢复。

断点续传上传方法参数
/**
 * 断点续传文件上传
 * @param {File|Blob} file - 要上传的文件对象 (必需)
 * @param {string} url - 上传地址 (必需)
 * @param {Object} options - 配置选项 (可选)
 * @returns {Promise<Object>} - 上传结果
 */
uploadWithResume(file, url, options = {})
上传断点续传示例
// 断点续传上传示例
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);
        // 稍后可以使用相同的参数重新调用来继续上传
    }
};
断点续传下载方法参数
/**
 * 普通文件下载方法
 * @param {string} url - 下载地址 (必需)
 * @param {Object} options - 配置选项 (可选)
 * @param {string} options.filename - 下载保存的文件名 (可选,提供此参数将自动触发浏览器下载)
 * @param {Function} options.onProgress - 下载进度回调 (可选)
 * @returns {Promise<Blob>} - 下载的文件Blob对象
 */
download(url, options = {})
下载断点续传示例
// 断点续传下载示例
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. 分片上传接口:
// 服务端接收分片示例(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'];
    // 处理分片...
});
  1. 断点下载支持:
// 服务端支持断点下载示例
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' |

// 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. 使用场景:

    • 跨域登录认证
    • 需要访问用户会话数据
    • 多服务之间的认证信息共享
// 完整配置示例
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 使用标准化的错误对象,包含以下属性:

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();

实际应用场景

  1. 搜索场景下取消上一次请求:
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;
        }
    }
};
  1. 组件卸载时取消请求:
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>;
}
  1. 超时和取消的结合:
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
});
  1. 通用配置集中管理
// 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;
  1. 业务模块封装
// 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);
    }
};
  1. 请求取消处理
// 搜索场景
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. 请求重试机制

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;
};

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, {
            // 注意:使用 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)}%`);
    }
};