RxHarmony:响应式编程思想在鸿蒙中的应用(45)
2026/7/24 22:05:53 网站建设 项目流程

在鸿蒙(HarmonyOS)原生开发中,官方提供的@ohos.net.http模块虽然灵活,但原生写法较为冗长,且缺乏原生的拦截器(Interceptor)机制。在实际工程中,我们通常需要手动封装拦截器来实现 Token 自动注入、统一错误处理以及泛型数据解析。

以下是基于@ohos.net.http封装通用网络请求工具类的完整实战:

一、 定义统一的响应数据模型

为了规范后端返回的数据结构,建议定义一个包含泛型的响应包装类,将codemessage和核心data统一管理:

// ResponseResult.ets export class ResponseResult<T> { code: number; msg: string; data?: T; constructor(code: number, msg: string, data?: T) { this.code = code; this.msg = msg; this.data = data; } }

二、 设计并实现拦截器接口

通过定义拦截器接口,我们可以将“请求前处理”和“响应后处理”抽离出来,实现 AOP(面向切面)的效果:

// Interceptors.ets import { http } from '@kit.NetworkKit'; // 1. 请求拦截器接口 export interface HttpRequestInterceptor { intercept: (options: http.HttpRequestOptions) => http.HttpRequestOptions; } // 2. 响应拦截器接口 export interface HttpResponseInterceptor { intercept: (response: http.HttpResponse) => http.HttpResponse; } // 3. 默认请求拦截器示例:自动注入 Token export class DefaultHeadersInterceptor implements HttpRequestInterceptor { intercept(options: http.HttpRequestOptions): http.HttpRequestOptions { const headers = options.header as Record<string, string>; const token = 'your_auth_token'; // 实际项目中可从安全存储中获取 if (token) { headers['Authorization'] = `Bearer ${token}`; } return options; } } // 4. 默认响应拦截器示例:统一处理错误码 export class DefaultResponseInterceptor implements HttpResponseInterceptor { intercept(response: http.HttpResponse): http.HttpResponse { if (response.responseCode === 401) { // 触发全局提示或跳转登录页 console.error('Token 失效,请重新登录'); } return response; } }

三、 封装核心网络请求类 (NetworkManager)

将拦截器链、URL 拼接、超时配置以及泛型解析封装在一个核心类中:

// NetworkManager.ets import { http } from '@kit.NetworkKit'; import { ResponseResult } from './ResponseResult'; import { HttpRequestInterceptor, HttpResponseInterceptor, DefaultHeadersInterceptor, DefaultResponseInterceptor } from './Interceptors'; export class NetworkManager { private baseURL: string; private requestInterceptors: HttpRequestInterceptor[] = []; private responseInterceptors: HttpResponseInterceptor[] = []; constructor(baseURL: string) { this.baseURL = baseURL; // 默认注入基础拦截器 this.requestInterceptors.push(new DefaultHeadersInterceptor()); this.responseInterceptors.push(new DefaultResponseInterceptor()); } // 支持外部动态添加拦截器 addRequestInterceptor(interceptor: HttpRequestInterceptor) { this.requestInterceptors.push(interceptor); } // 核心请求方法,支持泛型 T async request<T>(path: string, options: http.HttpRequestOptions): Promise<ResponseResult<T>> { let fullUrl = this.baseURL + path; let requestOptions = { ...options }; // 1. 执行请求拦截器链 this.requestInterceptors.forEach(interceptor => { requestOptions = interceptor.intercept(requestOptions); }); const httpRequest = http.createHttp(); try { // 2. 发起网络请求 let response = await httpRequest.request(fullUrl, requestOptions); // 3. 执行响应拦截器链 this.responseInterceptors.forEach(interceptor => { response = interceptor.intercept(response); }); // 4. 解析数据并封装为统一格式 const resultData = JSON.parse(response.result.toString()) as T; if (response.responseCode === http.ResponseCode.OK) { return new ResponseResult<T>(200, 'success', resultData); } else { return new ResponseResult<T>(response.responseCode, `请求失败: ${response.responseCode}`); } } catch (error) { return new ResponseResult<T>(-1, error.message || '网络请求异常'); } finally { // 5. 销毁请求实例,释放资源 httpRequest.destroy(); } } // 封装常用的 GET 方法 async get<T>(path: string, params?: Record<string, any>): Promise<ResponseResult<T>> { return this.request<T>(path, { method: http.RequestMethod.GET, extraData: params // GET请求参数 }); } // 封装常用的 POST 方法 async post<T>(path: string, data?: object): Promise<ResponseResult<T>> { return this.request<T>(path, { method: http.RequestMethod.POST, header: { 'Content-Type': 'application/json' }, extraData: JSON.stringify(data) }); } }

四、 业务层调用示例

封装完成后,业务层(如 ViewModel)只需关注接口路径和数据类型,无需关心底层的 Token 注入和 JSON 解析:

// UserViewModel.ets import { NetworkManager } from './NetworkManager'; interface UserInfo { id: number; name: string; } const network = new NetworkManager('https://api.example.com'); export class UserViewModel { async fetchUserProfile(userId: number) { // 发起请求,自动带上 Token,自动解析为 UserInfo 类型 const res = await network.get<UserInfo>(`/user/${userId}`); if (res.code === 200 && res.data) { console.log(`获取到用户: ${res.data.name}`); } else { console.error(`获取失败: ${res.msg}`); } } }

五、 请求并发控制与防抖节流

在鸿蒙 UI 交互中,用户快速滑动列表或连续点击按钮极易触发大量重复请求。

  • 请求去重(防抖):NetworkManager内部维护一个pendingRequests: Map<string, Promise<any>>。发起请求前,通过URL + 参数生成唯一 Key,若该 Key 已存在,则直接返回缓存的 Promise,避免重复发起网络请求。
  • 并发数限制:对于列表页的瀑布流加载,同时发起几十个图片请求会导致 OOM 或系统限流。可以引入类似p-limit的并发控制逻辑,限制同时处于Pending状态的请求数量(如最大 5 个),超出的请求放入等待队列。
1. 请求去重(防抖与拦截)实现

通过维护一个Map来缓存正在执行的 Promise,可以完美解决快速滑动或连点导致的重复请求问题。

class RequestDeduplicationController { // 核心:维护一个正在执行的请求池 private pendingRequests: Map<string, Promise<any>> = new Map(); /** * @param key 请求的唯一标识(如 URL + 序列化后的参数) * @param requestFn 实际的网络请求函数 */ async deduplicatedRequest<T>(key: string, requestFn: () => Promise<T>): Promise<T> { // 1. 拦截:如果该 Key 已有请求在执行,直接返回缓存的 Promise if (this.pendingRequests.has(key)) { console.info(`[RequestDedup] 命中去重拦截: ${key}`); return this.pendingRequests.get(key) as Promise<T>; } // 2. 发起新请求并加入池中 const requestPromise = requestFn(); this.pendingRequests.set(key, requestPromise); try { const result = await requestPromise; return result; } finally { // 3. 无论成功或失败,请求结束后必须从池中移除 this.pendingRequests.delete(key); } } } // 使用场景示例: const dedupController = new RequestDeduplicationController(); // 在组件中触发请求 async function fetchUserDetail(userId: string) { const key = `user_detail_${userId}`; return dedupController.deduplicatedRequest(key, async () => { // 实际的 HTTP 请求 return await httpRequest.get(`/api/users/${userId}`); }); }
2. 并发数限制(类似 p-limit)实现

对于瀑布流加载等场景,我们需要一个任务队列和并发计数器来限制同时处于 Pending 状态的请求数量。

class PLimitController { private queue: Array<() => void> = []; // 等待队列 private activeCount: number = 0; // 当前正在执行的任务数 private concurrency: number; // 最大并发数 constructor(concurrency: number) { this.concurrency = concurrency; } /** * 包装异步任务 */ async enqueue<T>(taskFn: () => Promise<T>): Promise<T> { return new Promise((resolve, reject) => { // 将任务包装后推入队列 this.queue.push(async () => { try { const result = await taskFn(); resolve(result); } catch (error) { reject(error); } finally { // 任务执行完毕,释放并发槽位,并触发下一个任务 this.activeCount--; this.runNext(); } }); // 尝试启动任务 this.runNext(); }); } private runNext(): void { // 如果未达到并发上限且队列不为空,则取出任务执行 if (this.activeCount < this.concurrency && this.queue.length > 0) { this.activeCount++; const nextTask = this.queue.shift(); nextTask?.(); } } } // 使用场景示例: // 限制图片请求最大并发数为 5 const imageLimit = new PLimitController(5); // 在列表加载时批量发起请求 async function loadWaterfallImages(imageUrls: string[]) { const tasks = imageUrls.map(url => imageLimit.enqueue(() => fetchImageFromNetwork(url))); // 等待所有图片下载完成(实际并发被严格控制在5个以内) const results = await Promise.all(tasks); return results; }
3. 进阶优化:结合防抖(Debounce)

对于搜索框输入等高频触发事件,除了去重,还需要加入时间维度的防抖控制:

class SearchRequestManager { private debounceTimer: number = -1; private readonly DEBOUNCE_DELAY: number = 300; // 300ms 防抖 triggerSearch(query: string, callback: (res: any) => void) { // 清除上一个定时器 clearTimeout(this.debounceTimer); // 重新计时,只有用户停顿 300ms 后才真正发起请求 this.debounceTimer = setTimeout(async () => { const result = await fetchSearchResults(query); callback(result); }, this.DEBOUNCE_DELAY) as unknown as number; } }

六、 多级缓存策略(Cache-First 模式)

鸿蒙设备通常对流畅度要求极高,纯网络请求在弱网下体验极差。

  • 内存缓存(LruCache):对于高频访问且数据量较小的字典类接口,使用内存 LRU 缓存,实现毫秒级响应。
  • 持久化缓存:结合鸿蒙的PreferencesRDB(关系型数据库),将核心数据落盘。在发起网络请求时,采用“先展示缓存数据,后台静默刷新,刷新成功后更新 UI”的策略(即 Stale-While-Revalidate 模式)。

七、 异常精细化处理与重试机制

基础的try-catch往往无法应对复杂的网络环境。

  • 网络状态监听:利用鸿蒙的@ohos.net.connection模块监听网络状态。当检测到断网时,直接拦截请求并返回离线提示,避免无意义的超时等待。
  • 智能重试(Retry):对于502 Bad Gateway504 Gateway Timeout或网络抖动导致的异常,在响应拦截器中实现指数退避重试算法(Exponential Backoff),自动重试 1~3 次。
  • Token 无感刷新:在响应拦截器中捕获401 Unauthorized。此时不要立即跳转登录页,而是挂起当前请求,调用刷新 Token 接口。刷新成功后,使用新 Token 重新发起被挂起的请求;若刷新失败,再强制登出。

八、 拥抱鸿蒙原生能力:升级 @kit.NetworkKit

在 HarmonyOS NEXT(API 12+)中,官方对网络模块进行了重构。

  • 废弃旧模块:@ohos.net.http已被标记为即将废弃,建议全面迁移至@kit.NetworkKit
  • 使用 HttpClient:新版HttpClient提供了更现代的 API 设计,原生支持配置连接池、TLS 证书校验、以及更优雅的异步回调机制,大幅提升了底层网络请求的性能和安全性。

九、 安全合规与证书固定(Certificate Pinning)

金融级或高安全要求的鸿蒙应用,必须防范中间人攻击(MITM)。

  • 证书固定:NetworkManager初始化时,通过鸿蒙的caCerts配置项,将服务端的公钥证书或 CA 根证书硬编码到客户端。即使攻击者伪造了系统级证书,网络请求也会因证书校验失败而被拦截。
  • 敏感数据脱敏:在请求拦截器打印日志时,务必对 Header 中的Authorization和 Body 中的密码、身份证号等字段进行掩码处理,防止在 Debug 模式下发生数据泄露。

十、 结合 MVVM 与 DI 的终极形态

将网络层与前面讨论的架构深度结合:

  • DI 注入网络实例:通过@provide将配置好拦截器和缓存策略的NetworkManager注入到 IoC 容器中。
  • Repository 模式:ViewModel 不再直接调用NetworkManager,而是调用UserRepository。Repository 内部决定是先读缓存还是先走网络,并将结果封装为FlowStateFlow(响应式流),View 层只需绑定状态,即可实现极致丝滑的数据驱动 UI。
1. 配置网络实例与 IoC 容器注入

首先,我们需要一个配置好拦截器、缓存策略和并发控制的NetworkManager。在鸿蒙或跨平台架构中,可以通过 IoC(控制反转)容器将其作为单例注入。

// 1. 定义网络管理器接口(面向抽象编程) export interface INetworkManager { get<T>(url: string, params?: Record<string, any>): Promise<T>; } // 2. 实现网络管理器(整合拦截器与缓存) export class NetworkManager implements INetworkManager { private httpClient: HttpClient; constructor() { this.httpClient = new HttpClient(); // 注入请求拦截器(如统一添加 Token) this.httpClient.addRequestInterceptor((config) => { config.headers['Authorization'] = `Bearer ${getToken()}`; return config; }); // 注入响应拦截器(统一处理错误码) this.httpClient.addResponseInterceptor((response) => { if (response.code !== 200) throw new Error(response.msg); return response.data; }); } async get<T>(url: string, params?: Record<string, any>): Promise<T> { // 内部可结合前面讨论的并发控制和去重逻辑 return this.httpClient.request(url, { method: 'GET', params }) as Promise<T>; } } // 3. IoC 容器注册(以伪代码/通用DI框架为例) // 将配置好的 NetworkManager 作为单例注入到全局容器 Container.provide('INetworkManager', () => new NetworkManager(), { singleton: true });
2. Repository 模式封装数据策略

Repository 是数据的统一入口。它不关心数据是来自本地数据库还是网络,而是根据业务逻辑(如:先读缓存,无缓存则请求网络并更新缓存)协调数据源。

export class UserRepository { // 通过构造函数注入网络实例,实现解耦 constructor( private networkManager: INetworkManager, private localCache: ILocalCache ) {} /** * 获取用户列表(响应式流) */ async *fetchUsers(page: number): AsyncGenerator<User[], void, undefined> { // 1. 优先读取本地缓存,实现首屏秒开 const cachedUsers = await this.localCache.getUsers(page); if (cachedUsers) yield cachedUsers; // 2. 发起网络请求 try { const remoteUsers = await this.networkManager.get<User[]>(`/api/users`, { page }); // 3. 将最新数据写入本地缓存 await this.localCache.saveUsers(page, remoteUsers); // 4. 发射最新网络数据,触发 UI 更新 yield remoteUsers; } catch (error) { // 如果网络失败且没有缓存,抛出异常交由 ViewModel 处理 if (!cachedUsers) throw error; } } }
3. ViewModel 消费响应式流

ViewModel 负责持有 Repository 并将数据转化为 UI 层可直接绑定的状态(如StateFlow)。

export class UserViewModel { private userRepository: UserRepository; // 定义 UI 状态(密封类/联合类型) private _uiState = new StateFlow<UserUiState>(new LoadingState()); public get uiState(): StateFlow<UserUiState> { return this._uiState; } constructor() { // 从 IoC 容器获取依赖 const network = Container.resolve<INetworkManager>('INetworkManager'); const cache = Container.resolve<ILocalCache>('ILocalCache'); this.userRepository = new UserRepository(network, cache); } public async loadUsers(page: number) { try { this._uiState.value = new LoadingState(); // 消费 Repository 提供的响应式流 for await (const users of this.userRepository.fetchUsers(page)) { this._uiState.value = new SuccessState(users); } } catch (error) { this._uiState.value = new ErrorState((error as Error).message); } } }
4. View 层声明式绑定

在鸿蒙 ArkUI 等声明式 UI 框架中,View 层只需监听 ViewModel 的状态变化,实现零业务代码的极致丝滑渲染。

@Component export struct UserListPage { @ObjectLink vm: UserViewModel; @State currentState: UserUiState = new LoadingState(); aboutToAppear() { // 监听 StateFlow 的变化,自动映射到组件状态 this.vm.uiState.subscribe((state) => { this.currentState = state; }); this.vm.loadUsers(1); } build() { Column() { if (this.currentState instanceof LoadingState) { LoadingProgress() } else if (this.currentState instanceof ErrorState) { Text(`加载失败: ${this.currentState.message}`) } else if (this.currentState instanceof SuccessState) { List() { ForEach(this.currentState.users, (user: User) => { ListItem() { Text(user.name) } }) } } } } }

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询