npm package: @seed-fe/request
Description: A powerful and flexible HTTP request library based on Axios
Author: xianghongai
Latest version: 1.2.0

# @seed-fe/request

基于 Axios 封装的企业级 HTTP 请求库，提供多实例管理、灵活的拦截器配置和强大的错误处理能力。

[![TypeScript](https://img.shields.io/badge/TypeScript-5.0+-blue.svg)](https://www.typescriptlang.org/)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)

## 核心特性

- 多实例管理
  - 支持多个 API 实例并行工作，每个实例独立配置 baseURL、超时、拦截器等。
- 灵活的拦截器
  - 支持函数、对象、元组、数组等多种格式，满足不同场景需求。

- 清晰的执行模型
  - **实例级拦截器**: 永久注册到 Axios，保持原生语义
  - **共享预设 + 请求级拦截器**: 按请求手动链式执行，按需组合
- 并发安全
  - 请求发起时捕获共享上下文快照，拦截器读取只读视图，确保并发请求隔离。
- 强大的错误处理
  - Koa 风格中间件管道，支持三级错误处理 (请求级 → 实例级 → 共享级)。
- 完整的 TypeScript 支持
  - 严格类型定义，智能代码提示，类型安全的 API。

## 安装

```bash
# npm
npm install @seed-fe/request axios

# yarn
yarn add @seed-fe/request axios

# pnpm
pnpm add @seed-fe/request axios
```

> **注意**: axios 是 peer dependency，需要单独安装。

## 快速开始

### 基础配置

```typescript
import { defineRequestConfig } from '@seed-fe/request';

// 定义默认实例配置
defineRequestConfig({
  baseURL: 'https://api.example.com',
  timeout: 5000,
  interceptors: {
    request: {
      onConfig: (config) => {
        // 添加认证 token
        config.headers.Authorization = `Bearer ${getToken()}`;
        return config;
      }
    },
    response: {
      onConfig: (response) => response,
      onError: (error) => {
        console.error('请求失败:', error);
        return Promise.reject(error);
      }
    }
  }
});
```

### 发起请求

```typescript
import httpRequest from '@seed-fe/request';

// GET 请求
const users = await httpRequest.get('/api/users');

// POST 请求
const newUser = await httpRequest.post('/api/users', {
  name: 'John',
  email: 'john@example.com'
});

// 带查询参数
const user = await httpRequest.get('/api/users/1', {
  params: { include: 'profile' }
});
```

## 核心概念

### 执行模型

本库采用 **"实例级永久 + 共享/请求级手动链"** 的混合执行策略:

```
┌─────────────────────────────────────────────────────────┐
│                     请求发起                              │
└─────────────────────────────────────────────────────────┘
                          ↓
┌─────────────────────────────────────────────────────────┐
│  1. 捕获共享上下文快照 (并发隔离)                         │
└─────────────────────────────────────────────────────────┘
                          ↓
┌─────────────────────────────────────────────────────────┐
│  2. 请求拦截器链 (手动执行)                               │
│     ├─ 共享预设拦截器                                     │
│     └─ 请求级拦截器                                       │
└─────────────────────────────────────────────────────────┘
                          ↓
┌─────────────────────────────────────────────────────────┐
│  3. Axios 发送请求 (自动执行实例级拦截器)                  │
└─────────────────────────────────────────────────────────┘
                          ↓
┌─────────────────────────────────────────────────────────┐
│  4. 响应拦截器链 (手动执行)                               │
│     ├─ 请求级拦截器                                       │
│     └─ 共享预设拦截器                                     │
└─────────────────────────────────────────────────────────┘
                          ↓
┌─────────────────────────────────────────────────────────┐
│  5. 错误处理管道 (如果出错)                               │
│     请求级 → 实例级 → 共享级                              │
└─────────────────────────────────────────────────────────┘
```

**优势**:

- 实例级拦截器性能最优 (注册一次，永久生效)
- 共享预设灵活组合 (运行期可更新上下文)
- 请求级拦截器按需使用 (临时覆盖，不污染实例)

## 高级功能

### 1. 多实例管理

为不同的 API 服务创建独立实例:

```typescript
import { defineRequestConfig, getRequestInstance } from '@seed-fe/request';

// 配置多个实例
defineRequestConfig([
  {
    instanceName: 'userAPI',
    baseURL: 'https://api.example.com',
    timeout: 5000
  },
  {
    instanceName: 'authAPI',
    baseURL: 'https://auth.example.com',
    timeout: 3000
  }
]);

// 使用不同实例
const userRequest = getRequestInstance('userAPI');
const authRequest = getRequestInstance('authAPI');

const users = await userRequest.get('/users');
const authInfo = await authRequest.post('/login', credentials);
```

### 2. 共享预设 (Shared Preset)

在多个实例间共享通用逻辑（最常见：鉴权与语言注入）：

```typescript
import { defineSharedPreset, defineRequestConfig, updateSharedContext } from '@seed-fe/request';
import type { AxiosResponse, InternalAxiosRequestConfig } from 'axios';
// 建议把具体注入逻辑放在项目的 configs/request 目录下
import { setAuthorizationHeader } from '@/configs/request/injectors/auth';
import { setAcceptLanguageHeader } from '@/configs/request/injectors/locale';

// 定义共享预设（装配最小注入逻辑：鉴权与语言）
defineSharedPreset({
  onRequest: {
    handlers: [
      (config: InternalAxiosRequestConfig) => setAuthorizationHeader(config),
      (config: InternalAxiosRequestConfig) => setAcceptLanguageHeader(config),
    ],
  },
  onResponse: {
    handlers: [(response: AxiosResponse) => response], // 返回 AxiosResponse；数据解包由 withFullResponse 控制
  },
  onError: {
    handlers: [], // 启用错误标准化（在响应拦截器中注入 onRejected）
  },
});

// 定义实例 (默认启用共享预设)
defineRequestConfig({
  instanceName: 'api',
  baseURL: '/api'
});

// （可选）若你的 setAcceptLanguageHeader 需要读取共享上下文，可在运行期更新
// updateSharedContext({ lang: 'zh-CN' });
```

**层级覆盖优先级**: 请求级 > 实例级 > 共享级

```typescript
// 临时禁用共享预设的错误处理
await httpRequest.get('/public/health', {
  presetOverrides: {
    onError: { enabled: false }
  }
});
```

### 3. 拦截器配置

支持多种灵活的拦截器格式:

#### 函数形式 (简洁)

```typescript
interceptors: {
  request: (config) => {
    config.headers.Token = getToken();
    return config;
  }
}
```

#### 对象形式 (分离成功/异常)

```typescript
interceptors: {
  request: {
    onConfig: (config) => {
      config.headers.Token = getToken();
      return config;
    },
    onError: (error) => {
      console.error('请求配置错误:', error);
      return Promise.reject(error);
    }
  }
}
```

#### 元组形式 (Axios 原生风格)

```typescript
interceptors: {
  request: [
    (config) => config,                    // onFulfilled
    (error) => Promise.reject(error)       // onRejected
  ]
}
```

#### 数组形式 (链式多个拦截器)

```typescript
interceptors: {
  request: [
    // 拦截器 1: 添加 token
    (config) => {
      config.headers.Authorization = `Bearer ${getToken()}`;
      return config;
    },
    // 拦截器 2: 添加时间戳
    (config) => {
      config.params = { ...config.params, _t: Date.now() };
      return config;
    },
    // 拦截器 3: 对象形式
    {
      onConfig: (config) => {
        console.log('Request:', config.url);
        return config;
      },
      onError: (error) => Promise.reject(error)
    }
  ]
}
```

### 4. 错误处理管道 (推荐)

Koa 风格中间件，支持三级错误处理:

#### 请求级中间件 (临时处理)

```typescript
await httpRequest.get('/api/users', {
  errorPipeline: [
    async (ctx, next) => {
      // 特定请求的错误提示
      console.warn('请求失败:', ctx.normalized.message);
      await next();  // 继续下一级
    }
  ]
});
```

#### 实例级中间件 (特定服务)

```typescript
import { hasResponseStatus } from '@seed-fe/request';

defineRequestConfig({
  instanceName: 'payment',
  baseURL: '/api/payment',
  errorPipeline: [
    async (ctx, next) => {
      // 处理支付服务特有的 409 冲突错误
      if (hasResponseStatus(ctx.response) && ctx.response.status === 409) {
        showConflictDialog('订单已存在');
        return;  // 终止，不继续下探
      }
      await next();  // 其他错误交给共享级
    }
  ]
});
```

#### 共享级中间件 (全局统一)

```typescript
import { setGlobalErrorPipeline, getResponseStatus } from '@seed-fe/request';

// 命名中间件函数（便于复用/测试/维护）
async function logErrorMw(ctx, next) {
  console.error('Error:', {
    message: ctx.normalized.message,
    url: ctx.config.url,
    instance: ctx.instanceName,
    status: getResponseStatus(ctx)
  });
  await next();
}

async function auth401RedirectMw(ctx, next) {
  if (getResponseStatus(ctx) === 401) {
    redirectToLogin();
    return; // 终止
  }
  await next();
}

setGlobalErrorPipeline([logErrorMw, auth401RedirectMw]);
```

#### 跳过错误处理

```typescript
// 跳过整条错误管道
const data = await httpRequest.get('/api/users', {
  skipErrorPipeline: true
});
```

**类型安全的辅助函数**:

```typescript
import {
  hasResponseStatus,
  getResponseStatus,
  type ErrorMiddleware
} from '@seed-fe/request';

const middleware: ErrorMiddleware = async (ctx, next) => {
  // 类型守卫: 检查是否有 status 字段
  if (hasResponseStatus(ctx.response)) {
    console.log('Status:', ctx.response.status);  // TypeScript 类型安全
  }

  // 辅助函数: 安全获取 status
  const status = getResponseStatus(ctx);
  if (status === 404) {
    // 处理 404
  }

  await next();
};
```

### 5. 获取完整响应

```typescript
// 默认: 只返回 response.data
const data = await httpRequest.get('/api/users');

// 获取完整响应对象
const response = await httpRequest.get('/api/users', {
  withFullResponse: true
});

console.log(response.status);   // 200
console.log(response.headers);  // { ... }
console.log(response.data);     // [ ... ]
```

## 实例管理 API

### defineRequestConfig(config)

注册请求实例配置。

```typescript
// 单个实例
defineRequestConfig({
  baseURL: 'https://api.example.com',
  timeout: 5000
});

// 多个实例
defineRequestConfig([
  { instanceName: 'api1', baseURL: 'https://api1.example.com' },
  { instanceName: 'api2', baseURL: 'https://api2.example.com' }
]);
```

### getRequestInstance(instanceName?)

获取指定的请求实例。

```typescript
const api = getRequestInstance('api1');
const data = await api.get('/users');
```

### hasInstance(instanceName?)

检查实例是否存在。

```typescript
if (hasInstance('api1')) {
  console.log('实例存在');
}
```

### clearInstance(instanceName?)

清除指定实例。

```typescript
clearInstance('api1');
```

### clearAllInstances()

清除所有实例。

```typescript
clearAllInstances();
```

## 共享预设 API

### defineSharedPreset(preset)

定义共享预设配置（简单、易用的装配示例）。

```typescript
// 装配拦截器：注入 Authorization 与 Accept-Language；保持 onResponse 返回 AxiosResponse
import { defineSharedPreset } from '@seed-fe/request';
import type { AxiosResponse, InternalAxiosRequestConfig } from 'axios';

// 迁移后：把请求相关注入逻辑放在你自己的项目目录（示例路径）
import { setAuthorizationHeader } from '@/configs/request/injectors/auth';
import { setAcceptLanguageHeader } from '@/configs/request/injectors/locale';

export function registerSharedPreset(): void {
  defineSharedPreset({
    id: 'app-shared-preset',
    onRequest: {
      // 建议顺序：先鉴权，再语言
      handlers: [
        (config: InternalAxiosRequestConfig): InternalAxiosRequestConfig => setAuthorizationHeader(config),
        (config: InternalAxiosRequestConfig): InternalAxiosRequestConfig => setAcceptLanguageHeader(config),
      ],
    },
    onResponse: {
      // 注意：onResponse 必须返回 AxiosResponse；是否只返回 data 由 withFullResponse 控制
      handlers: [(response: AxiosResponse) => response],
    },
    onError: {
      // 仅开启“错误标准化”能力（在响应拦截器中追加 onRejected），无需编写具体 handler
      handlers: [],
    },
  });
}
```

### updateSharedContext(context)

更新共享上下文 (运行期动态更新)。

```typescript
updateSharedContext({
  lang: 'zh-CN',
  userId: '123'
});
```

### readPresetContext(config)

在拦截器中读取共享上下文快照（可选高级用法）。

```typescript
// 提示：readPresetContext 在“拦截器执行时”读取按请求捕获的上下文快照；
// 注册 defineSharedPreset 本身不会触发读取。
defineSharedPreset({
  onRequest: {
    handlers: [(config) => {
      const snapshot = readPresetContext(config);
      if (snapshot.lang) {
        (config.headers as any)['Accept-Language'] = String(snapshot.lang);
      }
      return config;
    }],
  },
});
```

### getSharedPreset()

获取当前共享预设 (只读)。

```typescript
const preset = getSharedPreset();
console.log(preset);
```

### clearSharedPreset()

清除共享预设。

```typescript
clearSharedPreset();
```

## 错误处理 API

### setGlobalErrorPipeline(middlewares)

设置全局错误中间件。

```typescript
import { setGlobalErrorPipeline } from '@seed-fe/request';

async function globalLogMw(ctx, next) {
  console.error('Global error:', ctx.normalized.message);
  await next();
}

setGlobalErrorPipeline([globalLogMw]);
```

### clearGlobalErrorPipeline()

清除全局错误中间件。

```typescript
clearGlobalErrorPipeline();
```

## HTTP 方法快捷方式

```typescript
// GET
httpRequest.get<T>(url, config?)

// POST
httpRequest.post<T>(url, data?, config?)

// PUT
httpRequest.put<T>(url, data?, config?)

// DELETE
httpRequest.delete<T>(url, config?)

// PATCH
httpRequest.patch<T>(url, data?, config?)

// HEAD
httpRequest.head<T>(url, config?)

// OPTIONS
httpRequest.options<T>(url, config?)
```

## 完整示例

### 企业级配置示例

```typescript
import logger from '@seed-fe/logger';
import {
  defineRequestConfig,
  defineSharedPreset,
  updateSharedContext,
  setGlobalErrorPipeline,
  getResponseStatus,
  hasResponseStatus,
  readPresetContext
} from '@seed-fe/request';

// 1. 定义共享预设
function injectAuth(config: any) {
  const token = getToken?.();
  if (token) {
    config.headers = config.headers || {};
    (config.headers as any).Authorization = `Bearer ${token}`;
  }
  return config;
}

function injectLang(config: any) {
  const snapshot = readPresetContext(config);
  if (snapshot.lang) {
    config.headers = config.headers || {};
    config.headers['Accept-Language'] = String(snapshot.lang);
  }
  return config;
}

function injectTrace(config: any) {
  config.headers = config.headers || {};
  config.headers['X-Request-ID'] = crypto.randomUUID();
  // 可选：统一追踪链路
  if (typeof getTraceId === 'function') {
    (config.headers as any)['X-Trace-ID'] = getTraceId();
  }
  return config;
}

function injectAppVersion(config: any) {
  config.headers = config.headers || {};
  config.headers['X-App-Version'] = '1.0.0';
  return config;
}

defineSharedPreset({
  onRequest: {
    // 建议顺序：鉴权 → 语言 → 追踪 → 版本
    handlers: [injectAuth, injectLang, injectTrace, injectAppVersion],
  },
  onError: { handlers: [] }, // 启用错误标准化
});

// 2. 配置全局错误处理
// 命名中间件函数（推荐）
async function logErrorMw(ctx, next) {
  logger.error('Request failed:', {
    url: ctx.config.url,
    method: ctx.config.method,
    status: getResponseStatus(ctx),
    message: ctx.normalized.message
  });
  await next();
}

async function auth401Mw(ctx, next) {
  if (getResponseStatus(ctx) === 401) {
    clearToken();
    window.location.href = '/login';
    return;
  }
  await next();
}

async function notifyMw(ctx) {
  const status = getResponseStatus(ctx);
  if (status && status >= 500) {
    showToast('服务器错误，请稍后重试');
  } else if (status === 404) {
    showToast('请求的资源不存在');
  }
}

setGlobalErrorPipeline([logErrorMw, auth401Mw, notifyMw]);

// 3. 定义多个实例（鉴权逻辑已在共享预设中统一配置）
defineRequestConfig([
  // 用户服务
  {
    instanceName: 'userAPI',
    baseURL: 'https://api.example.com',
    timeout: 5000
  },

  // 支付服务
  {
    instanceName: 'paymentAPI',
    baseURL: 'https://pay.example.com',
    timeout: 10000,
    errorPipeline: [
      async (ctx, next) => {
        // 支付特有错误处理
        if (hasResponseStatus(ctx.response) && ctx.response.status === 409) {
          showDialog('订单已存在，请勿重复提交');
          return;
        }
        await next();
      }
    ]
  }
]);

// 4. 运行期更新上下文
updateSharedContext({
  lang: localStorage.getItem('language') || 'zh-CN',
  userId: getCurrentUserId()
});

// 5. 使用
import httpRequest, { getRequestInstance } from '@seed-fe/request';

// 使用默认实例
const users = await httpRequest.get('/api/users');

// 使用指定实例
const paymentAPI = getRequestInstance('paymentAPI');
const order = await paymentAPI.post('/orders', orderData);
```

小结：执行顺序与性能

- 顺序建议：鉴权优先（可能影响后续签名/路由），其后语言、追踪、版本等无强依赖项；
- 性能说明：共享/请求级拦截器为“内存链”顺序执行，复杂度 O(N)；相较网络 I/O，拆分多个小 handler 的额外开销可忽略；
- 何时合并：若某些逻辑强耦合且有重计算（如签名/加密），可合并为单个 handler 以避免重复计算；
- 临时关闭：可通过共享上下文或请求级覆盖控制某个 handler 的早退（例如 snapshot.flags.disableLang === true 时跳过语言头）。

## 配置选项

### CustomRequestConfig

继承自 `AxiosRequestConfig`，扩展字段:

```typescript
interface CustomRequestConfig extends AxiosRequestConfig {
  // 实例名称
  instanceName?: InstanceName;

  // 是否返回完整响应 (默认 false，仅返回 response.data)
  withFullResponse?: boolean;

  // 跳过错误处理管道
  skipErrorPipeline?: boolean;

  // 是否启用共享预设 (默认 true)
  useSharedPreset?: boolean;

  // 错误处理中间件
  errorPipeline?: ErrorMiddleware[];

  // 拦截器配置
  interceptors?: {
    request?: RequestInterceptorConfig;
    response?: ResponseInterceptorConfig;
  };

  // 共享预设覆盖
  presetOverrides?: PresetOverrides;
}
```

### SharedPreset

```typescript
interface SharedPreset {
  id?: string;
  context?: Readonly<Record<string, unknown>>;
  onRequest?: StageConfig<RequestStageHook>;
  onResponse?: StageConfig<ResponseStageHook>;
  onError?: StageConfig<ErrorStageHook>;
}
```

### ErrorMiddleware

```typescript
type ErrorMiddleware = (ctx: ErrorContext, next: ErrorNext) => void | Promise<void>;

interface ErrorContext {
  error: unknown;              // 原始错误
  normalized: Error;           // 标准化后的 Error 对象
  config: CustomRequestConfig; // 请求配置
  instanceName?: InstanceName; // 实例名称
  response?: unknown;          // 响应对象 (如果有)
  state: Record<string, unknown>; // 中间件间共享状态
}
```

## 最佳实践

### 1. 安全性

```typescript
// ❌ 避免: 在日志中记录敏感信息
async function insecureLogMw(ctx) {
  console.error('Error:', ctx.config); // 可能包含 token
}
setGlobalErrorPipeline([insecureLogMw]);

// ✅ 推荐: 脱敏后记录
async function safeLogMw(ctx) {
  console.error('Error:', {
    url: ctx.config.url,
    method: ctx.config.method,
    status: getResponseStatus(ctx)
    // 不记录 headers/data
  });
}
setGlobalErrorPipeline([safeLogMw]);
```

### 2. 可观测性

```typescript
// 添加请求追踪
defineSharedPreset({
  onRequest: {
    handlers: [(config) => {
      config.headers = config.headers || {};
      config.headers['X-Request-ID'] = crypto.randomUUID();
      config.headers['X-Trace-ID'] = getTraceId();
      return config;
    }]
  }
});
```

### 3. 错误处理

```typescript
// ✅ 推荐: 分层处理错误
// 请求级: 特定请求的 UI 反馈
// 实例级: 特定服务的错误逻辑
// 共享级: 通用错误 (401/500 等)

// ❌ 避免: 在错误中间件中吞掉错误
errorPipeline: [
  async (ctx, next) => {
    try {
      await next();
    } catch (e) {
      // ❌ 不要吞掉错误
    }
  }
]

// ✅ 推荐: 让错误继续传播
errorPipeline: [
  async (ctx, next) => {
    console.error('Error:', ctx.normalized.message);
    await next();  // 继续下一级
  }
]
```

### 4. 性能优化

```typescript
// ✅ 推荐: 实例级拦截器用于固定逻辑 (性能最优)
defineRequestConfig({
  interceptors: {
    request: (config) => {
      config.headers.Authorization = `Bearer ${getToken()}`;
      return config;
    }
  }
});

// ✅ 推荐: 共享预设用于可配置逻辑
defineSharedPreset({
  onRequest: {
    handlers: [(config) => {
      const snapshot = readPresetContext(config);
      // 根据上下文动态处理
      return config;
    }]
  }
});

// ⚠️ 谨慎: 请求级拦截器用于临时覆盖
await httpRequest.get('/api/users', {
  interceptors: {
    request: (config) => {
      // 仅此次请求生效
      return config;
    }
  }
});
```

## 迁移指南

### 从旧版 errorHandler 迁移到 errorPipeline

旧版 (不推荐):

```typescript
defineRequestConfig({
  errorConfig: {
    errorHandler: (error, { config }) => {
      console.error('Error:', error);
    }
  }
});
```

新版 (推荐):

```typescript
defineRequestConfig({
  errorPipeline: [
    async (ctx, next) => {
      console.error('Error:', ctx.normalized.message);
      await next();
    }
  ]
});
```

详细迁移指南: [MIGRATION-alpha.md](./MIGRATION-alpha.md)

## 常见问题

### 1. 如何处理 token 过期?

```typescript
async function tokenRefreshMw(ctx, next) {
  if (getResponseStatus(ctx) === 401) {
    const refreshed = await refreshToken();
    if (refreshed) {
      // 重试请求
      return httpRequest(ctx.config);
    } else {
      // 跳转登录
      redirectToLogin();
    }
    return;
  }
  await next();
}
setGlobalErrorPipeline([tokenRefreshMw]);
```

### 2. 如何取消请求?

```typescript
const controller = new AbortController();

const promise = httpRequest.get('/api/users', {
  signal: controller.signal
});

// 取消请求
controller.abort();
```

### 3. 如何处理文件上传?

```typescript
const formData = new FormData();
formData.append('file', file);

await httpRequest.post('/api/upload', formData, {
  headers: {
    'Content-Type': 'multipart/form-data'
  },
  onUploadProgress: (progressEvent) => {
    const progress = Math.round((progressEvent.loaded * 100) / progressEvent.total);
    console.log('Upload progress:', progress);
  }
});
```

### 4. 如何设置请求重试?

```typescript
defineRequestConfig({
  errorPipeline: [
    async (ctx, next) => {
      const maxRetries = 3;
      const retryCount = ctx.state.retryCount || 0;

      if (retryCount < maxRetries && shouldRetry(ctx)) {
        ctx.state.retryCount = retryCount + 1;
        await sleep(1000 * retryCount);
        return httpRequest(ctx.config);
      }

      await next();
    }
  ]
});
```

## 贡献

欢迎贡献代码和反馈问题！

1. Fork 本仓库
2. 创建特性分支 (`git checkout -b feature/AmazingFeature`)
3. 提交更改 (`git commit -m 'Add some AmazingFeature'`)
4. 推送到分支 (`git push origin feature/AmazingFeature`)
5. 开启 Pull Request

## 协议

MIT License - 详见 [LICENSE](./LICENSE) 文件

## 致谢

本项目基于 [Axios](https://axios-http.com/) 构建，感谢 Axios 团队的出色工作。