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 调用频率,确保数据采集的合法性和稳定性。

相关推荐
xw520 分钟前
uni-app项目跑APP报useStore报错
前端·uni-app
!win !24 分钟前
uni-app项目跑APP报useStore报错
前端·uni-app
拾光拾趣录26 分钟前
Flexbox 布局:从“垂直居中都搞不定”到写出响应式万能布局
前端·css
huxihua20061 小时前
各种前端框架界面
前端
拾光拾趣录1 小时前
GET/POST 的区别:从“为什么登录请求不能用 GET”说起
前端·网络协议
Carlos_sam1 小时前
OpenLayers:ol-wind之渲染风场图全解析
前端·javascript
拾光拾趣录2 小时前
闭包:从“变量怎么还没死”到写出真正健壮的模块
前端·javascript
拾光拾趣录2 小时前
for..in 和 Object.keys 的区别:从“遍历对象属性的坑”说起
前端·javascript
OpenTiny社区2 小时前
把 SearchBox 塞进项目,搜索转化率怒涨 400%?
前端·vue.js·github
编程猪猪侠3 小时前
Tailwind CSS 自定义工具类与主题配置指南
前端·css