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 无法继续
小技巧:默认日志级别设为 INFO。生产环境别开 DEBUG,否则日志量能把你磁盘撑爆。我曾经见过一个项目,DEBUG 日志一天写了 20GB...

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;
}
注意:千万别用 Object.assign 做配置合并!它只做浅拷贝,嵌套对象会被共享引用。我曾经因为这个 bug 排查了整整两天...

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);
    }
  }
}
避坑指南:我曾经犯过一个错误——对所有错误都重试。结果用户配置错了,SDK 还在那傻傻地重试了 5 次,每次等 30 秒。用户气得直接投诉。记住:只对可恢复的错误重试,比如网络超时、服务端 5xx。4xx 的错误别重试,那是用户的问题。

4.5 模块关系与数据流

这四个模块不是孤立的。它们之间有清晰的协作关系。下面这张图展示了核心模块的交互流程:

SDK 核心模块关系图 SDK 核心模块协作流程 用户配置 配置管理模块 SDK 初始化 业务请求 重试机制 网络请求 日志模块 错误处理模块 日志贯穿整个流程 错误处理捕获异常 重试机制自动恢复

从这张图可以看出:日志模块贯穿整个流程,任何操作都会记录日志;错误处理模块在业务请求和重试环节捕获异常;配置管理在初始化阶段就完成校验,避免运行时出错。

4.6 实战建议

最后,分享几个我在实际项目中积累的经验:

  • 日志要带上下文:每条日志最好包含请求 ID、用户 ID 等信息,方便排查问题
  • 配置要有默认值:让 SDK 开箱即用,用户只需要配置最关键的几个参数
  • 错误信息要友好:别抛技术术语,用用户能看懂的话描述问题
  • 重试要有限度:设置最大重试次数和超时时间,别让用户无限等待
核心思想:好的 SDK 不是功能有多强,而是用户用起来有多省心。这四个模块,就是让用户省心的基础。