1. 为什么需要封装HTTP请求
在ArkTS开发中,直接使用原生HTTP接口会面临几个典型问题。首先,每次请求都需要重复编写基础配置代码,比如设置请求头、处理超时、错误重试等逻辑。其次,不同开发者对错误处理的方式可能不一致,导致项目维护困难。最重要的是,当需要更换底层网络库时,散落在各处的直接调用会让迁移变得异常痛苦。
我在实际项目中见过一个典型反面案例:某个应用中有37处直接使用fetch API的地方,当需要统一添加请求日志时,开发者不得不逐个文件修改。更糟的是,有些请求忘记处理401状态码,导致用户登录过期后出现界面错乱。
封装HTTP请求的核心价值在于:
- 统一处理公共逻辑(认证、错误码、日志)
- 简化业务代码调用方式
- 便于后续基础设施升级
- 提供类型安全的请求/响应接口
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. ArkTS中的HTTP基础能力
2.1 内置fetch API分析
ArkTS基于TypeScript,天然支持Web标准的fetch API。一个基础GET请求如下:
typescript复制fetch('https://api.example.com/data')
.then(response => {
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.json();
})
.then(data => console.log(data))
.catch(error => console.error('Fetch error:', error));
这种写法存在三个明显问题:
- 错误处理与业务逻辑耦合
- 缺乏请求超时控制
- 响应数据需要手动转换
2.2 常见问题排查
根据网络热词中出现的"unexpected status 502"等错误,我们在封装时需要特别注意:
- 502 Bad Gateway:需要实现自动重试机制
- 网络超时:建议默认设置10秒超时
- 请求取消:支持AbortController中断请求
我曾遇到过一个生产环境问题:移动网络下502错误频发,但简单的重试就能成功。后来我们在封装层实现了指数退避重试策略,显著提升了弱网体验。
3. GET请求的工程化封装
3.1 基础封装实现
我们先实现一个带类型支持的GET封装:
typescript复制class HttpClient {
private baseUrl: string;
constructor(baseUrl: string) {
this.baseUrl = baseUrl.endsWith('/') ? baseUrl.slice(0, -1) : baseUrl;
}
async get<T>(endpoint: string, params?: Record<string, string>): Promise<T> {
const url = new URL(endpoint, this.baseUrl);
if (params) {
Object.entries(params).forEach(([key, value]) => {
url.searchParams.append(key, value);
});
}
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 10000);
try {
const response = await fetch(url.toString(), {
signal: controller.signal,
headers: {
'Content-Type': 'application/json',
},
});
clearTimeout(timeoutId);
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
return response.json() as Promise<T>;
} catch (error) {
clearTimeout(timeoutId);
throw this.wrapError(error);
}
}
private wrapError(error: unknown): Error {
// 错误类型细化处理
if (error instanceof DOMException && error.name === 'AbortError') {
return new Error('Request timeout');
}
return error instanceof Error ? error : new Error(String(error));
}
}
3.2 高级功能扩展
在实际项目中,我们还需要考虑:
- 请求拦截器(添加认证token)
- 响应拦截器(统一错误处理)
- 缓存策略(ETag/Last-Modified)
- 取消请求(页面跳转时)
一个实用的技巧是为GET请求添加缓存层:
typescript复制private cache = new Map<string, { data: any; timestamp: number }>();
async getWithCache<T>(
endpoint: string,
params?: Record<string, string>,
ttl = 300000 // 5分钟缓存
): Promise<T> {
const cacheKey = `${endpoint}?${new URLSearchParams(params).toString()}`;
const cached = this.cache.get(cacheKey);
if (cached && Date.now() - cached.timestamp < ttl) {
return cached.data;
}
const data = await this.get<T>(endpoint, params);
this.cache.set(cacheKey, { data, timestamp: Date.now() });
return data;
}
4. POST请求的安全实践
4.1 基础POST实现
POST请求需要特别注意数据安全和格式处理:
typescript复制async post<T>(
endpoint: string,
body: unknown,
options: { headers?: Record<string, string> } = {}
): Promise<T> {
const url = new URL(endpoint, this.baseUrl);
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 10000);
try {
const response = await fetch(url.toString(), {
method: 'POST',
signal: controller.signal,
headers: {
'Content-Type': 'application/json',
...options.headers,
},
body: JSON.stringify(body),
});
clearTimeout(timeoutId);
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
return response.json() as Promise<T>;
} catch (error) {
clearTimeout(timeoutId);
throw this.wrapError(error);
}
}
4.2 安全增强措施
针对网络热词中出现的"unexpected status 502"等问题,我们增加以下保护:
- CSRF防护:自动添加token
- 请求体校验:使用zod等库验证数据结构
- 重试机制:对5xx错误自动重试
typescript复制private async postWithRetry<T>(
endpoint: string,
body: unknown,
retries = 2,
backoff = 300
): Promise<T> {
try {
return await this.post<T>(endpoint, body);
} catch (error) {
if (this.isServerError(error) && retries > 0) {
await new Promise(resolve => setTimeout(resolve, backoff));
return this.postWithRetry(endpoint, body, retries - 1, backoff * 2);
}
throw error;
}
}
private isServerError(error: unknown): boolean {
return error instanceof Error && /^HTTP 5\d{2}$/.test(error.message);
}
5. 生产环境实战技巧
5.1 性能优化方案
- 连接池复用:虽然fetch底层会自动管理,但在高频请求场景下可以手动保持连接
- 请求合并:对并行GET请求使用Promise.all
- 数据压缩:确保Accept-Encoding头包含br/gzip
typescript复制async getMultiple<T>(requests: {
endpoint: string;
params?: Record<string, string>;
}[]): Promise<T[]> {
return Promise.all(
requests.map(req => this.get<T>(req.endpoint, req.params))
);
}
5.2 监控与调试
根据热词中出现的各种HTTP错误,建议添加:
- 请求耗时统计
- 失败率监控
- 错误日志上报
typescript复制private async instrumentedFetch(input: RequestInfo, init?: RequestInit) {
const start = performance.now();
let status = 'success';
try {
const response = await fetch(input, init);
if (!response.ok) {
status = `error_${response.status}`;
}
return response;
} catch (error) {
status = 'network_error';
throw error;
} finally {
const duration = performance.now() - start;
reportMetrics({
url: typeof input === 'string' ? input : input.url,
method: init?.method || 'GET',
duration,
status,
});
}
}
6. 完整封装方案实现
结合上述所有要点,我们给出一个生产可用的完整实现:
typescript复制type Interceptor = (config: RequestConfig) => RequestConfig | Promise<RequestConfig>;
interface RequestConfig extends RequestInit {
params?: Record<string, string>;
timeout?: number;
}
class HttpClient {
private baseUrl: string;
private requestInterceptors: Interceptor[] = [];
private responseInterceptors: Interceptor[] = [];
constructor(baseUrl: string) {
this.baseUrl = baseUrl.replace(/\/$/, '');
}
async request<T>(endpoint: string, config: RequestConfig = {}): Promise<T> {
let currentConfig: RequestConfig = {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
timeout: 10000,
...config,
};
// 执行请求拦截器
for (const interceptor of this.requestInterceptors) {
currentConfig = await interceptor(currentConfig);
}
const url = new URL(endpoint, this.baseUrl);
if (currentConfig.params) {
Object.entries(currentConfig.params).forEach(([key, value]) => {
url.searchParams.append(key, value);
});
}
const controller = new AbortController();
const timeoutId = setTimeout(
() => controller.abort(),
currentConfig.timeout!
);
try {
const response = await fetch(url.toString(), {
...currentConfig,
signal: controller.signal,
});
clearTimeout(timeoutId);
// 执行响应拦截器
let processedResponse = response;
for (const interceptor of this.responseInterceptors) {
processedResponse = await interceptor(processedResponse);
}
if (!processedResponse.ok) {
throw new Error(`HTTP ${processedResponse.status}`);
}
return processedResponse.json() as Promise<T>;
} catch (error) {
clearTimeout(timeoutId);
throw this.wrapError(error);
}
}
// 快捷方法
get<T>(endpoint: string, params?: Record<string, string>) {
return this.request<T>(endpoint, { params });
}
post<T>(endpoint: string, body: unknown) {
return this.request<T>(endpoint, {
method: 'POST',
body: JSON.stringify(body),
});
}
// 拦截器管理
useRequestInterceptor(interceptor: Interceptor) {
this.requestInterceptors.push(interceptor);
}
useResponseInterceptor(interceptor: Interceptor) {
this.responseInterceptors.push(interceptor);
}
private wrapError(error: unknown): Error {
if (error instanceof DOMException && error.name === 'AbortError') {
return new Error('Request timeout');
}
return error instanceof Error ? error : new Error(String(error));
}
}
使用示例:
typescript复制const api = new HttpClient('https://api.example.com');
// 添加认证拦截器
api.useRequestInterceptor(config => {
return {
...config,
headers: {
...config.headers,
Authorization: `Bearer ${getAuthToken()}`,
},
};
});
// 统一错误处理
api.useResponseInterceptor(async response => {
if (response.status === 401) {
await refreshToken();
throw new Error('Please retry after auth refresh');
}
return response;
});
// 业务调用
try {
const data = await api.get<{ items: Product[] }>('/products', {
category: 'electronics',
});
console.log(data.items);
} catch (error) {
showToast(error.message);
}
7. 测试策略与异常处理
7.1 单元测试要点
针对HTTP封装层的测试应该覆盖:
- 正常请求流程
- 各种HTTP错误状态码
- 网络超时场景
- 请求取消逻辑
- 拦截器链式调用
使用jest的mock示例:
typescript复制describe('HttpClient', () => {
let http: HttpClient;
let mockFetch: jest.Mock;
beforeEach(() => {
http = new HttpClient('https://api.example.com');
mockFetch = jest.fn();
global.fetch = mockFetch;
});
it('should handle successful GET request', async () => {
mockFetch.mockResolvedValueOnce({
ok: true,
json: () => Promise.resolve({ data: 'test' }),
});
const result = await http.get('/test');
expect(result).toEqual({ data: 'test' });
expect(mockFetch).toHaveBeenCalledWith(
'https://api.example.com/test',
expect.any(Object)
);
});
it('should throw on 404', async () => {
mockFetch.mockResolvedValueOnce({
ok: false,
status: 404,
});
await expect(http.get('/missing')).rejects.toThrow('HTTP 404');
});
});
7.2 异常处理最佳实践
根据热词中出现的各种网络错误,建议:
- 对不同的错误类型提供恢复方案
- 用户友好的错误提示
- 关键操作的自动重试
错误分类处理示例:
typescript复制class ApiError extends Error {
constructor(
message: string,
public readonly type: 'network' | 'server' | 'client' | 'timeout'
) {
super(message);
}
}
// 在封装层转换错误
private wrapError(error: unknown): ApiError {
if (error instanceof DOMException && error.name === 'AbortError') {
return new ApiError('Request timeout', 'timeout');
}
if (error instanceof Error) {
if (/^HTTP 5\d{2}$/.test(error.message)) {
return new ApiError(error.message, 'server');
}
if (/^HTTP [34]\d{2}$/.test(error.message)) {
return new ApiError(error.message, 'client');
}
return new ApiError(error.message, 'network');
}
return new ApiError(String(error), 'network');
}
// 业务层处理
try {
await api.post('/order', { items: cart });
} catch (error) {
if (error instanceof ApiError) {
switch (error.type) {
case 'timeout':
showToast('网络超时,请重试');
break;
case 'server':
showToast('服务器繁忙,请稍后再试');
break;
default:
showToast('网络错误');
}
}
}
8. 与ArkUI的集成实践
8.1 状态管理配合
在ArkUI中使用封装好的HTTP客户端时,建议:
- 将请求状态与UI状态绑定
- 使用@State管理加载/错误状态
- 结合Refresh组件实现下拉刷新
示例组件:
typescript复制@Entry
@Component
struct ProductList {
@State products: Product[] = [];
@State loading: boolean = false;
@State error?: string;
private http = new HttpClient('https://api.example.com');
aboutToAppear() {
this.loadData();
}
async loadData() {
try {
this.loading = true;
this.error = undefined;
this.products = await this.http.get<Product[]>('/products');
} catch (error) {
this.error = error.message;
} finally {
this.loading = false;
}
}
build() {
Column() {
if (this.loading) {
LoadingIndicator().height(100)
} else if (this.error) {
Text(this.error).color(Color.Red)
Button('Retry').onClick(() => this.loadData())
} else {
List({ space: 10 }) {
ForEach(this.products, item => {
ListItem() {
ProductItem({ product: item })
}
})
}
}
}
}
}
8.2 性能优化技巧
- 请求去重:防止组件重复渲染导致的重复请求
- 数据缓存:使用内存或持久化缓存
- 请求优先级:关键数据优先加载
typescript复制class ProductService {
private cache = new Map<string, Product[]>();
private pendingRequests = new Map<string, Promise<Product[]>>();
async getProducts(category: string): Promise<Product[]> {
const cacheKey = `products_${category}`;
// 内存缓存
if (this.cache.has(cacheKey)) {
return this.cache.get(cacheKey)!;
}
// 请求去重
if (this.pendingRequests.has(cacheKey)) {
return this.pendingRequests.get(cacheKey)!;
}
const promise = this.http.get<Product[]>(`/products`, { category })
.then(data => {
this.cache.set(cacheKey, data);
return data;
})
.finally(() => {
this.pendingRequests.delete(cacheKey);
});
this.pendingRequests.set(cacheKey, promise);
return promise;
}
}
