Node.js + TypeScript 开发健壮的淘宝商品 API SDK

在电商数据服务开发中,可靠的 API SDK 是连接应用与平台的重要桥梁。本文将详细介绍如何使用 Node.js 和 TypeScript 开发一个健壮的淘宝商品 API SDK,实现类型安全、错误处理、请求签名等核心功能,高效对接淘宝平台。​

一、淘宝平台准备工作​

在开始开发 SDK 之前,需要完成淘宝平台的相关准备工作:​

  1. 注册开发者账号:访问注册账号并完成实名认证
  2. 创建应用:获取 Api Key 和 Api Secret调用api唯一凭证
  3. 申请 API 权限:为应用申请商品相关 API 的调用权限,如taobao.item.get、taobao.items.search等
  4. 了解 API 文档:熟悉淘宝 API 的请求格式、参数要求、返回结果及错误码

二、SDK 核心设计思路​

一个健壮的淘宝商品 API SDK 应具备以下特性:​

  • 类型安全:使用 TypeScript 定义请求参数和返回结果的类型
  • 签名机制:实现淘宝 API 要求的签名算法
  • 错误处理:统一的错误处理机制,包含网络错误和 API 错误
  • 请求控制:支持超时设置、重试机制和请求节流
  • 可扩展性:易于添加新的 API 方法和扩展功能
  • 日志记录:记录关键操作日志,便于调试和问题排查

三、项目初始化与依赖安装​

首先创建项目并安装必要的依赖:

bash 复制代码
mkdir taobao-api-sdk && cd taobao-api-sdk
npm init -y
npm install axios crypto-js qs
npm install -D typescript @types/node @types/crypto-js @types/qs ts-node
npx tsc --init

修改tsconfig.json配置,确保以下选项正确设置:

json 复制代码
{
  "compilerOptions": {
    "target": "ES2020",
    "module": "CommonJS",
    "outDir": "./dist",
    "rootDir": "./src",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "**/*.test.ts"]
}

四、核心代码实现​

  1. 类型定义

首先定义核心类型,确保类型安全:

css 复制代码
// src/types/index.ts

/**
 * 淘宝API公共响应结构
 */
export interface TaobaoResponse<T = any> {
  error_response?: {
    code: number;
    msg: string;
    sub_code?: string;
    sub_msg?: string;
  };
  [key: string]: T | undefined;
}

/**
 * 商品基本信息
 */
export interface ProductBase {
  num_iid: number; // 商品ID
  title: string; // 商品标题
  pic_url: string; // 商品主图
  price: string; // 商品价格
  orginal_price: string; // 商品原价
  sales: number; // 销量
  seller_id: number; // 卖家ID
  shop_title: string; // 店铺名称
}

/**
 * 商品详情信息
 */
export interface ProductDetail extends ProductBase {
  desc: string; // 商品描述
  item_imgs: { url: string }[]; // 商品图片
  props_name: string; // 商品属性名称
  props: { name: string; value: string }[]; // 商品属性
  skus: {
    sku_id: number;
    price: string;
    properties: string;
    properties_name: string;
    stock: number;
  }[]; // 商品规格
  stock: number; // 库存
  post_fee: string; // 运费
}

/**
 * 商品搜索结果
 */
export interface ProductSearchResult {
  items: {
    item: ProductBase[];
  };
  total_results: number;
  page_no: number;
  page_size: number;
}

/**
 * SDK配置选项
 */
export interface TaobaoSDKOptions {
  appKey: string;
  appSecret: string;
  timeout?: number; // 请求超时时间,默认5000ms
  retry?: number; // 重试次数,默认1次
  endpoint?: string; // API端点,默认https://eco.taobao.com/router/rest
  logger?: (message: string) => void; // 日志回调函数
}

/**
 * API请求参数
 */
export interface ApiParams {
  [key: string]: string | number | boolean | undefined;
}
  1. 错误处理

实现自定义错误类,统一处理各类错误:

typescript 复制代码
// src/errors.ts

export enum TaobaoErrorType {
  NETWORK_ERROR = 'NETWORK_ERROR',
  API_ERROR = 'API_ERROR',
  PARAM_ERROR = 'PARAM_ERROR',
  AUTH_ERROR = 'AUTH_ERROR'
}

export class TaobaoError extends Error {
  type: TaobaoErrorType;
  code?: number | string;
  details?: any;

  constructor(
    message: string,
    type: TaobaoErrorType,
    code?: number | string,
    details?: any
  ) {
    super(message);
    this.name = 'TaobaoError';
    this.type = type;
    this.code = code;
    this.details = details;
  }

  toString(): string {
    return `[${this.name}] ${this.type}: ${this.message} (code: ${this.code})`;
  }
}
  1. 签名工具

实现淘宝 API 要求的签名算法:

typescript 复制代码
// src/utils/sign.ts
import crypto from 'crypto-js';
import qs from 'qs';
import { ApiParams } from '../types';

/**
 * 生成淘宝API签名
 * 签名规则:https://open.taobao.com/doc.htm?docId=101617&docType=1&source=search
 */
export function generateSign(
  params: ApiParams,
  appSecret: string
): string {
  // 1. 去除空值参数
  const filteredParams = Object.entries(params).reduce(
    (obj, [key, value]) => {
      if (value !== undefined && value !== null && value !== '') {
        obj[key] = value;
      }
      return obj;
    },
    {} as Record<string, string | number | boolean>
  );

  // 2. 按键名ASCII排序
  const sortedParams = Object.keys(filteredParams).sort().reduce(
    (obj, key) => {
      obj[key] = filteredParams[key];
      return obj;
    },
    {} as Record<string, string | number | boolean>
  );

  // 3. 拼接为key=value&key=value形式
  const paramString = qs.stringify(sortedParams, { encode: false });

  // 4. 拼接appSecret,进行HMAC-SHA1加密
  const signString = appSecret + paramString + appSecret;
  const sign = crypto.HmacSHA1(signString, appSecret).toString().toUpperCase();

  return sign;
}
  1. 核心 SDK 类

实现 SDK 的核心功能,包括请求处理、签名生成等:

typescript 复制代码
// src/index.ts
import axios, { AxiosError, AxiosRequestConfig } from 'axios';
import { generateSign } from './utils/sign';
import {
  TaobaoSDKOptions,
  ApiParams,
  TaobaoResponse,
  ProductDetail,
  ProductSearchResult
} from './types';
import { TaobaoError, TaobaoErrorType } from './errors';

export class TaobaoSDK {
  private appKey: string;
  private appSecret: string;
  private endpoint: string;
  private timeout: number;
  private retry: number;
  private logger?: (message: string) => void;

  constructor(options: TaobaoSDKOptions) {
    if (!options.appKey || !options.appSecret) {
      throw new TaobaoError(
        'appKey和appSecret不能为空',
        TaobaoErrorType.PARAM_ERROR
      );
    }

    this.appKey = options.appKey;
    this.appSecret = options.appSecret;
    this.endpoint = options.endpoint || 'https://eco.taobao.com/router/rest';
    this.timeout = options.timeout || 5000;
    this.retry = options.retry !== undefined ? options.retry : 1;
    this.logger = options.logger;

    this.log('Taobao SDK 初始化成功');
  }

  /**
   * 日志记录
   */
  private log(message: string): void {
    if (this.logger) {
      this.logger(`[TaobaoSDK] ${message}`);
    }
  }

  /**
   * 生成公共参数
   */
  private getCommonParams(method: string): ApiParams {
    return {
      app_key: this.appKey,
      method: method,
      format: 'json',
      v: '2.0',
      sign_method: 'hmac',
      timestamp: new Date().toISOString().replace(/T/, ' ').replace(/..+/, ''),
      partner_id: 'top-sdk-nodejs',
      simplify: true
    };
  }

  /**
   * 发送API请求
   */
  private async request<T>(
    method: string,
    params: ApiParams = {},
    retryCount = 0
  ): Promise<T> {
    try {
      // 合并公共参数和接口参数
      const requestParams = {
        ...this.getCommonParams(method),
        ...params
      };

      // 生成签名
      const sign = generateSign(requestParams, this.appSecret);

      // 构建最终请求参数
      const finalParams = {
        ...requestParams,
        sign
      };

      this.log(`调用API: ${method}, 参数: ${JSON.stringify(finalParams)}`);

      // 发送请求
      const axiosConfig: AxiosRequestConfig = {
        url: this.endpoint,
        method: 'get',
        params: finalParams,
        timeout: this.timeout
      };

      const response = await axios(axiosConfig);
      const data: TaobaoResponse<T> = response.data;

      // 处理API错误
      if (data.error_response) {
        const error = data.error_response;
        this.log(`API错误: ${error.code} - ${error.msg}`);

        // 特定错误码不重试
        const noRetryCodes = [400, 401, 403, 404, 501];
        if (
          noRetryCodes.includes(error.code) ||
          retryCount >= this.retry
        ) {
          throw new TaobaoError(
            error.msg || error.sub_msg || 'API请求失败',
            error.code === 401 || error.code === 403
              ? TaobaoErrorType.AUTH_ERROR
              : TaobaoErrorType.API_ERROR,
            error.code,
            error
          );
        }

        // 重试
        this.log(`API请求失败,将进行第${retryCount + 1}次重试`);
        return this.request<T>(method, params, retryCount + 1);
      }

      // 提取业务数据
      const resultKey = Object.keys(data).find(
        (key) => key !== 'error_response'
      );
      
      if (!resultKey) {
        throw new TaobaoError('API返回格式异常', TaobaoErrorType.API_ERROR);
      }

      return data[resultKey] as T;
    } catch (error) {
      // 处理网络错误
      if (error instanceof AxiosError) {
        if (retryCount >= this.retry) {
          throw new TaobaoError(
            `网络请求失败: ${error.message}`,
            TaobaoErrorType.NETWORK_ERROR,
            error.code
          );
        }

        // 网络错误重试
        this.log(`网络请求失败,将进行第${retryCount + 1}次重试: ${error.message}`);
        return this.request<T>(method, params, retryCount + 1);
      }

      // 抛出其他错误
      if (error instanceof TaobaoError) {
        throw error;
      }

      throw new TaobaoError(
        `未知错误: ${(error as Error).message}`,
        TaobaoErrorType.NETWORK_ERROR
      );
    }
  }

  /**
   * 获取商品详情
   * @param numIid 商品ID
   */
  async getItemDetail(numIid: number): Promise<ProductDetail> {
    if (!numIid || typeof numIid !== 'number') {
      throw new TaobaoError(
        '商品ID必须为有效的数字',
        TaobaoErrorType.PARAM_ERROR
      );
    }

    return this.request<ProductDetail>('taobao.item.get', {
      num_iid: numIid,
      fields: 'num_iid,title,pic_url,price,orginal_price,sales,seller_id,shop_title,desc,item_imgs,props_name,props,skus,stock,post_fee'
    });
  }

  /**
   * 搜索商品
   * @param keyword 搜索关键词
   * @param page 页码,默认1
   * @param pageSize 每页数量,默认40
   */
  async searchItems(
    keyword: string,
    page = 1,
    pageSize = 40
  ): Promise<ProductSearchResult> {
    if (!keyword || typeof keyword !== 'string' || keyword.trim() === '') {
      throw new TaobaoError(
        '搜索关键词不能为空',
        TaobaoErrorType.PARAM_ERROR
      );
    }

    if (page < 1) page = 1;
    if (pageSize < 1 || pageSize > 100) pageSize = 40;

    return this.request<ProductSearchResult>('taobao.items.search', {
      q: keyword,
      page_no: page,
      page_size: pageSize,
      fields: 'num_iid,title,pic_url,price,orginal_price,sales,seller_id,shop_title'
    });
  }

  /**
   * 扩展方法:调用其他API
   * @param method API方法名
   * @param params API参数
   */
  async invokeApi<T>(method: string, params: ApiParams = {}): Promise<T> {
    if (!method || typeof method !== 'string' || method.trim() === '') {
      throw new TaobaoError(
        'API方法名不能为空',
        TaobaoErrorType.PARAM_ERROR
      );
    }

    return this.request<T>(method, params);
  }
}

export { TaobaoError, TaobaoErrorType } from './errors';
export * from './types';

五、使用示例​

下面是 SDK 的使用示例,展示如何初始化 SDK 并调用相关方法:

javascript 复制代码
// example.ts
import { TaobaoSDK, TaobaoError } from './src';

// 初始化SDK
const taobaoSDK = new TaobaoSDK({
  appKey: 'your_app_key',
  appSecret: 'your_app_secret',
  timeout: 10000,
  retry: 2,
  logger: (message) => {
    console.log(message);
  }
});

// 搜索商品示例
async function searchProducts() {
  try {
    const result = await taobaoSDK.searchItems('手机', 1, 20);
    console.log(`搜索到${result.total_results}个商品`);
    console.log('商品列表:', result.items.item.map(item => ({
      id: item.num_iid,
      title: item.title,
      price: item.price,
      sales: item.sales
    })));
  } catch (error) {
    if (error instanceof TaobaoError) {
      console.error(`搜索商品失败: ${error.message}, 类型: ${error.type}, 代码: ${error.code}`);
    } else {
      console.error('搜索商品发生未知错误:', error);
    }
  }
}

// 获取商品详情示例
async function getProductDetail(numIid: number) {
  try {
    const detail = await taobaoSDK.getItemDetail(numIid);
    console.log('商品详情:', {
      id: detail.num_iid,
      title: detail.title,
      price: detail.price,
      stock: detail.stock,
      shop: detail.shop_title,
      properties: detail.props.map(prop => `${prop.name}: ${prop.value}`).join('; ')
    });
  } catch (error) {
    if (error instanceof TaobaoError) {
      console.error(`获取商品详情失败: ${error.message}, 类型: ${error.type}, 代码: ${error.code}`);
    } else {
      console.error('获取商品详情发生未知错误:', error);
    }
  }
}

// 调用示例
async function runExamples() {
  await searchProducts();
  // 假设搜索结果中第一个商品的ID为123456
  await getProductDetail(123456);
  
  // 调用其他API示例
  try {
    const categories = await taobaoSDK.invokeApi('taobao.itemcats.get', {
      cid: 0,
      fields: 'cid,name,is_parent'
    });
    console.log('商品分类:', categories);
  } catch (error) {
    console.error('获取商品分类失败:', error);
  }
}

runExamples();

六、高级特性与优化​

  1. 请求节流

为避免超过 API 调用频率限制,可以实现请求节流功能:

ini 复制代码
// src/utils/throttle.ts
export function throttle<T extends (...args: any[]) => Promise<any>>(
  func: T,
  limit: number
): T {
  let lastCall = 0;
  let pendingPromise: Promise<any> | null = null;

  return (async (...args: Parameters<T>): Promise<ReturnType<T>> => {
    const now = Date.now();
    const elapsed = now - lastCall;

    if (elapsed >= limit) {
      lastCall = now;
      return func(...args);
    }

    // 等待上一个请求完成
    if (pendingPromise) {
      await pendingPromise;
    }

    // 再次检查时间
    const now2 = Date.now();
    if (now2 - lastCall >= limit) {
      lastCall = now2;
      return func(...args);
    }

    // 延迟执行
    return new Promise((resolve) => {
      setTimeout(async () => {
        lastCall = Date.now();
        pendingPromise = func(...args);
        const result = await pendingPromise;
        pendingPromise = null;
        resolve(result);
      }, limit - (now2 - lastCall));
    });
  }) as T;
}

在 SDK 中使用节流:

kotlin 复制代码
// 在构造函数中添加
import { throttle } from './utils/throttle';

// ...

this.request = throttle(this.request.bind(this), 1000); // 限制每秒最多1次请求
  1. 缓存机制

对于不常变化的数据,可以添加缓存功能:

kotlin 复制代码
// src/utils/cache.ts
export class Cache {
  private data: Map<string, { value: any; expiry: number }>;

  constructor() {
    this.data = new Map();
    // 定期清理过期缓存
    setInterval(() => this.cleanup(), 60000);
  }

  get<T>(key: string): T | null {
    const item = this.data.get(key);
    if (!item) return null;
    if (item.expiry < Date.now()) {
      this.data.delete(key);
      return null;
    }
    return item.value as T;
  }

  set<T>(key: string, value: T, ttl: number = 300000): void {
    this.data.set(key, {
      value,
      expiry: Date.now() + ttl
    });
  }

  delete(key: string): void {
    this.data.delete(key);
  }

  clear(): void {
    this.data.clear();
  }

  private cleanup(): void {
    const now = Date.now();
    for (const [key, item] of this.data) {
      if (item.expiry < now) {
        this.data.delete(key);
      }
    }
  }
}

七、总结​

本文介绍了如何使用 Node.js 和 TypeScript 开发一个健壮的淘宝商品 API SDK。该 SDK 具有以下特点:​

  1. 类型安全:使用 TypeScript 定义所有请求和响应类型,提供良好的开发体验
  2. 健壮可靠:实现了完善的错误处理和重试机制,保证 API 调用的稳定性
  3. 易于使用:封装了常用的商品 API,提供简洁的接口
  4. 可扩展性:设计灵活,易于添加新的 API 方法和功能扩展

通过这个 SDK,开发者可以轻松地与淘宝平台进行集成,快速实现商品数据的获取和处理。在实际使用中,还可以根据具体需求进一步扩展 SDK 的功能,如添加更复杂的缓存策略、请求监控等。​

最后,建议在使用 SDK 时遵守淘宝开放平台的使用规范,合理控制 API 调用频率,确保数据采集的合法性和稳定性。

相关推荐
阿卡不卡3 分钟前
基于多场景的通用单位转换功能实现
前端·javascript
♡喜欢做梦14 分钟前
jQuery 从入门到实践:基础语法、事件与元素操作全解析
前端·javascript·jquery
flyliu18 分钟前
前端权限控制应该怎么做
前端·前端工程化
酸菜土狗22 分钟前
gitignor配置禁止上传文件目录到 Git
前端·javascript
小猪猪屁22 分钟前
告别依赖地狱!Monorepo 打造高效 Vue3 项目体系
前端·前端框架
前端老爷更车22 分钟前
深度解析VUE3 Composition API 中的setup 函数
前端
王六岁23 分钟前
JavaScript 运算符的那些"坑"与技巧
前端·javascript
酸菜土狗24 分钟前
nvm常用命令行操作
前端·javascript
Danny_FD25 分钟前
解决 null byte is not allowed in input:PNPM/npm 下载报错的编码陷阱
前端·程序员
用户617203501162526 分钟前
Cesium 中的 Primitive 深入理解
前端