本实战项目旨在通过TypeScript与Node.js构建一个类型安全的RESTful API客户端,深入掌握TypeScript在后端开发中的核心应用,特别是类型系统、泛型工具与装饰器模式的实践。通过封装统一的API调用层,实现请求/响应类型约束、错误处理与日志记录的标准化,为大型应用提供可靠的接口交互方案。
在开始实现前,需明确API客户端的核心需求,确保覆盖RESTful API交互的关键场景:
技术栈选择需围绕类型安全与开发效率展开:
module: ESNext(支持现代模块系统)与moduleResolution: bundler(优化模块查找逻辑),同时启用strict: true增强类型检查严格性。类型定义是确保API类型安全的基础,需先明确核心类型与接口:
// 基础配置接口:定义API客户端的全局设置
interface ApiConfig {
baseURL: string; // API基础路径,如"https://api.example.com"
timeout: number; // 请求超时时间(毫秒),如5000
}
// HTTP方法类型:限制仅支持RESTful核心方法,避免非法请求
type HttpMethod = 'get' | 'post' | 'put' | 'delete';
// 响应数据通用结构:假设后端返回格式统一为{ code: number; data: T; message?: string }
interface ApiResponse<T> {
code: number; // 业务状态码(如200表示成功,400表示参数错误)
data: T; // 实际响应数据,通过泛型T指定具体类型
message?: string; // 可选消息描述
}
类型安全关键:通过ApiResponse<T>泛型接口统一响应格式,确保业务数据data的类型与调用方声明一致。例如,用户信息接口可声明为ApiResponse<User>,TypeScript将自动校验返回数据是否符合User接口结构。
基于axios封装核心ApiClient类,实现请求发送的底层逻辑,并通过泛型方法确保响应类型安全:
import axios, { AxiosRequestConfig, AxiosError } from 'axios';
class ApiClient {
private instance; // axios实例
constructor(config: ApiConfig) {
// 初始化axios实例,应用基础配置
this.instance = axios.create({
baseURL: config.baseURL,
timeout: config.timeout,
headers: { 'Content-Type': 'application/json' }
});
}
/**
* 泛型请求方法:核心类型安全保障
* @param method HTTP请求方法
* @param url 请求路径(相对于baseURL)
* @param data 请求数据(POST/PUT时使用)
* @returns Promise<T> 响应数据(自动解析ApiResponse<T>中的data字段)
*/
async request<T>(
method: HttpMethod,
url: string,
data?: any
): Promise<T> {
try {
// 构建axios请求配置
const config: AxiosRequestConfig = { method, url };
// 根据方法类型设置数据字段(GET用params,其他用data)
if (method === 'get') {
config.params = data;
} else {
config.data = data;
}
// 发送请求并获取响应,通过泛型约束响应类型
const response = await this.instance.request<ApiResponse<T>>(config);
// 仅返回业务数据部分(剥离code和message)
return response.data.data;
} catch (error) {
// 错误统一抛出,由装饰器处理
throw error;
}
}
}
泛型方法解析:request<T>通过泛型参数T指定响应数据类型,结合ApiResponse<T>接口,实现三层类型保障:
method参数限制为HttpMethod,避免拼写错误(如"get"误写为"GET");instance.request<ApiResponse<T>>确保后端返回符合统一响应格式;response.data.data时自动推断为T类型,调用方无需手动类型转换。利用TypeScript装饰器(Decorator)模式,在不修改ApiClient核心逻辑的前提下,添加请求日志与错误处理能力:
import { RequestMethod } from 'axios';
/**
* 请求日志装饰器:记录请求详情与耗时
*/
function logRequest(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const originalMethod = descriptor.value;
descriptor.value = async function(...args: any[]) {
const [method, url] = args; // 提取方法和URL参数
const startTime = Date.now();
console.log(`[API Request] ${method.toUpperCase()} ${url} - Start`);
try {
const result = await originalMethod.apply(this, args);
const duration = Date.now() - startTime;
console.log(`[API Request] ${method.toUpperCase()} ${url} - Success (${duration}ms)`);
return result;
} catch (error) {
const duration = Date.now() - startTime;
console.error(`[API Request] ${method.toUpperCase()} ${url} - Failed (${duration}ms)`);
throw error;
}
};
return descriptor;
}
/**
* 错误处理装饰器:统一格式化错误信息
*/
function handleError(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const originalMethod = descriptor.value;
descriptor.value = async function(...args: any[]) {
try {
return await originalMethod.apply(this, args);
} catch (error) {
// 区分axios错误与其他错误
if (error instanceof AxiosError) {
const { response } = error;
// 格式化错误信息:包含状态码、路径与响应内容
const errorMsg = `API Error: ${response?.status || 'Network Error'} - ${response?.data?.message || error.message}`;
throw new Error(errorMsg);
} else {
// 非axios错误直接抛出
throw new Error(`Request Error: ${error.message}`);
}
}
};
return descriptor;
}
// 在ApiClient的request方法上应用装饰器
class ApiClient {
// ... 其他代码不变 ...
@logRequest
@handleError
async request<T>(method: HttpMethod, url: string, data?: any): Promise<T> {
// ... 原有实现 ...
}
}
装饰器拦截逻辑:装饰器通过重写request方法实现功能增强,执行流程为:
@handleError后@logRequest),但实际拦截顺序为日志先执行(因装饰器工厂函数执行顺序相反)。基于ApiClient扩展业务接口,以用户模块(userApi.ts)为例,定义具体请求/响应类型并实现接口方法:
// 用户相关类型定义
interface User {
id: number;
name: string;
email: string;
createdAt: string;
}
interface CreateUserRequest {
name: string;
email: string;
password: string;
}
// 用户API客户端,继承基础ApiClient
class UserApi extends ApiClient {
constructor(config: ApiConfig) {
super(config);
}
/**
* 获取用户列表
* @returns 用户数组(类型自动推断为User[])
*/
getUsers() {
return this.request<User[]>('get', '/users');
}
/**
* 创建新用户
* @param data 创建用户请求数据(类型约束为CreateUserRequest)
* @returns 创建成功的用户信息(类型为User)
*/
createUser(data: CreateUserRequest) {
return this.request<User>('post', '/users', data);
}
/**
* 更新用户信息
* @param id 用户ID
* @param data 更新数据(部分User字段)
* @returns 更新后的用户信息
*/
updateUser(id: number, data: Partial<User>) {
return this.request<User>('put', `/users/${id}`, data);
}
/**
* 删除用户
* @param id 用户ID
* @returns 空响应(类型为void)
*/
deleteUser(id: number) {
return this.request<void>('delete', `/users/${id}`);
}
}
业务类型安全:通过为每个接口方法指定具体泛型参数(如getUsers()的User[]),实现:
createUser(data)的data必须符合CreateUserRequest结构,缺失name或email将触发编译错误;userApi.getUsers()时,IDE自动提示返回数组的每个元素具有id、name等User接口字段;User接口(如新增status字段),仅需更新User类型定义,所有调用处将自动获得类型提示。通过Jest结合TypeScript类型断言,验证接口调用的类型约束:
import { UserApi } from './userApi';
// 模拟API配置
const apiConfig = { baseURL: 'https://api.example.com', timeout: 5000 };
const userApi = new UserApi(apiConfig);
describe('UserApi类型安全测试', () => {
it('getUsers应返回User[]类型', async () => {
// 模拟axios响应
jest.spyOn(userApi['instance'], 'request').mockResolvedValue({
data: { code: 200, data: [{ id: 1, name: 'Test', email: 'test@example.com', createdAt: '2023-01-01' }] }
});
const users = await userApi.getUsers();
// 类型断言测试:验证返回值符合User[]结构
expect(users[0]).toHaveProperty('id');
expect(users[0]).toHaveProperty('name');
});
it('createUser应校验请求参数类型', () => {
// @ts-expect-error 故意传入错误参数,验证TypeScript编译错误
userApi.createUser({ name: 'Test' }); // 缺失email和password,应报错
});
});
请求缓存:基于method+url+data生成唯一键,使用Map缓存请求结果,避免重复请求:
private cache = new Map<string, any>();
// 在request方法中添加缓存逻辑
const cacheKey = `${method}-${url}-${JSON.stringify(data)}`;
if (method === 'get' && this.cache.has(cacheKey)) {
return this.cache.get(cacheKey);
}
// 请求成功后存入缓存
this.cache.set(cacheKey, response.data.data);
重试机制:对网络错误或5xx状态码自动重试,通过装饰器实现:
function retry(maxRetries = 3) {
return (target: any, propertyKey: string, descriptor: PropertyDescriptor) => {
const originalMethod = descriptor.value;
descriptor.value = async function(...args: any[]) {
let retries = 0;
while (retries < maxRetries) {
try {
return await originalMethod.apply(this, args);
} catch (error) {
retries++;
if (retries >= maxRetries) throw error;
await new Promise(res => setTimeout(res, 1000 * retries)); // 指数退避
}
}
};
};
}
利用浏览器/Node.js原生的AbortController实现请求取消,允许调用方终止未完成的请求:
// 定义取消令牌类型
type CancelToken = AbortController;
// 增强request方法支持取消令牌
async request<T>(
method: HttpMethod,
url: string,
data?: any,
cancelToken?: CancelToken
): Promise<T> {
try {
const config: AxiosRequestConfig = {
method,
url,
signal: cancelToken?.signal // 传递AbortSignal
};
// ... 其余逻辑不变 ...
} catch (error) {
if (error.name === 'AbortError') {
console.log('Request aborted');
}
throw error;
}
}
// 使用示例
const controller = new AbortController();
userApi.getUsers(controller.signal);
// 取消请求(如组件卸载时)
controller.abort();
通过上述实现,我们构建了一个兼具类型安全、可扩展性与可维护性的RESTful API客户端,充分发挥了TypeScript在类型约束、泛型工具与装饰器模式上的优势,为企业级应用的接口交互提供了标准化解决方案。