Node.js + TypeScript 开发健壮的淘宝商品 API SDK
在电商数据服务开发中,可靠的 API SDK 是连接应用与平台的重要桥梁。本文将详细介绍如何使用 Node.js 和 TypeScript 开发一个健壮的淘宝商品 API SDK,实现类型安全、错误处理、请求签名等核心功能,高效对接淘宝平台。
一、淘宝平台准备工作
在开始开发 SDK 之前,需要完成淘宝平台的相关准备工作:
- 注册开发者账号:访问注册账号并完成实名认证
- 创建应用:获取 Api Key 和 Api Secret调用api唯一凭证
- 申请 API 权限:为应用申请商品相关 API 的调用权限,如taobao.item.get、taobao.items.search等
- 了解 API 文档:熟悉淘宝 API 的请求格式、参数要求、返回结果及错误码
二、SDK 核心设计思路
一个健壮的淘宝商品 API SDK 应具备以下特性:
- 类型安全:使用 TypeScript 定义请求参数和返回结果的类型
- 签名机制:实现淘宝 API 要求的签名算法
- 错误处理:统一的错误处理机制,包含网络错误和 API 错误
- 请求控制:支持超时设置、重试机制和请求节流
- 可扩展性:易于添加新的 API 方法和扩展功能
- 日志记录:记录关键操作日志,便于调试和问题排查
三、项目初始化与依赖安装
首先创建项目并安装必要的依赖:
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配置,确保以下选项正确设置:
{"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. 类型定义
首先定义核心类型,确保类型安全:
// 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; // 商品IDtitle: string; // 商品标题pic_url: string; // 商品主图price: string; // 商品价格orginal_price: string; // 商品原价sales: number; // 销量seller_id: number; // 卖家IDshop_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; // 请求超时时间,默认5000msretry?: number; // 重试次数,默认1次endpoint?: string; // API端点,默认https://eco.taobao.com/router/restlogger?: (message: string) => void; // 日志回调函数
}/*** API请求参数*/
export interface ApiParams {[key: string]: string | number | boolean | undefined;
}
2. 错误处理
实现自定义错误类,统一处理各类错误:
// src/errors.tsexport 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})`;}
}
3. 签名工具
实现淘宝 API 要求的签名算法:
// 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;
}
4. 核心 SDK 类
实现 SDK 的核心功能,包括请求处理、签名生成等:
// 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 并调用相关方法:
// 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为123456await 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 调用频率限制,可以实现请求节流功能:
// 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 中使用节流:
// 在构造函数中添加
import { throttle } from './utils/throttle';// ...this.request = throttle(this.request.bind(this), 1000); // 限制每秒最多1次请求
2. 缓存机制
对于不常变化的数据,可以添加缓存功能:
// 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 具有以下特点:
- 类型安全:使用 TypeScript 定义所有请求和响应类型,提供良好的开发体验
- 健壮可靠:实现了完善的错误处理和重试机制,保证 API 调用的稳定性
- 易于使用:封装了常用的商品 API,提供简洁的接口
- 可扩展性:设计灵活,易于添加新的 API 方法和功能扩展
通过这个 SDK,开发者可以轻松地与淘宝平台进行集成,快速实现商品数据的获取和处理。在实际使用中,还可以根据具体需求进一步扩展 SDK 的功能,如添加更复杂的缓存策略、请求监控等。
最后,建议在使用 SDK 时遵守淘宝开放平台的使用规范,合理控制 API 调用频率,确保数据采集的合法性和稳定性。