@lark-apaas/hey-api-custom-client-axios
v1.0.4
Published
Custom axios client plugin for @hey-api/openapi-ts
Downloads
663
Readme
Custom Axios Client Plugin
基于 @hey-api/client-axios 的自定义 axios 客户端插件,用于 @hey-api/openapi-ts 代码生成。
设计理念
单例模式:一个应用使用一个全局 axios 客户端实例,所有 API 请求基于此实例。通过配置文件统一封装认证、错误处理等必备逻辑,业务代码无需感知这些细节,只需传递业务相关参数。
安装
npm install @lark-apaas/hey-api-custom-client-axios
# or
yarn add @lark-apaas/hey-api-custom-client-axios快速开始
1. 配置插件(代码生成时)
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,封装应用级的必备逻辑:
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 后,业务代码只需传递业务参数:
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 目录)
- 推荐使用: 通过此选项将全局配置逻辑集中管理
customClientAxiosPlugin({
runtimeConfigPath: './src/api/client-config.ts',
})throwOnError
- 类型:
boolean - 默认值:
false - 说明: 请求失败时是否抛出异常。设为
false时,错误会作为返回对象的error字段返回
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. 统一认证管理
// 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. 统一错误处理
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. 环境配置
// 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. 请求追踪
export const createClientConfig = () => {
return {
headers: {
'X-Request-ID': () => generateUUID(), // 每次请求生成新的 ID
'X-Session-ID': getSessionId(), // 会话级别的 ID
},
};
};5. 数据转换
// 转换日期字符串
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 实例:
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,但也可以在运行时动态更新配置:
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
},
});
}自定义生成的客户端代码
生成的客户端代码会被复制到输出目录,可以直接修改(不推荐,升级时会被覆盖):
// 生成的 client.ts 文件
export const createClient = (config: Config = {}): Client => {
// 在这里添加自定义逻辑
// 注意:这些修改会在重新生成时丢失
};工作原理
- 代码生成:
@hey-api/openapi-ts根据 OpenAPI 规范生成客户端 SDK - 客户端复制: 插件将自定义的客户端实现文件复制到生成的 SDK 目录中
- 运行时配置: 如果指定了
runtimeConfigPath,客户端初始化时会调用createClientConfig()函数 - 全局单例: 所有生成的 API 函数共享同一个客户端实例
- 配置合并: 每次请求时,会合并全局配置和请求级配置
与 @hey-api/client-axios 的区别
| 特性 | @hey-api/client-axios | @lark-apaas/hey-api-custom-client-axios | |------|----------------------|----------------------------------------| | 设计理念 | 灵活多实例 | 单例模式,统一管理 | | 源码控制 | NPM 包,不可修改 | 源码复制到项目,可修改 | | 配置方式 | 运行时动态配置 | 推荐使用 runtimeConfigPath | | 适用场景 | 需要多个独立客户端 | 单一应用,统一配置 | | 业务感知 | 业务需要关心认证等逻辑 | 业务无感知,只传业务参数 |
FAQ
Q: runtimeConfigPath 的路径是相对于哪里的?
A: 相对于生成的 SDK 输出目录。例如:
// 代码生成配置
createClient({
output: { path: './src/generated/sdk' },
plugins: [
customClientAxiosPlugin({
runtimeConfigPath: '../api/client-config.ts', // 相对于 ./src/generated/sdk
}),
],
});
// 实际路径:./src/api/client-config.tsQ: 如何在不同环境使用不同的配置?
A: 在 createClientConfig 中读取环境变量:
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,使用不同的配置文件:
// 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 配置函数统一处理:
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
