npm package: @lark-apaas/hey-api-custom-client-axios
Description: Custom axios client plugin for @hey-api/openapi-ts
Latest version: 1.0.5

# Custom Axios Client Plugin

基于 `@hey-api/client-axios` 的自定义 axios 客户端插件，用于 `@hey-api/openapi-ts` 代码生成。

## 设计理念

**单例模式**：一个应用使用一个全局 axios 客户端实例，所有 API 请求基于此实例。通过配置文件统一封装认证、错误处理等必备逻辑，业务代码无需感知这些细节，只需传递业务相关参数。

## 安装

```bash
npm install @lark-apaas/hey-api-custom-client-axios
# or
yarn add @lark-apaas/hey-api-custom-client-axios
```

## 快速开始

### 1. 配置插件（代码生成时）

```typescript
import { customClientAxiosPlugin } from '@lark-apaas/hey-api-custom-client-axios';
import { createClient } from '@hey-api/openapi-ts';

await createClient({
  input: openapiPath,
  output: {
    path: clientSdkOutPath,
  },
  plugins: [
    '@hey-api/typescript',
    '@hey-api/sdk',
    customClientAxiosPlugin({
      // 指定运行时配置文件路径（推荐）
      runtimeConfigPath: './src/api/client-config.ts',
      // 其他配置
      throwOnError: false,
    }),
  ],
});
```

### 2. 创建全局配置文件（运行时）

创建 `src/api/client-config.ts`，封装应用级的必备逻辑：

```typescript
import type { CreateClientConfig } from '@lark-apaas/hey-api-custom-client-axios';

/**
 * 创建全局客户端配置
 * 这个函数会在客户端初始化时被调用
 */
export const createClientConfig: CreateClientConfig = () => {
  return {
    // 基础 URL
    baseURL: process.env.API_BASE_URL || 'https://api.example.com',

    // 全局 headers
    headers: {
      'X-App-Version': '1.0.0',
      'X-Platform': 'web',
    },

    // 统一认证逻辑
    auth: async (auth) => {
      // 从 localStorage 或其他地方获取 token
      const token = localStorage.getItem('auth_token');
      if (!token) {
        throw new Error('未登录');
      }
      return token;
    },

    // 响应数据转换（可选）
    responseTransformer: async (data: any) => {
      // 统一转换日期字符串为 Date 对象
      return transformDates(data);
    },

    // 响应数据验证（可选）
    responseValidator: async (data: any) => {
      // 统一验证响应格式
      if (data.code !== 0) {
        throw new Error(data.message || '请求失败');
      }
      return data.data;
    },

    // 其他全局配置
    timeout: 30000,
    withCredentials: true,
  };
};
```

### 3. 业务代码使用（无感知）

生成 SDK 后，业务代码只需传递业务参数：

```typescript
import { getUserById, updateUser, listUsers } from './generated/sdk';

// 示例 1: 简单查询
// 自动应用全局配置（baseURL、认证、headers 等）
const user = await getUserById({
  path: { id: '123' },
});

// 示例 2: 传递请求级额外 header
const users = await listUsers({
  query: { page: 1, limit: 10 },
  headers: {
    'X-Request-ID': generateRequestId(), // 可选的请求级 header
  },
});

// 示例 3: 更新数据
await updateUser({
  path: { id: '123' },
  body: { name: 'New Name' },
});
```

## 配置选项

### 插件配置（代码生成时）

在 `customClientAxiosPlugin()` 中可配置：

#### runtimeConfigPath

- **类型**: `string`
- **说明**: 运行时配置文件路径（相对于生成的 SDK 目录）
- **推荐使用**: 通过此选项将全局配置逻辑集中管理

```typescript
customClientAxiosPlugin({
  runtimeConfigPath: './src/api/client-config.ts',
})
```

#### throwOnError

- **类型**: `boolean`
- **默认值**: `false`
- **说明**: 请求失败时是否抛出异常。设为 `false` 时，错误会作为返回对象的 `error` 字段返回

```typescript
customClientAxiosPlugin({
  throwOnError: true, // 请求失败时抛出异常
})
```

### 运行时配置（createClientConfig）

支持所有 axios 和 @hey-api/client-axios 的配置选项：

#### 基础配置

| 配置项 | 类型 | 说明 |
|--------|------|------|
| `baseURL` | `string` | API 基础 URL |
| `timeout` | `number` | 请求超时时间（毫秒） |
| `headers` | `object` | 全局请求头 |
| `withCredentials` | `boolean` | 是否携带凭证（cookie） |

#### 高级配置

| 配置项 | 类型 | 说明 |
|--------|------|------|
| `auth` | `function` | 统一认证逻辑，返回 token |
| `responseTransformer` | `function` | 响应数据转换函数 |
| `responseValidator` | `function` | 响应数据验证函数 |
| `requestValidator` | `function` | 请求数据验证函数 |
| `bodySerializer` | `function` | 请求体序列化函数 |
| `querySerializer` | `object \| function` | 查询参数序列化配置 |

#### Axios 原生配置

支持所有 axios 原生配置（除了 `method`）：

- `responseType` - 响应类型
- `responseEncoding` - 响应编码
- `xsrfCookieName` / `xsrfHeaderName` - CSRF 防护
- `maxContentLength` / `maxBodyLength` - 内容限制
- `maxRedirects` - 重定向限制
- `validateStatus` - 状态码验证
- `paramsSerializer` - Axios 原生参数序列化
- 等等...

## 最佳实践

### 1. 统一认证管理

```typescript
// src/api/client-config.ts
export const createClientConfig = () => {
  return {
    auth: async (auth) => {
      // 根据不同的认证类型返回不同的 token
      if (auth.type === 'http' && auth.scheme === 'bearer') {
        const token = await getAccessToken();
        return token;
      }
      if (auth.type === 'apiKey' && auth.in === 'header') {
        return process.env.API_KEY;
      }
    },
  };
};
```

### 2. 统一错误处理

```typescript
export const createClientConfig = () => {
  return {
    responseValidator: async (data: any) => {
      // 统一的响应格式验证
      if (!data || typeof data !== 'object') {
        throw new Error('Invalid response format');
      }

      // 业务错误码处理
      if (data.code !== 0) {
        const error = new Error(data.message || '请求失败');
        error.code = data.code;
        throw error;
      }

      // 返回实际数据
      return data.data;
    },

    throwOnError: false, // 不抛出 HTTP 错误
  };
};

// 业务使用
const result = await getUserById({ path: { id: '123' } });
if (result.error) {
  // 处理错误
  console.error(result.error);
} else {
  // 使用数据
  console.log(result.data);
}
```

### 3. 环境配置

```typescript
// src/api/client-config.ts
const getBaseURL = () => {
  if (process.env.NODE_ENV === 'production') {
    return 'https://api.production.com';
  }
  if (process.env.NODE_ENV === 'staging') {
    return 'https://api.staging.com';
  }
  return 'http://localhost:3000';
};

export const createClientConfig = () => {
  return {
    baseURL: getBaseURL(),
    headers: {
      'X-Environment': process.env.NODE_ENV,
    },
  };
};
```

### 4. 请求追踪

```typescript
export const createClientConfig = () => {
  return {
    headers: {
      'X-Request-ID': () => generateUUID(), // 每次请求生成新的 ID
      'X-Session-ID': getSessionId(),       // 会话级别的 ID
    },
  };
};
```

### 5. 数据转换

```typescript
// 转换日期字符串
const transformDates = (data: any): any => {
  if (!data || typeof data !== 'object') return data;

  const result = Array.isArray(data) ? [] : {};
  for (const [key, value] of Object.entries(data)) {
    if (typeof value === 'string' && /^\d{4}-\d{2}-\d{2}T/.test(value)) {
      result[key] = new Date(value);
    } else if (typeof value === 'object') {
      result[key] = transformDates(value);
    } else {
      result[key] = value;
    }
  }
  return result;
};

export const createClientConfig = () => {
  return {
    responseTransformer: async (data: any) => {
      return transformDates(data);
    },
  };
};
```

## 高级用法

### 访问客户端实例

生成的 SDK 导出了全局客户端实例，可以访问底层 axios 实例：

```typescript
import { client } from './generated/sdk';

// 访问 axios 实例，添加拦截器
client.instance.interceptors.request.use(
  (config) => {
    console.log('Request:', config.url);
    return config;
  }
);

client.instance.interceptors.response.use(
  (response) => {
    console.log('Response:', response.status);
    return response;
  },
  (error) => {
    console.error('Error:', error.message);
    return Promise.reject(error);
  }
);
```

### 动态更新配置

虽然推荐使用 `runtimeConfigPath`，但也可以在运行时动态更新配置：

```typescript
import { client } from './generated/sdk';

// 用户登录后更新 token
function onUserLogin(token: string) {
  client.setConfig({
    headers: {
      Authorization: `Bearer ${token}`,
    },
  });
}

// 用户登出后清除 token
function onUserLogout() {
  client.setConfig({
    headers: {
      Authorization: null, // null 值会删除该 header
    },
  });
}
```

### 自定义生成的客户端代码

生成的客户端代码会被复制到输出目录，可以直接修改（不推荐，升级时会被覆盖）：

```typescript
// 生成的 client.ts 文件
export const createClient = (config: Config = {}): Client => {
  // 在这里添加自定义逻辑
  // 注意：这些修改会在重新生成时丢失
};
```

## 工作原理

1. **代码生成**: `@hey-api/openapi-ts` 根据 OpenAPI 规范生成客户端 SDK
2. **客户端复制**: 插件将自定义的客户端实现文件复制到生成的 SDK 目录中
3. **运行时配置**: 如果指定了 `runtimeConfigPath`，客户端初始化时会调用 `createClientConfig()` 函数
4. **全局单例**: 所有生成的 API 函数共享同一个客户端实例
5. **配置合并**: 每次请求时，会合并全局配置和请求级配置

## 与 @hey-api/client-axios 的区别

| 特性 | @hey-api/client-axios | @lark-apaas/hey-api-custom-client-axios |
|------|----------------------|----------------------------------------|
| **设计理念** | 灵活多实例 | 单例模式，统一管理 |
| **源码控制** | NPM 包，不可修改 | 源码复制到项目，可修改 |
| **配置方式** | 运行时动态配置 | 推荐使用 runtimeConfigPath |
| **适用场景** | 需要多个独立客户端 | 单一应用，统一配置 |
| **业务感知** | 业务需要关心认证等逻辑 | 业务无感知，只传业务参数 |

## FAQ

### Q: runtimeConfigPath 的路径是相对于哪里的？

A: 相对于生成的 SDK 输出目录。例如：

```typescript
// 代码生成配置
createClient({
  output: { path: './src/generated/sdk' },
  plugins: [
    customClientAxiosPlugin({
      runtimeConfigPath: '../api/client-config.ts', // 相对于 ./src/generated/sdk
    }),
  ],
});

// 实际路径：./src/api/client-config.ts
```

### Q: 如何在不同环境使用不同的配置？

A: 在 `createClientConfig` 中读取环境变量：

```typescript
export const createClientConfig = () => {
  return {
    baseURL: process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3000',
    headers: {
      'X-Environment': process.env.NODE_ENV,
    },
  };
};
```

### Q: 生成的代码可以修改吗？

A: 可以但不推荐。生成的代码在重新生成时会被覆盖。推荐通过 `runtimeConfigPath` 配置逻辑，或者使用 axios 拦截器。

### Q: 如何处理多个不同的 API？

A: 推荐为每个 API 生成独立的 SDK，使用不同的配置文件：

```typescript
// API 1
createClient({
  input: './openapi-1.json',
  output: { path: './src/generated/api1' },
  plugins: [
    customClientAxiosPlugin({
      runtimeConfigPath: '../config/api1-config.ts',
    }),
  ],
});

// API 2
createClient({
  input: './openapi-2.json',
  output: { path: './src/generated/api2' },
  plugins: [
    customClientAxiosPlugin({
      runtimeConfigPath: '../config/api2-config.ts',
    }),
  ],
});
```

### Q: 支持哪些认证方式？

A: 支持所有 OpenAPI 规范定义的认证方式：

- HTTP 认证（Bearer、Basic）
- API Key（header、query、cookie）
- OAuth 2.0
- OpenID Connect

通过 `auth` 配置函数统一处理：

```typescript
export const createClientConfig = () => {
  return {
    auth: async (auth) => {
      if (auth.type === 'http' && auth.scheme === 'bearer') {
        return await getBearerToken();
      }
      if (auth.type === 'apiKey') {
        return process.env.API_KEY;
      }
    },
  };
};
```

## License

MIT