HarmonyOS HTTP请求:从"能跑就行"到"优雅可靠"的进化之路
一、当网络请求遇上"薛定谔的响应"
还记得第一次在HarmonyOS里写网络请求吗?满心欢喜地调完接口,结果页面一片空白------没有错误提示,没有崩溃日志,就像什么都没发生过一样。你盯着代码看了半小时,最后才发现:哦,忘了在module.json5里加网络权限。
这种"静默失败"的经历,相信不少开发者都遇到过。HTTP请求看似简单,但在移动端开发中,它就像一座冰山------表面上的几行代码,下面藏着连接管理、超时控制、错误处理、数据解析等一系列复杂问题。
今天,我们就来彻底拆解HarmonyOS的HTTP请求机制。不只是教你怎么用,更要告诉你为什么这么用,以及如何用得更好。
二、HTTP请求在HarmonyOS里到底经历了什么?
1. 什么方式呢
通过
失败
发起请求
权限检查
创建HttpRequest对象
静默失败
配置请求参数
建立TCP连接
发送HTTP请求
等待服务器响应
接收响应头
触发headersReceive事件
接收响应体
请求完成回调
销毁HttpRequest对象
2. 那些容易忽略的关键细节
- 权限不是可选项 :没有
ohos.permission.INTERNET,请求直接消失,连个错误都不给 - 每个请求都是独立的 :
http.createHttp()创建的对象不能复用,用完必须destroy() - 回调地狱的救赎:支持Promise和Callback两种方式,但混用容易出问题
- 内存泄漏的隐形杀手:忘记销毁的HttpRequest对象会一直占用连接资源
三、你的第一个"真正可用"的HTTP请求
1. 基础配置(别再忘了这一步)
json
// module.json5 - 没有这个,一切免谈
{
"module": {
"requestPermissions": [
{
"name": "ohos.permission.INTERNET",
"usedScene": {
"when": "always"
}
}
]
}
}
2. 基础GET请求(Promise版本)
typescript
import http from '@ohos.net.http';
async function fetchUserProfile(userId: string): Promise<User> {
// 注意:每次请求都要创建新实例
const httpRequest = http.createHttp();
try {
const response = await httpRequest.request(
`https://api.example.com/users/${userId}`,
{
method: http.RequestMethod.GET,
connectTimeout: 10000, // 10秒连接超时
readTimeout: 15000, // 15秒读取超时
header: {
'Content-Type': 'application/json',
'Accept': 'application/json'
}
}
);
if (response.responseCode === 200) {
// response.result可能是string或ArrayBuffer
const result = JSON.parse(response.result as string);
return result as User;
} else {
throw new Error(`HTTP ${response.responseCode}: ${response.result}`);
}
} catch (error) {
console.error('网络请求失败:', error);
throw error;
} finally {
// 关键!必须销毁,否则内存泄漏
httpRequest.destroy();
}
}
3. POST请求的"坑"与"填"
typescript
async function login(username: string, password: string): Promise<LoginResponse> {
const httpRequest = http.createHttp();
try {
const response = await httpRequest.request(
'https://api.example.com/auth/login',
{
method: http.RequestMethod.POST,
header: {
'Content-Type': 'application/json'
},
// 注意:POST数据放在extraData,不是body!
extraData: JSON.stringify({
username: username,
password: password
}),
connectTimeout: 10000,
readTimeout: 10000
}
);
return JSON.parse(response.result as string);
} finally {
httpRequest.destroy();
}
}
看到那个extraData了吗?这是HarmonyOS和浏览器Fetch API的一个关键区别。第一次用的时候,我对着文档看了三遍才确认------没错,POST数据确实叫extraData。
四、封装一个生产级的HTTP客户端
直接使用原生API就像用螺丝刀拧螺丝------能干活,但效率不高。真正项目中,我们需要的是电动螺丝刀。下面这个封装,是我在多个项目中打磨出来的。
1. 类型定义(TypeScript的优势就在这里)
typescript
// network/HttpTypes.ets
export interface ApiResponse<T> {
code: number;
message: string;
data: T;
}
export interface RequestConfig {
url: string;
method?: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH';
params?: Record<string, string | number>;
data?: any;
headers?: Record<string, string>;
timeout?: number;
}
export class HttpError extends Error {
constructor(
public code: number,
message: string,
public originalError?: any
) {
super(message);
this.name = 'HttpError';
}
}
2. 核心HttpClient类
typescript
// network/HttpClient.ets
import http from '@ohos.net.http';
type RequestInterceptor = (config: RequestConfig) => RequestConfig;
type ResponseInterceptor<T> = (response: ApiResponse<T>) => ApiResponse<T>;
export class HttpClient {
private baseUrl: string;
private defaultTimeout: number = 15000;
private requestInterceptors: RequestInterceptor[] = [];
private responseInterceptors: ResponseInterceptor<any>[] = [];
// 单例模式,全局一个实例就够了
private static instance: HttpClient;
static getInstance(baseUrl?: string): HttpClient {
if (!HttpClient.instance) {
if (!baseUrl) {
throw new Error('首次调用需要提供baseUrl');
}
HttpClient.instance = new HttpClient(baseUrl);
}
return HttpClient.instance;
}
private constructor(baseUrl: string) {
this.baseUrl = baseUrl.endsWith('/') ? baseUrl.slice(0, -1) : baseUrl;
}
// 添加请求拦截器(用于添加Token等)
addRequestInterceptor(interceptor: RequestInterceptor): void {
this.requestInterceptors.push(interceptor);
}
// 添加响应拦截器(用于统一错误处理)
addResponseInterceptor<T>(interceptor: ResponseInterceptor<T>): void {
this.responseInterceptors.push(interceptor);
}
async request<T>(config: RequestConfig): Promise<T> {
// 1. 执行请求拦截器
let finalConfig = { ...config };
for (const interceptor of this.requestInterceptors) {
finalConfig = interceptor(finalConfig);
}
// 2. 构建完整URL
let url = `${this.baseUrl}${finalConfig.url}`;
// 3. 处理查询参数
if (finalConfig.params) {
const queryString = Object.entries(finalConfig.params)
.map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`)
.join('&');
url += `?${queryString}`;
}
// 4. 创建请求实例
const httpRequest = http.createHttp();
try {
// 5. 发起请求
const response = await httpRequest.request(url, {
method: this.mapMethod(finalConfig.method || 'GET'),
header: {
'Content-Type': 'application/json',
'Accept': 'application/json',
...finalConfig.headers
},
extraData: finalConfig.data ? JSON.stringify(finalConfig.data) : undefined,
connectTimeout: finalConfig.timeout || this.defaultTimeout,
readTimeout: finalConfig.timeout || this.defaultTimeout
});
// 6. 解析响应
const rawData = response.result as string;
const parsedResponse: ApiResponse<T> = JSON.parse(rawData);
// 7. 执行响应拦截器
let finalResponse = parsedResponse;
for (const interceptor of this.responseInterceptors) {
finalResponse = interceptor(finalResponse);
}
// 8. 统一错误处理
if (finalResponse.code !== 0 && finalResponse.code !== 200) {
throw new HttpError(
finalResponse.code,
finalResponse.message || '请求失败'
);
}
return finalResponse.data;
} catch (error) {
// 9. 网络错误处理
if (error instanceof HttpError) {
throw error;
}
// 处理HTTP状态码错误
if (error.responseCode && error.responseCode !== 200) {
throw new HttpError(
error.responseCode,
`HTTP ${error.responseCode}`,
error
);
}
// 处理网络异常
throw new HttpError(
-1,
'网络连接失败,请检查网络设置',
error
);
} finally {
// 10. 清理资源
httpRequest.destroy();
}
}
private mapMethod(method: string): http.RequestMethod {
const methodMap: Record<string, http.RequestMethod> = {
'GET': http.RequestMethod.GET,
'POST': http.RequestMethod.POST,
'PUT': http.RequestMethod.PUT,
'DELETE': http.RequestMethod.DELETE,
'PATCH': http.RequestMethod.PATCH
};
return methodMap[method] || http.RequestMethod.GET;
}
// 快捷方法
get<T>(url: string, params?: Record<string, any>, config?: Partial<RequestConfig>): Promise<T> {
return this.request<T>({
url,
method: 'GET',
params,
...config
});
}
post<T>(url: string, data?: any, config?: Partial<RequestConfig>): Promise<T> {
return this.request<T>({
url,
method: 'POST',
data,
...config
});
}
}
3. 使用示例
typescript
// 初始化
const apiClient = HttpClient.getInstance('https://api.example.com');
// 添加Token拦截器
apiClient.addRequestInterceptor((config) => {
const token = AppStorage.get<string>('userToken');
if (token) {
config.headers = {
...config.headers,
'Authorization': `Bearer ${token}`
};
}
return config;
});
// 添加错误拦截器
apiClient.addResponseInterceptor((response) => {
if (response.code === 401) {
// Token过期,跳转到登录页
router.replaceUrl({ url: 'pages/Login' });
throw new HttpError(401, '请重新登录');
}
return response;
});
// 在组件中使用
@Component
struct UserProfile {
@State user: User | null = null;
@State loading: boolean = false;
async loadUserData() {
this.loading = true;
try {
this.user = await apiClient.get<User>('/api/user/profile');
} catch (error) {
promptAction.showToast({
message: error.message,
duration: 3000
});
} finally {
this.loading = false;
}
}
build() {
Column() {
if (this.loading) {
LoadingIndicator()
} else if (this.user) {
UserCard({ user: this.user })
}
}
}
}
五、电商应用 vs 即时通讯
1. 电商应用:请求可缓存,失败可重试
typescript
class ECommerceService {
private cache = new Map<string, { data: any; timestamp: number }>();
async getProductList(category: string, forceRefresh = false): Promise<Product[]> {
const cacheKey = `products_${category}`;
// 检查缓存(5分钟内有效)
if (!forceRefresh) {
const cached = this.cache.get(cacheKey);
if (cached && Date.now() - cached.timestamp < 5 * 60 * 1000) {
return cached.data;
}
}
// 带重试机制的请求
let lastError: Error;
for (let i = 0; i < 3; i++) {
try {
const products = await apiClient.get<Product[]>(`/api/products`, { category });
// 更新缓存
this.cache.set(cacheKey, {
data: products,
timestamp: Date.now()
});
return products;
} catch (error) {
lastError = error;
if (i < 2) {
// 等待指数退避时间
await new Promise(resolve => setTimeout(resolve, 1000 * Math.pow(2, i)));
}
}
}
throw lastError!;
}
}
2. 即时通讯:长连接 + 短轮询混合
typescript
class ChatService {
private ws: WebSocket | null = null;
private messageQueue: Message[] = [];
private isOnline = false;
// 初始化WebSocket连接
connect(userId: string): void {
this.ws = new WebSocket(`wss://chat.example.com/ws?userId=${userId}`);
this.ws.onopen = () => {
this.isOnline = true;
this.flushMessageQueue();
};
this.ws.onmessage = (event) => {
this.handleIncomingMessage(JSON.parse(event.data));
};
this.ws.onclose = () => {
this.isOnline = false;
// 退回到HTTP轮询
this.startPolling();
};
}
// 发送消息(优先WebSocket,失败则用HTTP)
async sendMessage(message: Message): Promise<void> {
if (this.isOnline && this.ws) {
this.ws.send(JSON.stringify(message));
} else {
// 加入队列,等待连接恢复
this.messageQueue.push(message);
// 同时尝试HTTP发送
try {
await apiClient.post('/api/messages/send', message);
this.messageQueue = this.messageQueue.filter(m => m.id !== message.id);
} catch (error) {
console.error('消息发送失败,已加入重试队列:', error);
}
}
}
// 轮询备选方案
private startPolling(): void {
setInterval(async () => {
try {
const messages = await apiClient.get<Message[]>('/api/messages/poll');
messages.forEach(msg => this.handleIncomingMessage(msg));
} catch (error) {
console.error('轮询失败:', error);
}
}, 5000); // 每5秒轮询一次
}
}
看到区别了吗?电商应用追求的是稳定性 和性能 ,所以用缓存+重试。即时通讯追求的是实时性 和可靠性,所以用WebSocket为主,HTTP轮询为备。
六、鸿蒙6新特性:HTTP请求的"智能进化"
从HarmonyOS 6开始,HTTP模块有了几个让人眼前一亮的新特性:
1. 智能重试与熔断
typescript
// HarmonyOS 6的新配置项
const response = await httpRequest.request(url, {
method: http.RequestMethod.GET,
// 新增:自动重试配置
retryConfig: {
maxRetries: 3, // 最大重试次数
retryDelay: 1000, // 重试延迟(毫秒)
retryOnStatus: [502, 503, 504] // 对这些状态码重试
},
// 新增:熔断器配置
circuitBreaker: {
failureThreshold: 5, // 5次失败后熔断
resetTimeout: 30000, // 30秒后尝试恢复
halfOpenMaxRequests: 3 // 半开状态最多3个请求
}
});
2. 请求优先级调度
typescript
// 关键请求优先处理
const highPriorityRequest = http.createHttp({
priority: http.RequestPriority.HIGH // 新增:请求优先级
});
// 图片加载可以降低优先级
const imageRequest = http.createHttp({
priority: http.RequestPriority.LOW
});
3. 智能数据压缩
typescript
// 自动协商压缩算法
const response = await httpRequest.request(url, {
method: http.RequestMethod.GET,
// 新增:压缩配置
compression: {
enabled: true,
minSize: 1024, // 超过1KB才压缩
algorithms: ['gzip', 'br', 'deflate']
}
});
4. 原生支持拦截器
typescript
// 不再需要手动封装,系统原生支持
const httpRequest = http.createHttp();
// 添加请求拦截器
httpRequest.addRequestInterceptor((config) => {
config.header['X-Request-ID'] = generateUUID();
return config;
});
// 添加响应拦截器
httpRequest.addResponseInterceptor((response) => {
if (response.responseCode === 429) {
// 处理限流
showRateLimitToast();
}
return response;
});
这些新特性让HTTP请求从"能用"变成了"好用"。特别是那个熔断器------再也不用担心某个接口挂掉拖垮整个应用了。
七、那些年我们踩过的HTTP坑
1. 内存泄漏的幽灵
typescript
// 错误做法:在组件中直接创建请求
@Component
struct LeakyComponent {
aboutToAppear() {
const httpRequest = http.createHttp();
httpRequest.request(url, (err, data) => {
// 处理响应
// 问题:忘记调用httpRequest.destroy()
});
}
}
// 正确做法:使用可销毁的管理器
class RequestManager {
private requests: http.HttpRequest[] = [];
createRequest(): http.HttpRequest {
const request = http.createHttp();
this.requests.push(request);
return request;
}
destroyAll() {
this.requests.forEach(req => req.destroy());
this.requests = [];
}
}
2. 并发的陷阱
typescript
// 错误做法:并发请求共享同一个实例
async function fetchMultipleData() {
const httpRequest = http.createHttp();
// 并发请求,但共享同一个实例
const [user, orders] = await Promise.all([
httpRequest.request(userUrl),
httpRequest.request(ordersUrl) // 可能失败!
]);
httpRequest.destroy();
}
// 正确做法:每个请求独立实例
async function fetchMultipleDataCorrectly() {
const [user, orders] = await Promise.all([
this.createRequest(userUrl),
this.createRequest(ordersUrl)
]);
return { user, orders };
}
private async createRequest(url: string): Promise<any> {
const httpRequest = http.createHttp();
try {
const response = await httpRequest.request(url);
return response.result;
} finally {
httpRequest.destroy();
}
}
3. 超时设置的学问
typescript
// 不同场景的超时策略
const timeoutStrategies = {
// 关键操作:短超时 + 快速失败
critical: {
connectTimeout: 3000, // 3秒连接超时
readTimeout: 5000 // 5秒读取超时
},
// 文件上传:长超时
upload: {
connectTimeout: 10000, // 10秒连接超时
readTimeout: 300000 // 5分钟读取超时(大文件)
},
// 实时聊天:中等超时 + 重试
chat: {
connectTimeout: 5000,
readTimeout: 10000,
retryCount: 2
}
};
八、性能优化:让HTTP请求"飞"起来
1. 连接池复用
typescript
class ConnectionPool {
private pool: Map<string, http.HttpRequest[]> = new Map();
private maxPoolSize = 5;
getRequest(baseUrl: string): http.HttpRequest {
const pool = this.pool.get(baseUrl) || [];
// 复用空闲连接
if (pool.length > 0) {
return pool.pop()!;
}
// 创建新连接
return http.createHttp();
}
releaseRequest(baseUrl: string, request: http.HttpRequest): void {
const pool = this.pool.get(baseUrl) || [];
if (pool.length < this.maxPoolSize) {
pool.push(request);
this.pool.set(baseUrl, pool);
} else {
request.destroy();
}
}
}
2. 请求去重
typescript
class RequestDeduplicator {
private pendingRequests: Map<string, Promise<any>> = new Map();
async deduplicatedRequest<T>(
key: string,
requestFn: () => Promise<T>
): Promise<T> {
// 如果已有相同请求在进行中,直接复用Promise
if (this.pendingRequests.has(key)) {
return this.pendingRequests.get(key) as Promise<T>;
}
const promise = requestFn().finally(() => {
this.pendingRequests.delete(key);
});
this.pendingRequests.set(key, promise);
return promise;
}
}
// 使用示例
const deduplicator = new RequestDeduplicator();
// 多个组件同时请求用户信息,只会发一次请求
const user1 = await deduplicator.deduplicatedRequest(
'user_123',
() => apiClient.get('/api/user/123')
);
const user2 = await deduplicator.deduplicatedRequest(
'user_123',
() => apiClient.get('/api/user/123') // 不会真正执行
);
3. 智能缓存策略
typescript
interface CacheEntry {
data: any;
timestamp: number;
expiresIn: number;
staleWhileRevalidate?: boolean;
}
class SmartCache {
private cache: Map<string, CacheEntry> = new Map();
async getOrFetch<T>(
key: string,
fetchFn: () => Promise<T>,
options: {
expiresIn: number; // 缓存过期时间(毫秒)
staleWhileRevalidate?: boolean; // 是否允许过期后继续使用旧数据
}
): Promise<T> {
const entry = this.cache.get(key);
const now = Date.now();
// 缓存命中
if (entry) {
const isFresh = now - entry.timestamp < entry.expiresIn;
const isStaleAcceptable = entry.staleWhileRevalidate &&
now - entry.timestamp < entry.expiresIn * 2;
if (isFresh || isStaleAcceptable) {
// 如果允许过期后使用旧数据,在后台更新缓存
if (!isFresh && entry.staleWhileRevalidate) {
this.refreshInBackground(key, fetchFn);
}
return entry.data;
}
}
// 缓存未命中或已过期,重新获取
const data = await fetchFn();
this.cache.set(key, {
data,
timestamp: now,
expiresIn: options.expiresIn,
staleWhileRevalidate: options.staleWhileRevalidate
});
return data;
}
private async refreshInBackground(key: string, fetchFn: () => Promise<any>): void {
// 在后台更新缓存,不影响当前请求
setTimeout(async () => {
try {
const data = await fetchFn();
this.cache.set(key, {
data,
timestamp: Date.now(),
expiresIn: 5 * 60 * 1000 // 5分钟
});
} catch (error) {
console.warn(`后台缓存更新失败: ${key}`, error);
}
}, 0);
}
}
九、监控和调试:给HTTP请求装上"眼睛"
1. 请求日志记录
typescript
class RequestLogger {
logRequest(config: RequestConfig, startTime: number): void {
const duration = Date.now() - startTime;
console.info(`[HTTP] ${config.method} ${config.url} - ${duration}ms`);
// 发送到监控平台
this.sendToAnalytics({
type: 'http_request',
method: config.method,
url: config.url,
duration: duration,
timestamp: new Date().toISOString()
});
}
logError(config: RequestConfig, error: Error, duration: number): void {
console.error(`[HTTP ERROR] ${config.method} ${config.url}`, error);
// 错误上报
this.sendToErrorTracking({
type: 'http_error',
method: config.method,
url: config.url,
error: error.message,
duration: duration,
stack: error.stack
});
}
}
2. 性能监控面板
typescript
@Component
struct RequestMonitor {
@State requests: RequestMetric[] = [];
build() {
Column() {
// 请求统计
Text(`总请求数: ${this.requests.length}`)
Text(`成功: ${this.requests.filter(r => r.success).length}`)
Text(`失败: ${this.requests.filter(r => !r.success).length}`)
Text(`平均耗时: ${this.getAverageDuration()}ms`)
// 请求列表
List() {
ForEach(this.requests, (metric) => {
ListItem() {
RequestMetricItem({ metric: metric })
}
})
}
}
}
}
十、总结一下下:HTTP请求的"道"与"术"
写了这么多代码,看了这么多配置,其实HTTP请求的核心就三件事:
第一,可靠性。你的请求能不能在各种网络环境下都正常工作?弱网怎么办?服务器出错怎么办?Token过期怎么办?这些问题的答案,决定了应用的稳定性。
第二,性能。用户不会等你。研究显示,页面加载时间每增加1秒,转化率下降7%。缓存、压缩、连接复用------这些优化手段,都是为了把那1秒抢回来。
第三,可维护性。三个月后,你还能看懂自己的代码吗?新同事接手时,需要花多少时间理解你的网络层?清晰的架构、完善的注释、统一的错误处理,这些"软实力"往往比技术本身更重要。
HarmonyOS的HTTP模块,从最初的"能用就行",到现在的功能丰富,再到鸿蒙6的智能化,一直在进化。但工具再先进,也替代不了开发者的思考。
下次写HTTP请求时,不妨问自己三个问题:
- 这个请求失败了对用户有什么影响?
- 有没有更高效的方式获取这些数据?
- 如果明天要换网络库,改动成本有多大?
想清楚这些问题,你的HTTP代码就不会差。记住:好的网络层代码,是让业务代码感受不到网络的存在。用户看到的是流畅的界面、及时的数据更新,而不是"加载中"的转圈圈。
最后的实用小小建议:
- 一定要加权限 :
ohos.permission.INTERNET,说三遍都不够 - 一定要销毁 :每个
createHttp()都要配一个destroy() - 一定要处理错误:用户讨厌白屏,更讨厌莫名其妙的失败
- 一定要监控:没有监控的代码就像闭着眼睛开车
- 一定要测试弱网:在你的WiFi环境下跑得飞快,不代表在电梯里也行
从今天开始,不要再写"能跑就行"的HTTP代码了。让你的每一个请求,都经得起推敲,扛得住压力,对得起用户。