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

@djvlc/openapi-client-core

v1.2.14

Published

DJV Low-code Platform - OpenAPI Client 公共运行时

Readme

@djvlc/openapi-client-core

OpenAPI Client 公共运行时库,为 @djvlc/openapi-*-client 系列包提供核心功能。

特性

  • 🔌 模块化架构:按功能拆分,每个模块职责单一
  • 🔗 拦截器系统:请求/响应/错误拦截器,支持顺序控制
  • 🔐 多种认证:Bearer/API Key/Basic/自定义认证
  • 🔄 智能重试:支持 Retry-After、指数退避、自定义策略
  • 🎯 请求去重:避免相同请求并发执行
  • 📊 指标收集:请求级别的性能指标和统计
  • 📝 日志系统:可配置的请求日志
  • 🛡️ 类型安全:完整的 TypeScript 类型定义

安装

pnpm add @djvlc/openapi-client-core

模块结构

src/
├── types/           # 类型定义
├── errors/          # 错误处理
├── auth/            # 认证模块
├── interceptors/    # 拦截器
│   ├── request/     # 请求拦截器
│   ├── response/    # 响应拦截器
│   └── error/       # 错误拦截器
├── plugins/         # 插件
├── clients/         # HTTP 客户端
├── utils/           # 工具函数
└── index.ts         # 主入口

快速开始

使用 Fetch 客户端

import { createFetchClient } from '@djvlc/openapi-client-core';

const client = createFetchClient({
  baseUrl: 'https://api.example.com',
  timeout: 30000,
  auth: {
    type: 'bearer',
    getToken: () => localStorage.getItem('token'),
  },
});

// 发起请求
const data = await client.get<User>('/users/me');

使用 Axios 客户端(推荐用于 openapi-generator)

import { createAxiosInstance } from '@djvlc/openapi-client-core';
import { PagesApi, Configuration } from './generated';

const axiosInstance = createAxiosInstance({
  baseUrl: '/api/admin',
  auth: { 
    type: 'bearer', 
    getToken: () => localStorage.getItem('token'),
  },
  retry: { 
    maxRetries: 2, 
    initialDelayMs: 1000, 
    maxDelayMs: 10000, 
    backoff: 'exponential',
  },
});

const config = new Configuration();
const pagesApi = new PagesApi(config, undefined, axiosInstance);

HTTP 客户端详解

本库提供两种 HTTP 客户端实现,共享相同的配置接口,功能完全对等:

客户端对比

| 特性 | FetchClient | Axios Client | |------|-------------|--------------| | 依赖 | 无(原生 fetch) | 需要 axios | | 使用场景 | 独立使用、轻量场景 | OpenAPI 生成代码集成 | | Bundle 大小 | 更小 | 需包含 axios | | 请求拦截器 | ✅ 支持 | ✅ 支持 | | 响应拦截器 | ✅ 支持 | ✅ 支持 | | 错误拦截器 | ✅ 支持 | ✅ 支持 | | 认证 | ✅ 全类型 | ✅ 全类型 | | 自动重试 | ✅ 支持 | ✅ 支持 | | 超时控制 | ✅ AbortController | ✅ 原生 timeout | | 请求取消 | ✅ AbortSignal | ✅ CancelToken | | 调试日志 | ✅ 支持 | ✅ 支持 | | Retry-After | ✅ 尊重 | ✅ 尊重 |

如何选择?

┌─────────────────────────────────────────────────────────────┐
│                     你的使用场景是什么?                      │
└─────────────────────────────────────────────────────────────┘
                              │
              ┌───────────────┴───────────────┐
              ▼                               ▼
    使用 OpenAPI 生成的代码?           独立发起 HTTP 请求?
              │                               │
              ▼                               ▼
    ┌─────────────────┐             ┌─────────────────┐
    │  Axios Client   │             │  FetchClient    │
    │  (推荐)          │             │  (推荐)          │
    └─────────────────┘             └─────────────────┘

createEnhancedFetch(推荐用于 OpenAPI 生成代码)

createEnhancedFetch 创建一个增强的 fetch 函数,专为与 typescript-fetch 生成的 API 类集成设计:

import { createEnhancedFetch, createConsoleLogger } from '@djvlc/openapi-client-core';
import { PagesApi, Configuration } from '@djvlc/openapi-user-client';

// 创建增强的 fetch 函数
const enhancedFetch = createEnhancedFetch({
  baseUrl: '/api/admin',
  timeout: 30000,
  
  // 认证
  auth: {
    type: 'bearer',
    getToken: () => localStorage.getItem('token') ?? '',
  },
  
  // 重试
  retry: {
    maxRetries: 3,
    backoffStrategy: 'exponential',
  },
  
  // 日志
  logger: createConsoleLogger({ prefix: '[API]' }),
  debug: true,
});

// 与生成的 API 类集成
const config = new Configuration({ basePath: '/api/admin' });
const pagesApi = new PagesApi(config, '/api/admin', enhancedFetch);

// 使用
const pages = await pagesApi.listPages();

增强功能包括:

  • ✅ 自动认证(Bearer/API Key/Basic/自定义)
  • ✅ 智能重试(指数退避 + Retry-After)
  • ✅ 超时控制(AbortController)
  • ✅ 请求追踪(X-Request-ID, X-Trace-ID)
  • ✅ 错误转换(ApiError, NetworkError, TimeoutError)
  • ✅ 调试日志

FetchClient 完整配置

FetchClient 基于原生 fetch API,适合独立使用(不依赖 OpenAPI 生成代码):

import { 
  createFetchClient,
  createConsoleLogger,
  type FetchClientConfig,
  type RequestInterceptor,
  type ResponseInterceptor,
  type ErrorInterceptor,
} from '@djvlc/openapi-client-core';

const config: FetchClientConfig = {
  // ============ 基础配置 ============
  
  /** 必填:API 基础 URL */
  baseUrl: 'https://api.example.com',
  
  /** 请求超时时间(毫秒),默认 30000 */
  timeout: 30000,
  
  /** 默认请求头 */
  headers: {
    'X-Custom-Header': 'value',
  },
  
  // ============ 认证配置 ============
  
  /** 认证配置 */
  auth: {
    type: 'bearer',
    getToken: async () => await tokenService.getAccessToken(),
    // 其他选项见 "认证" 章节
  },
  
  // ============ 重试配置 ============
  
  /** 是否启用自动重试,默认 true */
  enableRetry: true,
  
  /** 重试策略 */
  retry: {
    maxRetries: 3,
    initialDelayMs: 1000,
    maxDelayMs: 30000,
    backoffStrategy: 'exponential',
    retryableStatusCodes: [429, 500, 502, 503, 504],
    retryOnNetworkError: true,
    retryOnTimeout: true,
    respectRetryAfter: true,
    jitterFactor: 0.1,
    onRetry: (info) => {
      console.log(`重试 ${info.attempt}/${info.maxRetries}`);
    },
  },
  
  // ============ 拦截器 ============
  
  /** 请求拦截器列表 */
  requestInterceptors: [
    {
      name: 'custom-header',
      order: 0,
      intercept(context) {
        context.options.headers['X-Request-Time'] = Date.now().toString();
        return context.options;
      },
    },
  ],
  
  /** 响应拦截器列表 */
  responseInterceptors: [
    {
      name: 'response-logger',
      order: 0,
      intercept(response, context) {
        console.log(`${context.options.method} ${context.options.path} -> ${response.status}`);
        return response;
      },
    },
  ],
  
  /** 错误拦截器列表 */
  errorInterceptors: [
    {
      name: 'error-logger',
      order: 0,
      intercept(error, context) {
        console.error(`请求失败: ${error.message}`);
        return undefined; // 不处理,继续传播
      },
    },
  ],
  
  // ============ 调试配置 ============
  
  /** 日志器 */
  logger: createConsoleLogger({ prefix: '[API]', level: 'debug' }),
  
  /** 是否启用调试日志,默认 false */
  debug: true,
};

const client = createFetchClient(config);

// 使用客户端
const user = await client.get<User>('/users/me');
const created = await client.post<User>('/users', { name: 'Alice' });
const updated = await client.put<User>('/users/1', { name: 'Bob' });
const patched = await client.patch<User>('/users/1', { name: 'Charlie' });
await client.delete('/users/1');

Axios Client 完整配置

createAxiosInstance 返回一个配置好的 Axios 实例,适合与 OpenAPI 生成代码集成:

import { 
  createAxiosInstance,
  createConsoleLogger,
  type AxiosClientConfig,
} from '@djvlc/openapi-client-core';
import { PagesApi, ComponentsApi, Configuration } from '@djvlc/openapi-user-client';

const config: AxiosClientConfig = {
  // ============ 基础配置 ============
  
  /** 必填:API 基础 URL */
  baseUrl: '/api/admin',
  
  /** 请求超时时间(毫秒),默认 30000 */
  timeout: 30000,
  
  /** 默认请求头 */
  headers: {
    'X-Client-Version': '1.0.0',
  },
  
  // ============ 认证配置 ============
  
  auth: {
    type: 'bearer',
    getToken: () => localStorage.getItem('accessToken') ?? '',
  },
  
  // ============ 重试配置 ============
  
  enableRetry: true,
  
  retry: {
    maxRetries: 3,
    initialDelayMs: 1000,
    maxDelayMs: 30000,
    backoffStrategy: 'exponential',
    retryableStatusCodes: [429, 500, 502, 503, 504],
    onRetry: (info) => {
      console.log(`[Retry] ${info.method} ${info.url} - attempt ${info.attempt}`);
    },
  },
  
  // ============ 拦截器(与 FetchClient 相同) ============
  
  requestInterceptors: [...],
  responseInterceptors: [...],
  errorInterceptors: [...],
  
  // ============ 调试配置 ============
  
  logger: createConsoleLogger({ prefix: '[Admin API]' }),
  debug: process.env.NODE_ENV === 'development',
};

// 创建 Axios 实例
const axiosInstance = createAxiosInstance(config);

// 与 OpenAPI 生成代码集成
const apiConfig = new Configuration();
const pagesApi = new PagesApi(apiConfig, undefined, axiosInstance);
const componentsApi = new ComponentsApi(apiConfig, undefined, axiosInstance);

// 使用生成的 API 客户端
const pages = await pagesApi.listPages({ limit: 10 });
const page = await pagesApi.getPage({ id: 'page-123' });

配置选项速查表

| 配置项 | 类型 | 默认值 | 说明 | |--------|------|--------|------| | baseUrl | string | - | 必填,API 基础 URL | | timeout | number | 30000 | 请求超时(毫秒) | | headers | Record<string, string> | {} | 默认请求头 | | auth | AuthConfig | - | 认证配置 | | retry | RetryConfig | - | 重试配置 | | enableRetry | boolean | true | 是否启用重试 | | requestInterceptors | RequestInterceptor[] | [] | 请求拦截器 | | responseInterceptors | ResponseInterceptor[] | [] | 响应拦截器 | | errorInterceptors | ErrorInterceptor[] | [] | 错误拦截器 | | logger | Logger | - | 日志器实例 | | debug | boolean | false | 是否启用调试日志 |

完整示例:生产环境配置

import {
  createAxiosInstance,
  createConsoleLogger,
  createMetricsCollector,
  createMetricsInterceptors,
  createLoggingInterceptor,
  createReportingInterceptor,
  ApiError,
} from '@djvlc/openapi-client-core';

// ============ 创建日志器 ============
const logger = createConsoleLogger({
  prefix: '[DJV-API]',
  level: process.env.NODE_ENV === 'production' ? 'warn' : 'debug',
  timestamp: true,
});

// ============ 创建指标收集器 ============
const metricsCollector = createMetricsCollector({
  maxMetrics: 1000,
  ttlMs: 300000, // 5 分钟
  onMetrics: (m) => {
    // 发送到监控系统
    if (m.durationMs > 3000) {
      logger.warn(`慢请求: ${m.method} ${m.path} - ${m.durationMs}ms`);
    }
  },
});

const metricsInterceptors = createMetricsInterceptors(metricsCollector);

// ============ 创建错误上报拦截器 ============
const errorReporter = createReportingInterceptor({
  reporter: {
    report: (error, context) => {
      // 只上报服务端错误
      if (ApiError.is(error) && error.isServerError()) {
        Sentry.captureException(error, {
          extra: {
            requestId: context.requestId,
            traceId: context.traceId,
            url: context.url,
          },
        });
      }
    },
  },
  sampleRate: 1.0, // 生产环境 100% 采样
});

// ============ 创建客户端 ============
const axiosInstance = createAxiosInstance({
  baseUrl: import.meta.env.VITE_API_BASE_URL,
  timeout: 30000,
  
  auth: {
    type: 'bearer',
    getToken: () => authStore.getAccessToken(),
  },
  
  retry: {
    maxRetries: 3,
    initialDelayMs: 1000,
    maxDelayMs: 30000,
    backoffStrategy: 'exponential',
    jitterFactor: 0.2,
    retryableStatusCodes: [429, 500, 502, 503, 504],
    respectRetryAfter: true,
    onRetry: (info) => {
      logger.info(`重试请求: ${info.method} ${info.url} (${info.attempt}/${info.maxRetries})`);
    },
  },
  
  requestInterceptors: [
    metricsInterceptors.request,
    {
      name: 'app-version',
      order: 100,
      intercept(ctx) {
        ctx.options.headers['X-App-Version'] = APP_VERSION;
        ctx.options.headers['X-Platform'] = 'web';
        return ctx.options;
      },
    },
  ],
  
  responseInterceptors: [
    metricsInterceptors.response,
  ],
  
  errorInterceptors: [
    metricsInterceptors.error,
    errorReporter,
    {
      name: 'auth-redirect',
      order: 1000,
      intercept(error, context) {
        if (ApiError.is(error) && error.isUnauthorized()) {
          // Token 过期,跳转登录
          authStore.logout();
          router.push('/login');
        }
        return undefined;
      },
    },
  ],
  
  logger,
  debug: import.meta.env.DEV,
});

// ============ 定期上报指标 ============
setInterval(() => {
  const summary = metricsCollector.summary();
  analytics.track('api_metrics', {
    totalRequests: summary.totalRequests,
    successRate: summary.successRate,
    p50: summary.p50Ms,
    p95: summary.p95Ms,
    p99: summary.p99Ms,
  });
}, 60000);

export { axiosInstance, metricsCollector };

错误处理

import { 
  ApiError, 
  NetworkError, 
  TimeoutError, 
  AbortError,
  isRetryableError,
} from '@djvlc/openapi-client-core';

try {
  await client.get('/users/me');
} catch (e) {
  if (ApiError.is(e)) {
    // 业务错误(服务器返回的错误响应)
    console.log('业务错误:', e.code, e.message);
    console.log('HTTP 状态码:', e.statusCode);
    console.log('追踪 ID:', e.traceId);
    
    // 状态码判断
    if (e.isAuthError()) {
      // 认证错误 (401/403)
      redirectToLogin();
    } else if (e.isRateLimited()) {
      // 限流错误 (429)
      const retryAfter = e.getRetryAfter(); // 秒
      console.log('请等待', retryAfter, '秒后重试');
    } else if (e.isServerError()) {
      // 服务端错误 (5xx)
      console.log('服务器繁忙,请稍后重试');
    }
  } else if (TimeoutError.is(e)) {
    console.log('请求超时', e.timeoutMs, 'ms');
  } else if (AbortError.is(e)) {
    console.log('请求已取消');
  } else if (NetworkError.is(e)) {
    console.log('网络错误:', e.message);
  }
  
  // 判断是否可重试
  if (isRetryableError(e)) {
    console.log('此错误可以重试');
  }
}

ApiError 方法

| 方法 | 说明 | |---|---| | isUnauthorized() | 是否 401 未授权 | | isForbidden() | 是否 403 禁止访问 | | isAuthError() | 是否认证错误 (401/403) | | isClientError() | 是否客户端错误 (4xx) | | isServerError() | 是否服务端错误 (5xx) | | isRateLimited() | 是否限流 (429) | | isNotFound() | 是否未找到 (404) | | isConflict() | 是否冲突 (409) | | isValidationError() | 是否验证失败 (400/422) | | getRetryAfter() | 获取 Retry-After 头的值(秒) | | getRetryDelayMs(default) | 获取重试延迟(毫秒) |

认证

Bearer Token

import { createFetchClient } from '@djvlc/openapi-client-core';

const client = createFetchClient({
  baseUrl: 'https://api.example.com',
  auth: {
    type: 'bearer',
    getToken: async () => {
      // 支持异步获取 Token
      return await tokenService.getAccessToken();
    },
    headerName: 'Authorization', // 可选,默认 'Authorization'
    prefix: 'Bearer',            // 可选,默认 'Bearer'
  },
});

API Key

const client = createFetchClient({
  baseUrl: 'https://api.example.com',
  auth: {
    type: 'api-key',
    apiKey: 'your-api-key',
    headerName: 'X-API-Key', // 可选
  },
});

Basic Auth

const client = createFetchClient({
  baseUrl: 'https://api.example.com',
  auth: {
    type: 'basic',
    username: 'admin',
    password: 'secret',
  },
});

自定义认证

const client = createFetchClient({
  baseUrl: 'https://api.example.com',
  auth: {
    type: 'custom',
    authenticate: async (headers) => {
      headers['X-Custom-Auth'] = await getCustomToken();
      headers['X-Timestamp'] = Date.now().toString();
    },
  },
});

拦截器

请求拦截器

import { 
  createFetchClient,
  createAuthInterceptor,
  createRequestIdInterceptor,
  createTraceInterceptor,
  createBearerAuthenticator,
} from '@djvlc/openapi-client-core';

const client = createFetchClient({
  baseUrl: 'https://api.example.com',
  requestInterceptors: [
    // 请求 ID 拦截器
    createRequestIdInterceptor({
      headerName: 'X-Request-ID',
    }),
    
    // 追踪拦截器
    createTraceInterceptor({
      traceIdHeader: 'X-Trace-ID',
      addTraceparent: true,
      getTraceparent: () => otel.getCurrentSpan()?.spanContext(),
    }),
  ],
});

错误拦截器

import { 
  createFetchClient,
  createLoggingInterceptor,
  createReportingInterceptor,
  createConsoleLogger,
} from '@djvlc/openapi-client-core';

const client = createFetchClient({
  baseUrl: 'https://api.example.com',
  errorInterceptors: [
    // 日志拦截器
    createLoggingInterceptor({
      logger: createConsoleLogger({ prefix: '[API]' }),
      verbose: true,
    }),
    
    // 错误上报拦截器
    createReportingInterceptor({
      reporter: {
        report: (error, context) => {
          Sentry.captureException(error, { extra: context });
        },
      },
      sampleRate: 0.1, // 10% 采样
    }),
  ],
});

自定义拦截器

import type { RequestInterceptor, ErrorInterceptor } from '@djvlc/openapi-client-core';

// 自定义请求拦截器
const customRequestInterceptor: RequestInterceptor = {
  name: 'custom-request',
  order: 0,
  intercept(context) {
    // 修改请求
    context.options.headers['X-Custom'] = 'value';
    return context.options;
  },
};

// 自定义错误拦截器
const customErrorInterceptor: ErrorInterceptor = {
  name: 'custom-error',
  order: 0,
  intercept(error, context) {
    console.log('请求失败:', context.url, error.message);
    return undefined; // 不处理,让错误继续传播
  },
};

const client = createFetchClient({
  baseUrl: 'https://api.example.com',
  requestInterceptors: [customRequestInterceptor],
  errorInterceptors: [customErrorInterceptor],
});

重试配置

基础用法

const client = createFetchClient({
  baseUrl: 'https://api.example.com',
  enableRetry: true,  // 启用重试,默认 true
  retry: {
    maxRetries: 3,
    initialDelayMs: 1000,
    maxDelayMs: 30000,
    backoffStrategy: 'exponential',
  },
});

完整配置选项

import type { RetryConfig, RetryInfo, RetryContext } from '@djvlc/openapi-client-core';

const retryConfig: RetryConfig = {
  // ============ 基础参数 ============
  
  /** 最大重试次数,默认 3 */
  maxRetries: 3,
  
  /** 初始延迟(毫秒),默认 1000 */
  initialDelayMs: 1000,
  
  /** 最大延迟(毫秒),默认 30000 */
  maxDelayMs: 30000,
  
  // ============ 退避策略 ============
  
  /**
   * 退避策略
   * - 'fixed': 固定延迟,每次重试使用 initialDelayMs
   * - 'linear': 线性增长,delay = initialDelayMs * attempt
   * - 'exponential': 指数增长,delay = initialDelayMs * (2 ^ attempt)
   * 默认 'exponential'
   */
  backoffStrategy: 'exponential',
  
  /**
   * 抖动系数(0-1)
   * 在计算的延迟基础上添加随机抖动,防止惊群效应
   * 实际延迟 = delay * (1 + random(-jitterFactor, +jitterFactor))
   * 默认 0.1
   */
  jitterFactor: 0.1,
  
  // ============ 重试条件 ============
  
  /**
   * 可重试的 HTTP 状态码
   * 默认 [429, 500, 502, 503, 504]
   */
  retryableStatusCodes: [429, 500, 502, 503, 504],
  
  /** 网络错误是否重试,默认 true */
  retryOnNetworkError: true,
  
  /** 超时是否重试,默认 true */
  retryOnTimeout: true,
  
  /** 是否尊重 Retry-After 响应头,默认 true */
  respectRetryAfter: true,
  
  /**
   * 自定义重试判断函数
   * 返回 true 表示应该重试
   */
  shouldRetry: (error: Error, attempt: number, context: RetryContext): boolean => {
    // 例:只重试 GET 请求
    if (context.method !== 'GET') return false;
    
    // 例:429 错误最多重试 5 次
    if (context.statusCode === 429 && attempt >= 5) return false;
    
    return true;
  },
  
  // ============ 回调 ============
  
  /**
   * 重试时的回调函数
   * 可用于日志、监控等
   */
  onRetry: (info: RetryInfo) => {
    console.log(`重试请求: ${info.method} ${info.url}`);
    console.log(`  - 第 ${info.attempt}/${info.maxRetries} 次`);
    console.log(`  - 延迟 ${info.delayMs}ms`);
    console.log(`  - 原因: ${info.error.message}`);
  },
};

退避策略对比

假设 initialDelayMs = 1000maxDelayMs = 30000

| 重试次数 | fixed | linear | exponential | |----------|-------|--------|-------------| | 第 1 次 | 1000ms | 1000ms | 1000ms | | 第 2 次 | 1000ms | 2000ms | 2000ms | | 第 3 次 | 1000ms | 3000ms | 4000ms | | 第 4 次 | 1000ms | 4000ms | 8000ms | | 第 5 次 | 1000ms | 5000ms | 16000ms | | 第 6 次 | 1000ms | 6000ms | 30000ms (capped) |

Retry-After 响应头

respectRetryAfter: true 时,客户端会检查响应头:

HTTP/1.1 429 Too Many Requests
Retry-After: 60

客户端会等待 60 秒后重试(但不超过 maxDelayMs)。

支持的格式:

  • 秒数:Retry-After: 120
  • HTTP 日期:Retry-After: Wed, 21 Oct 2025 07:28:00 GMT

禁用重试

// 方式 1:全局禁用
const client = createFetchClient({
  baseUrl: 'https://api.example.com',
  enableRetry: false,
});

// 方式 2:不配置 retry
const client = createFetchClient({
  baseUrl: 'https://api.example.com',
  // 不传 retry 配置
});

// 方式 3:单次请求禁用(通过自定义 shouldRetry)
const retryConfig: RetryConfig = {
  maxRetries: 3,
  shouldRetry: (error, attempt, context) => {
    // POST 请求不重试
    if (context.method === 'POST') return false;
    return true;
  },
};

请求去重

import { createRequestDeduper } from '@djvlc/openapi-client-core';

const deduper = createRequestDeduper({
  getOnly: true,  // 只对 GET 请求去重
  maxSize: 100,   // 最大缓存数量
});

// 包装 fetch
const dedupedFetch = deduper.wrap(fetch);

// 相同的请求只会执行一次,结果共享
const [r1, r2] = await Promise.all([
  dedupedFetch('/api/user'),
  dedupedFetch('/api/user'),
]);
// 只发送了 1 个请求!

console.log('当前飞行中请求数:', deduper.inflightCount);

指标收集

import { 
  createMetricsCollector, 
  createMetricsInterceptors,
  createFetchClient,
} from '@djvlc/openapi-client-core';

// 创建指标收集器
const metrics = createMetricsCollector({
  maxMetrics: 1000,
  ttlMs: 3600000,  // 1 小时过期
  onMetrics: (m) => {
    console.log(`${m.method} ${m.path} - ${m.durationMs}ms`);
  },
});

// 创建拦截器
const metricsInterceptors = createMetricsInterceptors(metrics);

const client = createFetchClient({
  baseUrl: 'https://api.example.com',
  requestInterceptors: [metricsInterceptors.request],
  responseInterceptors: [metricsInterceptors.response],
  errorInterceptors: [metricsInterceptors.error],
});

// 获取指标摘要
const summary = metrics.summary();
console.log('总请求数:', summary.totalRequests);
console.log('成功率:', (summary.successRate * 100).toFixed(1), '%');
console.log('P50:', summary.p50Ms, 'ms');
console.log('P95:', summary.p95Ms, 'ms');
console.log('P99:', summary.p99Ms, 'ms');

// 按路径查看
const pageMetrics = metrics.getByPath('/api/pages');

// 清空并获取所有指标
const allMetrics = metrics.flush();

指标字段

| 字段 | 类型 | 说明 | |---|---|---| | requestId | string | 请求唯一标识 | | url | string | 完整 URL | | path | string | 路径(不含 query) | | method | string | HTTP 方法 | | startTime | number | 开始时间戳 | | endTime | number | 结束时间戳 | | durationMs | number | 耗时(毫秒) | | status | number | HTTP 状态码 | | success | boolean | 是否成功 (2xx) | | retryCount | number | 重试次数 | | traceId | string? | 追踪 ID | | error | string? | 错误信息 |

日志

import { createConsoleLogger, createBufferLogger } from '@djvlc/openapi-client-core';

// 控制台日志器
const consoleLogger = createConsoleLogger({
  prefix: '[DJV-API]',
  level: 'debug',      // 最低日志级别
  timestamp: true,     // 显示时间戳
});

// 缓冲日志器(用于测试)
const bufferLogger = createBufferLogger(1000);

// 使用
const client = createFetchClient({
  baseUrl: 'https://api.example.com',
  logger: consoleLogger,
  debug: true,
});

// 获取缓冲的日志
const logs = bufferLogger.getLogs();
const errorLogs = bufferLogger.getLogsByLevel('error');

Token 自动刷新

import { 
  createFetchClient,
  createTokenRefreshInterceptor,
} from '@djvlc/openapi-client-core';

// 创建客户端时配置 Token 刷新
const client = createFetchClient({
  baseUrl: 'https://api.example.com',
  auth: {
    type: 'bearer',
    getToken: () => tokenService.getAccessToken(),
  },
});

// 添加 Token 刷新拦截器
client.interceptors.addErrorInterceptor(
  createTokenRefreshInterceptor({
    refreshToken: async () => {
      const newToken = await authService.refresh();
      tokenService.setAccessToken(newToken);
      return newToken;
    },
    triggerStatusCodes: [401],
    maxRetries: 1,
    onTokenRefreshed: (newToken) => {
      console.log('Token 已刷新');
    },
    onRefreshFailed: (error) => {
      console.log('Token 刷新失败,跳转登录');
      redirectToLogin();
    },
    executeRequest: (context) => client.request(context.originalOptions),
  })
);

工具函数

import {
  // 请求 ID
  generateRequestId,
  generateTraceId,
  isValidRequestId,
  
  // 重试延迟
  calculateRetryDelay,
  parseRetryAfter,
  
  // 延迟执行
  sleep,
  sleepWithAbort,
  
  // URL 处理
  buildUrl,
  extractPath,
  parseQueryString,
  joinPaths,
  
  // 请求头处理
  mergeHeaders,
  getHeader,
  setHeader,
  removeHeader,
} from '@djvlc/openapi-client-core';

// 生成 ID
const requestId = generateRequestId();  // req_m5abc123_k7def456
const traceId = generateTraceId();      // trace_m5abc123_k7def456_x8ghi789

// URL 处理
const url = buildUrl('https://api.example.com', '/users', { page: 1, size: 10 });
// https://api.example.com/users?page=1&size=10

// 请求头处理
const headers = mergeHeaders(
  { 'Content-Type': 'application/json' },
  { 'Authorization': 'Bearer xxx' },
);

API 参考

错误类

| 类 | 说明 | |---|---| | BaseClientError | 所有错误的基类 | | ApiError | 业务错误(HTTP 非 2xx 响应) | | NetworkError | 网络层错误 | | TimeoutError | 超时错误 | | AbortError | 请求被取消 |

认证器

| 类 | 说明 | |---|---| | BearerAuthenticator | Bearer Token 认证 | | ApiKeyAuthenticator | API Key 认证 | | BasicAuthenticator | Basic Auth 认证 | | CustomAuthenticator | 自定义认证 | | NoAuthenticator | 无认证 |

拦截器

| 类 | 类型 | 说明 | |---|---|---| | AuthInterceptor | 请求 | 自动添加认证信息 | | RequestIdInterceptor | 请求 | 生成请求 ID | | TraceInterceptor | 请求 | 添加追踪上下文 | | TimeoutInterceptor | 请求 | 智能设置超时 | | ErrorTransformInterceptor | 响应 | 错误格式转换 | | RetryInterceptor | 错误 | 自动重试 | | TokenRefreshInterceptor | 错误 | Token 自动刷新 | | LoggingInterceptor | 错误 | 错误日志记录 | | ReportingInterceptor | 错误 | 错误上报监控 |

客户端

| 类/函数 | 说明 | |---|---| | FetchClient | 基于原生 fetch 的独立客户端 | | createFetchClient() | 创建 FetchClient 实例 | | createEnhancedFetch() | 创建增强的 fetch 函数(用于 typescript-fetch 生成代码) | | createAxiosInstance() | 创建配置好的 Axios 实例(向后兼容) |

插件

| 类/函数 | 说明 | |---|---| | RequestDeduper | 请求去重器 | | DefaultMetricsCollector | 默认指标收集器 | | ConsoleLogger | 控制台日志器 | | BufferLogger | 缓冲日志器(测试用) |

版本信息

import { VERSION, SDK_NAME, getSdkInfo } from '@djvlc/openapi-client-core';

console.log(`${SDK_NAME}@${VERSION}`);
// @djvlc/[email protected]

const info = getSdkInfo();
// { name: '@djvlc/openapi-client-core', version: '1.0.0' }

迁移指南(从 1.x 升级)

破坏性变更

  1. 模块路径变更:原来的单文件被拆分为多个模块目录
  2. 类型定义变更:部分类型名称和结构有调整
  3. HttpClient 重命名HttpClient 改名为 FetchClient
  4. createClient 重命名createClient 改名为 createFetchClient
  5. 默认使用 Fetch:OpenAPI 生成的客户端默认使用 typescript-fetch,不再依赖 axios

从 axios 迁移到 fetch

如果你之前使用 createAxiosInstance,现在推荐使用 createEnhancedFetch

// 旧代码(axios)
import { createAxiosInstance } from '@djvlc/openapi-client-core';
const axiosInstance = createAxiosInstance({ baseUrl: '/api' });
const api = new PagesApi(config, '/api', axiosInstance);

// 新代码(fetch)
import { createEnhancedFetch } from '@djvlc/openapi-client-core';
const enhancedFetch = createEnhancedFetch({ baseUrl: '/api' });
const api = new PagesApi(config, '/api', enhancedFetch);

向后兼容

  • createAxiosInstance 仍然可用,用于需要 axios 的场景
  • 所有配置选项在 createEnhancedFetch 中保持一致

License

MIT