
引言
HTTP请求是应用开发中不可或缺的部分。本文将介绍如何封装一个统一的HTTP请求工具类,包括:
- 请求拦截器
- 响应拦截器
- 统一错误处理
- 请求取消机制
- 请求重试机制
通过本文,你将掌握如何构建一个健壮的HTTP请求封装。
学习目标
完成本文后,你将能够:
- ✅ 封装HTTP请求工具类
- ✅ 添加请求/响应拦截器
- ✅ 实现统一错误处理
- ✅ 添加请求取消机制
- ✅ 实现请求重试
需求分析
功能模块设计
| 模块 | 功能描述 | 技术要点 |
|---|---|---|
| 请求封装 | 统一请求入口 | Promise封装 |
| 请求拦截器 | 请求前处理 | Token注入、参数处理 |
| 响应拦截器 | 响应后处理 | 数据解析、错误处理 |
| 错误处理 | 统一错误处理 | 错误类型分类、提示 |
| 请求取消 | 取消正在进行的请求 | AbortController |
| 请求重试 | 失败自动重试 | 重试次数配置 |
技术原理深度解析
HTTP协议基础
HTTP(Hypertext Transfer Protocol)是一种无状态的应用层协议,用于在客户端和服务器之间传输超文本数据。
HTTP请求结构:
GET /api/articles?page=1 HTTP/1.1
Host: api.example.com
Authorization: Bearer token123
Content-Type: application/json
User-Agent: JieQiTong/1.0.0
(请求体,POST/PUT请求时存在)
HTTP响应结构:
HTTP/1.1 200 OK
Content-Type: application/json
Content-Length: 1234
Date: Wed, 29 May 2024 10:00:00 GMT
{"code":200,"data":[...],"message":"success"}
HTTP状态码分类:
| 状态码范围 | 含义 | 典型场景 |
|---|---|---|
| 1xx | 信息响应 | 请求已接收,继续处理 |
| 2xx | 成功 | 请求已成功处理 |
| 3xx | 重定向 | 需要进一步操作 |
| 4xx | 客户端错误 | 请求有问题 |
| 5xx | 服务器错误 | 服务器处理失败 |
常用状态码详解:
- 200 OK - 请求成功
- 201 Created - 资源创建成功
- 400 Bad Request - 请求参数错误
- 401 Unauthorized - 未授权,Token无效或过期
- 403 Forbidden - 禁止访问,权限不足
- 404 Not Found - 资源不存在
- 500 Internal Server Error - 服务器内部错误
拦截器机制原理
拦截器(Interceptor)是一种AOP(面向切面编程)思想的实现,允许在请求发送前和响应返回后插入自定义逻辑。
拦截器执行流程:
请求发起
↓
请求拦截器1 → 请求拦截器2 → ... → 请求拦截器N
↓
发送HTTP请求
↓
响应拦截器1 → 响应拦截器2 → ... → 响应拦截器N
↓
返回结果给调用者
请求拦截器常见用途:
- Token注入 - 在请求头中添加Authorization
- 请求参数统一处理 - 添加公共参数、签名
- 请求日志记录 - 记录请求信息用于调试
- 请求超时设置 - 统一设置超时时间
响应拦截器常见用途:
- 统一错误处理 - 处理Token过期、权限不足
- 数据格式转换 - 统一处理响应格式
- 响应日志记录 - 记录响应信息
- 缓存策略 - 对GET请求进行缓存
请求取消机制原理
AbortController是Web API提供的取消请求机制,通过AbortSignal控制请求的取消。
工作原理:
- 创建AbortController实例
- 将controller.signal传递给fetch请求
- 调用controller.abort()取消请求
- 请求会抛出AbortError异常
应用场景:
- 页面切换时取消未完成的请求
- 连续请求时取消前一个请求(防抖)
- 用户主动取消操作
请求重试机制原理
请求重试用于在网络不稳定时自动重试请求,提高系统容错能力。
重试策略:
- 固定间隔重试 - 每次重试间隔相同时间
- 指数退避 - 重试间隔呈指数增长
- 随机退避 - 在一定范围内随机选择间隔
指数退避公式:
delay = baseDelay * (2^retryCount) + random(0, jitter)
重试条件:
- 网络错误(NETWORK_ERROR)
- 服务器错误(5xx状态码)
- 特定的客户端错误(如408超时)
核心实现
步骤1: HTTP请求工具类封装
typescript
// utils/HttpService.ets
import prompt from '@ohos.prompt';
/**
* HTTP请求工具类
*/
export class HttpService {
// 基础URL
private baseUrl: string = '';
// 请求超时时间(毫秒)
private timeout: number = 30000;
// 最大重试次数
private maxRetries: number = 3;
// 请求拦截器列表
private requestInterceptors: Array<RequestInterceptor> = [];
// 响应拦截器列表
private responseInterceptors: Array<ResponseInterceptor> = [];
// 存储正在进行的请求
private pendingRequests: Map<string, AbortController> = new Map();
/**
* 构造函数
*/
constructor(baseUrl: string = '') {
this.baseUrl = baseUrl;
}
/**
* 添加请求拦截器
*/
addRequestInterceptor(interceptor: RequestInterceptor): void {
this.requestInterceptors.push(interceptor);
}
/**
* 添加响应拦截器
*/
addResponseInterceptor(interceptor: ResponseInterceptor): void {
this.responseInterceptors.push(interceptor);
}
/**
* GET请求
*/
async get<T>(url: string, params?: Record<string, any>): Promise<HttpResponse<T>> {
return this.request<T>('GET', url, undefined, params);
}
/**
* POST请求
*/
async post<T>(url: string, data?: Record<string, any>): Promise<HttpResponse<T>> {
return this.request<T>('POST', url, data);
}
/**
* PUT请求
*/
async put<T>(url: string, data?: Record<string, any>): Promise<HttpResponse<T>> {
return this.request<T>('PUT', url, data);
}
/**
* DELETE请求
*/
async delete<T>(url: string, params?: Record<string, any>): Promise<HttpResponse<T>> {
return this.request<T>('DELETE', url, undefined, params);
}
/**
* 统一请求方法
*/
private async request<T>(
method: HttpMethod,
url: string,
data?: Record<string, any>,
params?: Record<string, any>
): Promise<HttpResponse<T>> {
// 构建完整URL
const fullUrl = this.buildUrl(url, params);
// 生成请求标识
const requestKey = `${method}_${fullUrl}`;
// 取消之前的相同请求
this.cancelRequest(requestKey);
// 创建AbortController
const controller = new AbortController();
this.pendingRequests.set(requestKey, controller);
try {
// 应用请求拦截器
const config: RequestConfig = {
method,
url: fullUrl,
headers: {},
data,
timeout: this.timeout
};
for (const interceptor of this.requestInterceptors) {
await interceptor(config);
}
// 设置默认headers
if (!config.headers['Content-Type']) {
config.headers['Content-Type'] = 'application/json';
}
// 发送请求
const response = await fetch(fullUrl, {
method,
headers: config.headers,
body: data ? JSON.stringify(data) : undefined,
signal: controller.signal
});
// 应用响应拦截器
for (const interceptor of this.responseInterceptors) {
await interceptor(response);
}
// 解析响应
const result = await this.parseResponse<T>(response);
return result;
} catch (error) {
throw this.handleError(error);
} finally {
// 移除请求记录
this.pendingRequests.delete(requestKey);
}
}
/**
* 构建URL
*/
private buildUrl(url: string, params?: Record<string, any>): string {
let fullUrl = this.baseUrl ? `${this.baseUrl}${url}` : url;
if (params) {
const queryString = new URLSearchParams(params).toString();
if (queryString) {
fullUrl += (fullUrl.includes('?') ? '&' : '?') + queryString;
}
}
return fullUrl;
}
/**
* 解析响应
*/
private async parseResponse<T>(response: Response): Promise<HttpResponse<T>> {
let data: any;
try {
data = await response.json();
} catch {
data = await response.text();
}
if (response.ok) {
return {
success: true,
data,
status: response.status,
message: 'success'
};
} else {
throw {
success: false,
data,
status: response.status,
message: data?.message || '请求失败'
};
}
}
/**
* 处理错误
*/
private handleError(error: any): HttpError {
if (error.name === 'AbortError') {
return {
success: false,
code: 'REQUEST_CANCELLED',
message: '请求已取消',
data: null
};
}
if (!error.success) {
return {
success: false,
code: error.status?.toString() || 'UNKNOWN',
message: error.message || '请求失败',
data: error.data
};
}
return {
success: false,
code: 'NETWORK_ERROR',
message: '网络异常,请检查网络连接',
data: null
};
}
/**
* 取消请求
*/
cancelRequest(key: string): void {
const controller = this.pendingRequests.get(key);
if (controller) {
controller.abort();
this.pendingRequests.delete(key);
}
}
/**
* 取消所有请求
*/
cancelAllRequests(): void {
this.pendingRequests.forEach((controller) => {
controller.abort();
});
this.pendingRequests.clear();
}
}
// 类型定义
type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE';
interface RequestConfig {
method: HttpMethod;
url: string;
headers: Record<string, string>;
data?: Record<string, any>;
timeout: number;
}
interface HttpResponse<T> {
success: boolean;
data: T;
status: number;
message: string;
}
interface HttpError {
success: boolean;
code: string;
message: string;
data: any;
}
type RequestInterceptor = (config: RequestConfig) => Promise<void> | void;
type ResponseInterceptor = (response: Response) => Promise<void> | void;
/**
* 创建默认的HTTP服务实例
*/
export const createHttpService = (baseUrl: string = '') => {
const service = new HttpService(baseUrl);
// 添加默认请求拦截器 - 注入Token
service.addRequestInterceptor(async (config) => {
const token = await getToken();
if (token) {
config.headers['Authorization'] = `Bearer ${token}`;
}
});
// 添加默认响应拦截器 - 处理token过期
service.addResponseInterceptor(async (response) => {
if (response.status === 401) {
// Token过期,跳转到登录页
prompt.showToast({ message: '登录已过期,请重新登录' });
// 清除本地token
await clearToken();
// 跳转到登录页
router.pushUrl({ url: 'pages/Login' });
}
});
return service;
};
// Mock函数
async function getToken(): Promise<string | null> {
return null;
}
async function clearToken(): Promise<void> {}
</script>
设计要点:
- 使用Promise封装HTTP请求
- 支持请求/响应拦截器
- 请求取消机制
- 统一错误处理
步骤2: 使用HTTP服务
typescript
// 在页面中使用HTTP服务
import { createHttpService } from '../utils/HttpService';
const httpService = createHttpService('https://api.example.com');
@Entry
@Component
struct ApiTestPage {
@State data: any[] = [];
@State isLoading: boolean = false;
async onPageShow() {
await this.loadData();
}
async loadData() {
this.isLoading = true;
try {
const response = await httpService.get('/articles', { page: 1, limit: 10 });
if (response.success) {
this.data = response.data;
}
} catch (error) {
console.error('请求失败:', error);
prompt.showToast({ message: error.message || '请求失败' });
} finally {
this.isLoading = false;
}
}
async submitForm(formData: Record<string, any>) {
try {
const response = await httpService.post('/articles', formData);
if (response.success) {
prompt.showToast({ message: '提交成功' });
}
} catch (error) {
prompt.showToast({ message: error.message || '提交失败' });
}
}
build() {
// 页面内容
}
}
设计要点:
- 创建HTTP服务实例
- 使用get/post方法发送请求
- 处理成功和失败情况
步骤3: 请求重试机制
typescript
// 请求重试包装器
export class RetryHttpService extends HttpService {
private retryDelay: number = 1000; // 重试间隔(毫秒)
async requestWithRetry<T>(
method: HttpMethod,
url: string,
data?: Record<string, any>,
params?: Record<string, any>,
retryCount: number = 0
): Promise<HttpResponse<T>> {
try {
return await this.request<T>(method, url, data, params);
} catch (error) {
// 如果是网络错误,进行重试
if (error.code === 'NETWORK_ERROR' && retryCount < this.maxRetries) {
// 等待一段时间后重试
await this.delay(this.retryDelay * Math.pow(2, retryCount));
return this.requestWithRetry(method, url, data, params, retryCount + 1);
}
throw error;
}
}
private delay(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms));
}
// 重写父类方法
async get<T>(url: string, params?: Record<string, any>): Promise<HttpResponse<T>> {
return this.requestWithRetry<T>('GET', url, undefined, params);
}
async post<T>(url: string, data?: Record<string, any>): Promise<HttpResponse<T>> {
return this.requestWithRetry<T>('POST', url, data);
}
}
设计要点:
- 指数退避策略
- 网络错误自动重试
- 可配置最大重试次数
实际应用场景
场景1: API服务封装
在实际项目中,通常会为每个业务模块创建专门的API服务类:
typescript
// api/ArticleApi.ets
import { createHttpService, HttpResponse } from '../utils/HttpService';
const httpService = createHttpService('https://api.jieqitong.com');
/**
* 文章API服务
*/
export class ArticleApi {
/**
* 获取文章列表
*/
static async getArticles(page: number = 1, limit: number = 10): Promise<HttpResponse<Article[]>> {
return await httpService.get('/articles', { page, limit });
}
/**
* 获取文章详情
*/
static async getArticleDetail(id: string): Promise<HttpResponse<Article>> {
return await httpService.get(`/articles/${id}`);
}
/**
* 创建文章
*/
static async createArticle(data: CreateArticleRequest): Promise<HttpResponse<Article>> {
return await httpService.post('/articles', data);
}
/**
* 更新文章
*/
static async updateArticle(id: string, data: UpdateArticleRequest): Promise<HttpResponse<Article>> {
return await httpService.put(`/articles/${id}`, data);
}
/**
* 删除文章
*/
static async deleteArticle(id: string): Promise<HttpResponse<void>> {
return await httpService.delete(`/articles/${id}`);
}
}
interface Article {
id: string;
title: string;
content: string;
author: string;
readCount: number;
createdAt: string;
}
interface CreateArticleRequest {
title: string;
content: string;
category: string;
}
interface UpdateArticleRequest {
title?: string;
content?: string;
}
// 使用示例
async function loadArticles() {
try {
const response = await ArticleApi.getArticles(1, 20);
if (response.success) {
console.log('文章列表:', response.data);
}
} catch (error) {
console.error('加载文章失败:', error);
}
}
场景2: 文件上传
typescript
// 文件上传封装
export class FileApi {
static async uploadFile(filePath: string): Promise<HttpResponse<UploadResult>> {
const formData = new FormData();
formData.append('file', {
uri: filePath,
name: 'upload_file',
type: 'image/jpeg'
});
const response = await fetch('https://api.jieqitong.com/upload', {
method: 'POST',
headers: {
'Authorization': `Bearer ${await getToken()}`
},
body: formData
});
return await response.json();
}
}
interface UploadResult {
url: string;
fileName: string;
}
场景3: 批量请求处理
typescript
// 批量请求封装
export class BatchRequest {
/**
* 并行请求多个接口
*/
static async parallel<T extends any[]>(
...requests: Array<Promise<T[number]>>
): Promise<T> {
return await Promise.all(requests) as T;
}
/**
* 串行请求多个接口
*/
static async serial<T>(
requests: Array<() => Promise<T>>
): Promise<T[]> {
const results: T[] = [];
for (const request of requests) {
const result = await request();
results.push(result);
}
return results;
}
}
// 使用示例
async function loadDashboardData() {
// 并行加载多个数据
const [articles, stats, notifications] = await BatchRequest.parallel(
ArticleApi.getArticles(1, 5),
StatsApi.getStats(),
NotificationApi.getNotifications()
);
console.log('仪表盘数据加载完成');
}
常见问题与解决方案
问题1: 请求超时
现象: 请求长时间无响应
原因: 网络延迟、服务器响应慢
解决方案:
typescript
// 设置合理的超时时间
const controller = new AbortController();
const timeoutId = setTimeout(() => {
controller.abort();
}, 30000); // 30秒超时
try {
const response = await fetch(url, {
signal: controller.signal
});
} finally {
clearTimeout(timeoutId);
}
问题2: Token过期处理
现象: 接口返回401状态码
解决方案:
typescript
// 在响应拦截器中统一处理
service.addResponseInterceptor(async (response) => {
if (response.status === 401) {
// 尝试刷新Token
const refreshToken = await tokenManager.getRefreshToken();
if (refreshToken) {
try {
const refreshResponse = await httpService.post('/refresh', { refreshToken });
if (refreshResponse.success) {
// 保存新Token
await tokenManager.setToken(
refreshResponse.data.accessToken,
refreshResponse.data.refreshToken,
refreshResponse.data.expireTime
);
// 重新发起原请求
// 可以通过缓存请求配置实现自动重试
}
} catch {
// 刷新失败,跳转到登录页
await tokenManager.clearToken();
router.pushUrl({ url: 'pages/Login' });
}
} else {
// 没有refreshToken,直接跳转登录
await tokenManager.clearToken();
router.pushUrl({ url: 'pages/Login' });
}
}
});
问题3: 重复请求
现象: 快速点击按钮导致多次请求
解决方案:
typescript
// 使用防抖或节流
function debounce<T extends (...args: any[]) => any>(
func: T,
wait: number
): (...args: Parameters<T>) => void {
let timeout: number | null = null;
return (...args: Parameters<T>) => {
if (timeout) clearTimeout(timeout);
timeout = setTimeout(() => func(...args), wait);
};
}
// 使用示例
const debouncedLoadData = debounce(async () => {
await loadData();
}, 300);
Button('加载数据')
.onClick(() => debouncedLoadData());
问题4: 跨域问题
现象: 请求被浏览器阻止,报CORS错误
解决方案:
typescript
// 在服务端配置CORS
// 服务端响应头配置示例:
// Access-Control-Allow-Origin: *
// Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS
// Access-Control-Allow-Headers: Content-Type, Authorization
性能优化策略
策略1: 请求缓存
typescript
// 实现请求缓存
class CachedHttpService extends HttpService {
private cache: Map<string, CachedResponse> = new Map();
private cacheTtl: number = 5 * 60 * 1000; // 5分钟缓存
async get<T>(url: string, params?: Record<string, any>): Promise<HttpResponse<T>> {
const cacheKey = this.buildCacheKey(url, params);
// 检查缓存
const cached = this.cache.get(cacheKey);
if (cached && Date.now() < cached.expireTime) {
return cached.response;
}
// 发起请求
const response = await super.get<T>(url, params);
// 缓存成功的GET请求
if (response.success) {
this.cache.set(cacheKey, {
response,
expireTime: Date.now() + this.cacheTtl
});
}
return response;
}
private buildCacheKey(url: string, params?: Record<string, any>): string {
const queryString = params ? JSON.stringify(params) : '';
return `${url}_${queryString}`;
}
clearCache(): void {
this.cache.clear();
}
}
interface CachedResponse {
response: HttpResponse<any>;
expireTime: number;
}
策略2: 请求合并
typescript
// 批量请求合并
class BatchHttpService extends HttpService {
private pendingRequests: Map<string, Array<(response: any) => void>> = new Map();
async getWithBatch<T>(url: string, params?: Record<string, any>): Promise<HttpResponse<T>> {
const key = this.buildRequestKey(url, params);
return new Promise((resolve) => {
// 如果有相同请求正在进行,加入等待队列
if (this.pendingRequests.has(key)) {
this.pendingRequests.get(key)!.push(resolve);
return;
}
// 标记请求正在进行
this.pendingRequests.set(key, [resolve]);
// 执行请求
super.get<T>(url, params).then((response) => {
// 通知所有等待的请求
const resolvers = this.pendingRequests.get(key)!;
resolvers.forEach(r => r(response));
// 清理状态
this.pendingRequests.delete(key);
});
});
}
private buildRequestKey(url: string, params?: Record<string, any>): string {
return `${url}_${JSON.stringify(params)}`;
}
}
本章小结
核心知识点
本文完成了HTTP请求封装的深度解析:
1. HTTP协议原理
- 请求/响应结构
- 状态码分类
- 常用状态码详解
2. 拦截器机制
- 请求拦截器(Token注入、参数处理)
- 响应拦截器(错误处理、数据转换)
3. 请求取消机制
- AbortController实现
- 应用场景(防抖、页面切换)
4. 请求重试机制
- 指数退避策略
- 重试条件判断
5. 实际应用场景
- API服务封装
- 文件上传
- 批量请求处理
6. 常见问题解决方案
- 请求超时
- Token过期
- 重复请求
- 跨域问题
7. 性能优化策略
- 请求缓存
- 请求合并
下一步预告
HTTP请求封装已经完成!在下一篇文章中,我们将学习:
- 全局数据通信
- 全局状态管理
- 事件总线
- 数据共享
节气通应用已发布上线,可在应用市场下载体验
相关链接
- 项目源码 : Atomgit仓库