4. SDK技术选型与架构
SDK 的核心模块,说白了就是地基。地基不稳,上层功能再花哨也白搭。今天咱们聊聊四个最基础的模块:日志、配置管理、错误处理和重试机制。这四个东西,我几乎在每个 SDK 项目里都会重新打磨一遍。
4.1 日志模块:不只是 console.log
很多新手写 SDK,日志就扔个 console.log。这在开发阶段没问题,但到了生产环境,这就是灾难。你想想看,用户不可能去翻你的控制台输出。
我个人习惯把日志模块设计成可插拔的。SDK 内部只定义日志接口,具体实现让用户自己选。这样既灵活,又不会强绑定某个日志库。
4.1.1 日志级别设计
日志级别要清晰,别搞太复杂。我一般用这六级:
| 级别 | 数值 | 使用场景 |
|---|---|---|
| TRACE | 0 | 最细粒度的调试信息,一般不用 |
| DEBUG | 1 | 开发调试,排查问题 |
| INFO | 2 | 关键业务流程节点,比如初始化完成 |
| WARN | 3 | 潜在问题,不影响当前运行 |
| ERROR | 4 | 可恢复的错误,比如网络超时 |
| FATAL | 5 | 不可恢复的致命错误,SDK 无法继续 |
4.1.2 日志接口设计
接口要简单,但功能要完整。我通常这样设计:
// 日志接口定义
interface Logger {
trace(msg: string, ...args: any[]): void;
debug(msg: string, ...args: any[]): void;
info(msg: string, ...args: any[]): void;
warn(msg: string, ...args: any[]): void;
error(msg: string, ...args: any[]): void;
fatal(msg: string, ...args: any[]): void;
// 设置日志级别
setLevel(level: LogLevel): void;
// 获取当前级别
getLevel(): LogLevel;
}
// 默认实现:控制台输出
class ConsoleLogger implements Logger {
private level: LogLevel = LogLevel.INFO;
info(msg: string, ...args: any[]): void {
if (this.level <= LogLevel.INFO) {
console.log(`[INFO] ${msg}`, ...args);
}
}
// ... 其他方法类似
}
4.2 配置管理:让 SDK 适应各种环境
配置管理这块,我踩过不少坑。最典型的就是:配置项太多,用户根本不知道哪些是必填的。
我的做法是分层设计。把配置分成三层:默认配置、环境配置、用户配置。优先级从低到高,后面的覆盖前面的。
4.2.1 配置合并策略
合并配置时,要注意深拷贝。JavaScript 的对象引用问题,坑过很多人。
// 配置合并示例
function mergeConfig(defaults: Config, userConfig?: Partial<Config>): Config {
// 深拷贝,避免引用污染
const result = JSON.parse(JSON.stringify(defaults));
if (!userConfig) return result;
// 只合并用户提供的字段
for (const key of Object.keys(userConfig)) {
if (userConfig[key] !== undefined) {
result[key] = userConfig[key];
}
}
return result;
}
4.2.2 配置校验
配置校验要尽早做。我习惯在 SDK 初始化时就校验所有必填项,而不是等到运行时才报错。
| 校验类型 | 说明 | 示例 |
|---|---|---|
| 必填校验 | 检查必要参数是否存在 | apiKey 不能为空 |
| 类型校验 | 检查参数类型是否正确 | timeout 必须是数字 |
| 范围校验 | 检查参数是否在合理范围内 | timeout 必须在 1000-30000ms 之间 |
| 格式校验 | 检查参数格式是否符合规范 | URL 必须以 https:// 开头 |
4.3 错误处理:优雅地失败
错误处理是 SDK 的门面。用户对你的第一印象,往往来自你抛出的错误信息。
我见过太多 SDK 直接抛 Error: something went wrong。这种错误信息,用户看了想打人。好的错误处理,应该告诉用户:哪里错了、为什么错、怎么解决。
4.3.1 自定义错误类型
别用通用 Error。定义自己的错误类型,让用户能精确捕获和处理。
// 自定义错误基类
class SDKError extends Error {
constructor(
public code: string, // 错误码
message: string, // 错误描述
public details?: any // 附加信息
) {
super(message);
this.name = 'SDKError';
}
}
// 具体错误类型
class NetworkError extends SDKError {
constructor(message: string, details?: any) {
super('NETWORK_ERROR', message, details);
this.name = 'NetworkError';
}
}
class AuthError extends SDKError {
constructor(message: string, details?: any) {
super('AUTH_ERROR', message, details);
this.name = 'AuthError';
}
}
INVALID_PARAMS、RATE_LIMITED。这样用户查文档时一目了然。
4.3.2 错误处理策略
错误处理要分场景。不是所有错误都需要抛给用户。
- 可恢复错误:比如网络超时,内部重试即可,用户无感知
- 可通知错误:比如配置错误,通过回调通知用户,但不中断流程
- 致命错误:比如认证失败,必须抛异常,让用户处理
4.4 重试机制:让 SDK 更健壮
网络请求总会失败。这不是如果的问题,而是什么时候的问题。重试机制,就是 SDK 的“防弹衣”。
我做过一个统计:加了重试机制后,SDK 的请求成功率从 92% 提升到了 99.7%。效果很明显。
4.4.1 重试策略设计
重试不是简单重复。要讲究策略,否则会加重服务器负担。
| 策略 | 说明 | 适用场景 |
|---|---|---|
| 固定间隔 | 每次等待相同时间 | 简单场景,不推荐 |
| 指数退避 | 等待时间指数增长 | 网络波动,推荐 |
| 随机抖动 | 在退避基础上加随机值 | 高并发场景,防止雪崩 |
// 指数退避 + 随机抖动
function calculateDelay(attempt: number, baseMs: number = 1000): number {
// 指数退避:2^attempt * baseMs
const exponential = Math.pow(2, attempt) * baseMs;
// 随机抖动:±25%
const jitter = exponential * 0.25 * Math.random();
return exponential + jitter;
}
// 重试执行器
async function withRetry<T>(
fn: () => Promise<T>,
options: {
maxRetries?: number;
baseDelay?: number;
retryableErrors?: string[];
} = {}
): Promise<T> {
const maxRetries = options.maxRetries ?? 3;
const baseDelay = options.baseDelay ?? 1000;
for (let attempt = 0; attempt <= maxRetries; attempt++) {
try {
return await fn();
} catch (error) {
if (attempt === maxRetries) throw error;
// 检查是否是可重试的错误
if (!isRetryable(error, options.retryableErrors)) {
throw error;
}
const delay = calculateDelay(attempt, baseDelay);
await sleep(delay);
}
}
}
4.5 模块关系与数据流
这四个模块不是孤立的。它们之间有清晰的协作关系。下面这张图展示了核心模块的交互流程:
从这张图可以看出:日志模块贯穿整个流程,任何操作都会记录日志;错误处理模块在业务请求和重试环节捕获异常;配置管理在初始化阶段就完成校验,避免运行时出错。
4.6 实战建议
最后,分享几个我在实际项目中积累的经验:
- 日志要带上下文:每条日志最好包含请求 ID、用户 ID 等信息,方便排查问题
- 配置要有默认值:让 SDK 开箱即用,用户只需要配置最关键的几个参数
- 错误信息要友好:别抛技术术语,用用户能看懂的话描述问题
- 重试要有限度:设置最大重试次数和超时时间,别让用户无限等待