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

@sop-cli/request

v0.0.5

Published

```bash pnpm add @sop-cli/request ```

Readme

Quick Start 快速开始

pnpm add @sop-cli/request

架构与配置合并

设计原则

| 原则 | 说明 | |------|------| | 多实例隔离 | 每个 createHttpClient() 独立 Axios、拦截器、缓存、租户、运行时 | | 无全局污染 | 无进程级 RequestGlobals;配置均在 ClientContext 实例内 | | 配置分层 | 默认 < 实例 < 单次请求,见下表 | | 无 UI 耦合 | message / loading / 401 均由业务注入回调 |

配置合并策略

DEFAULT_APP_META + DEFAULT_AXIOS_CONFIG     ← 库级默认(config/config.ts)
        ↓ merge
HttpClientOptions(createHttpClient)        ← 实例级:baseURL、headers、runtime、tenantId
        ↓ merge(拦截器 + invoke)
RequestConfig(单次 get/post/...)           ← 请求级:showLoading、retryCount、cache 等
  • Axios 配置baseURLtimeoutheaderswithCredentialsresponseType):实例创建时 mergeAxiosConfig 深度合并 headers
  • AppRequestMetaretryCountshowErrorcache 等):mergeRequestConfig(instanceMeta, requestConfig),请求级覆盖实例级
  • 运行时getTokenmessageHandleron401):仅存于 ClientContext,实例 configureRuntime() 配置
  • 租户tenantId / getTenantId → 请求头 X-Tenant-Id(可配置)

默认 Axios 配置

| 项 | 默认值 | |----|--------| | responseType | json | | withCredentials | true | | timeout | 30s(normal 分级) | | Content-Type | application/json |

导出常量:DEFAULT_APP_METADEFAULT_AXIOS_CONFIGDEFAULT_CONFIG(合集)


5 分钟上手

0. 配置默认 request(main.tsx 最先执行)

request 为懒加载实例,首次使用前可透传全部 Axios / 实例参数:

import { configureDefaultHttpClient, request } from '@sop-cli/request';

configureDefaultHttpClient({
  baseURL: import.meta.env.VITE_API_BASE_URL,
  timeout: 30_000,
  withCredentials: true,
  responseType: 'json',
  headers: { 'X-App-Id': 'my-app' },
  // 实例级 AppRequestMeta
  showLoading: true,
  retryCount: 2,
  // 实例级 runtime / 租户
  runtime: { getToken: () => localStorage.getItem('token') },
  tenantId: 'tenant-001',
});

也可在首次调用 getDefaultHttpClient(options) 时传入(二者合并,仅初始化一次)。


1. 实例配置(推荐在 main.tsx)

所有配置均通过 HttpClient 实例方法 完成。使用库预设的 request 即可:

import { request } from '@sop-cli/request';
import { message } from 'antd';
import * as Sentry from '@sentry/react';

// Loading
request.setLoadingManager({
  start: () => message.loading('加载中...', 0),
  done: () => message.destroy(),
});

// 错误上报
request.setErrorReport({
  report: (error, config) => {
    console.error('请求错误:', { error, config });
    Sentry.captureException(error, {
      extra: { requestConfig: config },
    });
  },
});

2. 运行时与错误处理(推荐在 main.tsx)

import { request } from '@sop-cli/request';
import { APP_NAME, TOKEN } from '@/constant';
import { removeToken } from '@/utils/auth';

request.configureRuntime({
  getToken: () => localStorage.getItem(TOKEN),
  // Cookie 认证(withCredentials: true)时配置 CSRF
  getCsrfToken: () => {
    const match = document.cookie.match(/(?:^|;\s*)XSRF-TOKEN=([^;]*)/);
    return match ? decodeURIComponent(match[1]) : null;
  },
  csrfHeaderKey: 'X-XSRF-TOKEN', // Spring 默认;Django 常用 X-CSRFToken
  messageHandler: (type, content, ctx) => {
    // ctx.responseData 含后端完整响应体,如 { code, success, message }
    console[type === 'error' ? 'error' : 'log'](content, ctx.responseData);
  },
  on401: () => {
    removeToken();
    window.location.assign(`/${APP_NAME}/login`);
  },
});

request.setupErrorHandlers();

3. 发起请求

import { request } from '@sop-cli/request';

interface User {
  id: number;
  name: string;
  email: string;
}

// GET 请求
const user = await request.get<User>('/api/users/1');

// POST 请求
const newUser = await request.post<User>('/api/users', {
  name: '张三',
  email: '[email protected]',
});

// PUT 请求
const updatedUser = await request.put<User>('/api/users/1', {
  name: '李四',
});

// DELETE 请求
await request.del('/api/users/1');

4. 错误处理

import {
  isBusinessError,
  isNetworkError,
  isCancelError,
} from '@sop-cli/request';

try {
  const data = await request.get('/api/data');
} catch (error) {
  if (isCancelError(error)) {
    console.log('请求被取消');
    return;
  }

  if (isBusinessError(error)) {
    console.log('业务错误:', error.code, error.message);
    // 处理特定业务错误
    if (error.code === 'USER_NOT_FOUND') {
      // 跳转到登录
    }
    return;
  }

  if (isNetworkError(error)) {
    if (error.networkType === 'timeout') {
      console.log('请求超时,请重试');
    } else {
      console.log('网络连接失败,请检查网络');
    }
    return;
  }

  console.log('未知错误:', error);
}

5. 高级配置

// 开启全局 Loading(默认关闭)
await request.get('/api/data', undefined, {
  showLoading: true,
});

// 关闭错误提示
await request.get('/api/data', undefined, {
  showError: false,
});

// 跳过 Token(用于登录等公开接口,默认 skipToken: false)
await request.post('/api/login', credentials, {
  skipToken: true,
});

// 禁用重复请求取消
await request.post('/api/submit', data, {
  autoCancel: false,
});

// 自定义重试次数
await request.get('/api/unstable', undefined, {
  retryCount: 3,
});

// 跳过全局错误处理
try {
  await request.get('/api/data', undefined, {
    skipErrorHandler: true,
  });
} catch (error) {
  // 自行处理
}

6. 文件上传

import { request, type UploadProgressCallback } from '@sop-cli/request';

// 6.1 简单上传(FormData)
const formData = new FormData();
formData.append('file', file);
formData.append('name', filename);

const result = await request.upload('/api/upload', formData);

// 6.2 上传进度回调
const onProgress: UploadProgressCallback = (progressEvent) => {
  if (progressEvent.total) {
    const percent = Math.round((progressEvent.loaded * 100) / progressEvent.total);
    console.log(`上传进度: ${percent}%`);
  }
};

await request.upload('/api/upload', formData, {
  onUploadProgress: onProgress,
});

// 6.3 自定义上传超时(默认 10 分钟)
await request.upload('/api/upload', formData, {
  timeout: 20 * 60 * 1000, // 20 分钟
});

7. 多实例隔离

每个 createHttpClient() 创建完全独立的实例:Axios、缓存、取消队列、运行时配置(token / messageHandler / on401)、Loading、错误上报、租户上下文 互不干扰。适合多域名、多账号、多租户并行场景。

库导出预设默认实例 request业务层也可自行维护单例

import { createHttpClient, type HttpClient } from '@sop-cli/request';

let appClient: HttpClient | undefined;

/** 业务侧自管单例 */
export function getAppClient() {
  if (!appClient) {
    appClient = createHttpClient({
      baseURL: import.meta.env.VITE_API_BASE_URL,
      runtime: {
        getToken: () => localStorage.getItem('token'),
        messageHandler: (type, msg) => console[type === 'error' ? 'error' : 'log'](msg),
        on401: () => { /* 业务自行处理 */ },
      },
    });
    appClient.setupErrorHandlers();
  }
  return appClient;
}

多域名 + 多账号完全隔离(互不影响 token、Loading、错误处理):

import { createHttpClient } from '@sop-cli/request';

const accountA = createHttpClient({
  baseURL: 'https://api-a.example.com',
  runtime: {
    getToken: () => getTokenFor('account-a'),
    messageHandler: toastA.show,
    on401: () => logout('account-a'),
  },
});
accountA.setLoadingManager(loadingA);
accountA.setErrorReport({ report: sentryA.captureException });

const accountB = createHttpClient({
  baseURL: 'https://api-b.example.com',
  runtime: {
    getToken: () => getTokenFor('account-b'),
    resolveBaseURL: (cfg) => `https://api-b.example.com/${cfg.version ?? 'v1'}`,
  },
});

await accountA.get('/users');
await accountB.get('/users');

多实例场景使用 createHttpClient(),每个实例独立调用 configureRuntime()setLoadingManager() 等方法。

8. 多租户 tenantId 注入

创建实例时可传入 tenantId,请求拦截器会自动在请求头携带租户标识(默认头名 X-Tenant-Id)。

import { createHttpClient } from '@sop-cli/request';

const client = createHttpClient({
  baseURL: '/api',
  tenantId: 'tenant-001',
  runtime: {
    getToken: () => localStorage.getItem('token'),
  },
});

// 自动携带: X-Tenant-Id: tenant-001
await client.get('/users');

动态切换租户(用户切换组织/租户后):

// 方式 1:直接设置
client.setTenantId('tenant-002');

// 方式 2:动态解析(优先级高于 tenantId)
client.setTenantOptions({
  getTenantId: () => store.getState().currentTenantId,
});

// 方式 3:自定义请求头名称
client.setTenantOptions({
  tenantHeaderKey: 'X-Org-Id',
  tenantId: 'org-123',
});

单次请求跳过租户头(公开接口等):

await client.get('/public/config', undefined, { skipTenant: true });

多租户 + 多实例(每个租户独立 client,完全隔离):

import { createHttpClient, type HttpClient } from '@sop-cli/request';

const tenantClients = new Map<string, HttpClient>();

function getTenantClient(tenantId: string) {
  if (!tenantClients.has(tenantId)) {
    const client = createHttpClient({
      baseURL: import.meta.env.VITE_API_BASE_URL,
      tenantId,
      runtime: {
        getToken: () => getTokenForTenant(tenantId),
        on401: () => switchTenant(null),
      },
    });
    client.setupErrorHandlers();
    tenantClients.set(tenantId, client);
  }
  return tenantClients.get(tenantId)!;
}

await getTenantClient('tenant-a').get('/orders');
await getTenantClient('tenant-b').get('/orders');

9. 业务模块 API 组织

// src/domains/user/api.ts
import { request } from '@sop-cli/request';

const USER_BASE = '/api/users';

export const userApi = {
  getList: (params?: { page?: number; size?: number }) =>
    request.get(`${USER_BASE}`, params),

  getDetail: (id: number) =>
    request.get(`${USER_BASE}/${id}`),

  create: (data: CreateUserParams) =>
    request.post(USER_BASE, data),

  update: (id: number, data: UpdateUserParams) =>
    request.put(`${USER_BASE}/${id}`, data),

  delete: (id: number) =>
    request.del(`${USER_BASE}/${id}`),
};

// 使用
import { userApi } from '@/domains/user/api';

const users = await userApi.getList({ page: 1, size: 10 });
const user = await userApi.getDetail(1);

10. React Query 集成

import { useQuery, useMutation } from '@tanstack/react-query';
import { userApi } from '@/domains/user/api';

function UserList() {
  const { data, isLoading, error } = useQuery({
    queryKey: ['users'],
    queryFn: () => userApi.getList(),
  });

  return <div>{/* 渲染用户列表 */}</div>;
}

function CreateUserForm() {
  const mutation = useMutation({
    mutationFn: userApi.create,
    onSuccess: () => {
      // 刷新列表
    },
  });

  return <form onSubmit={(e) => mutation.mutate(formData)}>{/* 表单内容 */}</form>;
}

11. 取消请求

场景:组件卸载时

import { useEffect, useState } from 'react';
import { request } from '@sop-cli/request';

const CancelRequestExample = () => {
  const [data, setData] = useState(null);

  useEffect(() => {
    // 创建临时 AbortController
    const controller = new AbortController();

    const fetchData = async () => {
      try {
        const res = await request.get('/long-task', undefined, {
          signal: controller.signal,
        });
        setData(res);
      } catch (err) {
        if (err.name === 'CanceledError') {
          console.log('请求已取消:', err);
        }
      }
    };

    fetchData();

    // 组件卸载时取消请求
    return () => {
      controller.abort();
    };
  }, []);

  return <div>{data ? JSON.stringify(data) : '加载中...'}</div>;
};

后端 API 格式

后端返回数据格式(推荐规范):

export interface ApiResponse<T = unknown> {
  code?: number | string;
  success: boolean; // if request is success
  data: T; // response data
  status: number | string;
  name?: string;
  message?: string;
  error?: string;
  traceId?: string; // unique request ID
  host?: string; // Convenient for backend Troubleshooting: host of current access server
}

如果格式不一致,可以:

  1. 通过 client.addResponseInterceptor() 添加自定义响应拦截器进行适配
  2. 单个请求配置 skipErrorHandler: true 跳过默认错误处理
  3. 实例级 client.setupErrorHandlers() 覆盖错误码处理

API 参考

实例创建

| 方法 | 说明 | |------|------| | createHttpClient(options?) | 创建独立 HTTP 客户端(推荐多实例场景) | | request | 库预设默认实例(多数业务可直接使用) | | getDefaultHttpClient(config?) | 获取预设默认实例(懒初始化) |

HttpClient 实例方法

| 方法 | 说明 | |------|------| | configureRuntime(config) | 配置 token、CSRF、messageHandler、on401、resolveBaseURL | | setTenantId(id) | 动态设置租户 ID | | setTenantOptions(options) | 配置 tenantId / tenantHeaderKey / getTenantId | | resolveTenantId() | 获取当前生效的租户 ID | | setLoadingManager(manager) | 实例级 Loading | | setErrorReport(reporter) | 实例级错误上报 | | setupErrorHandlers(options?) | 注册 401 等错误码处理 | | addRequestInterceptor(...) | 添加自定义请求拦截器 | | addResponseInterceptor(...) | 添加自定义响应拦截器 | | cancelByGroup(group) | 按 group 批量取消 | | destroy() | 销毁实例,释放拦截器与 pending 请求 |

HTTP 方法

| 方法 | 说明 | |------|------| | request.get<T>(url, params?, config?) | GET 请求 | | request.post<T>(url, data?, config?) | POST 请求 | | request.put<T>(url, data?, config?) | PUT 请求 | | request.patch<T>(url, data?, config?) | PATCH 请求 | | request.del<T>(url, params?, config?) | DELETE 请求 | | request.upload<T>(url, formData, config?) | 文件上传 | | request.request<T>(config) | 通用请求 |

HttpClientOptions(createHttpClient 专用)

| 选项 | 类型 | 说明 | |------|------|------| | runtime | RequestRuntimeOptions | 实例级运行时(token、messageHandler、on401、resolveBaseURL) | | runtime.getCsrfToken | () => string \| null | POST/PUT/PATCH/DELETE 自动注入 CSRF 头 | | runtime.csrfHeaderKey | string | CSRF 请求头名,默认 X-CSRF-Token | | tenantId | string \| null | 租户 ID,自动注入请求头 | | tenantHeaderKey | string | 租户请求头名,默认 X-Tenant-Id | | getTenantId | () => string \| null | 动态解析租户 ID(优先于 tenantId) |

配置选项 (RequestConfig)

| 选项 | 类型 | 默认值 | 说明 | |------|------|--------|------| | showLoading | boolean | false | 是否显示全局 Loading | | showError | boolean | true | 是否显示错误提示 | | autoCancel | boolean | true | 是否自动取消重复请求 | | skipToken | boolean | false | 是否跳过 Token 注入 | | skipCsrf | boolean | false | 是否跳过 CSRF Token 注入 | | skipTenant | boolean | false | 是否跳过租户请求头注入 | | retryCount | number | 1 | 重试次数 | | skipErrorHandler | boolean | false | 是否跳过全局错误处理 | | timeout | number | 30000 | 请求超时(ms);也可用 timeoutTier | | timeoutTier | 'fast' \| 'normal' \| 'slow' \| 'upload' | 'normal' | 超时分级 | | unwrapData | boolean | false | 是否只返回 data 字段 | | cache | boolean | object | — | GET 响应缓存 |

错误类型

| 类型 | 守卫函数 | 说明 | |------|----------|------| | ApiError | isApiError() | HTTP 状态码错误 | | BusinessError | isBusinessError() | 业务逻辑错误 | | NetworkError | isNetworkError() | 网络错误 | | CancelError | isCancelError() | 请求取消错误 |


企业级能力清单

  • 多实例 + 多租户(tenantId / X-Tenant-Id
  • Token / CSRF 自动注入(可 skipToken / skipCsrf
  • 请求取消 + 防重复(AbortController)
  • 失败重试(指数退避,5xx/408/网络错误)
  • GET 缓存 / 防抖 / 节流
  • 错误分类:ApiError / BusinessError / NetworkError / CancelError
  • 泛型 REST 方法 + ErrorMessageContext(含 response.data
  • 自定义拦截器(内置拦截器不可移除)