尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

华为鸿蒙开发高级篇05-网络层架构实战:统一请求层设计

华为鸿蒙开发高级篇05-网络层架构实战:统一请求层设计 华为鸿蒙开发高级篇05-网络层架构实战统一请求层设计新闻客户端案例高级系列第 5 篇 · 案例新闻客户端——列表、详情两级页面要求缓存、重试、并发限制与错误提示统一。 目标把到处写 http 请求升级为统一请求层请求封装、拦截器、缓存/重试/取消、错误码归一一套代码服务所有页面。一、案例背景业务页面一多网络代码会出现这些失控信号每个页面自己写createHttp()/destroy()连接泄漏时隐时现Token 过期、错误码提示各写各的体验不一致断网、弱网场景没有统一兜底缓存、重试并发请求没限制弱网时把带宽和 CPU 打满。本文以新闻列表 详情为案例搭建一个可直接复制的统一请求层。二、核心原理请求层分层页面View── ApiClient唯一入口── 拦截器鉴权/日志── 底层请求http/axios │ ├── 缓存层内存/磁盘 ├── 重试 并发控制 └── 错误归一业务错误码 → 统一提示原则页面只依赖 ApiClient 的领域方法getNewsList()不感知 HTTP 细节。拦截器收敛横切逻辑Token、日志、错误上报都在这里。错误在出口归一网络异常、业务错误码统一转成ApiExceptionUI 只做一次错误展示。三、实际开发代码统一请求层3.1 领域模型与错误类型// net/ApiTypes.ets export interface NewsItem { id: number title: string summary: string source: string publishTime: string url: string } // 业务统一异常UI 只需读 message 展示 export class ApiException extends Error { code: number constructor(code: number, message: string) { super(message) this.code code } } // 统一响应包装 export interface ApiResponseT { code: number message: string data: T }3.2 底层请求封装http 封装// net/HttpClient.ets import { http } from kit.NetworkKit import { ApiException, ApiResponse } from ./ApiTypes export class HttpClient { private baseURL: string private timeout: number constructor(baseURL: string, timeout 15000) { this.baseURL baseURL this.timeout timeout } // 泛型请求方法页面与业务层只认识 T不认识 http async requestT( path: string, method: http.RequestMethod, params?: Recordstring, string | number | undefined, body?: string ): PromiseT { // 1) 拼 query let url this.baseURL path if (params) { const qs Object.entries(params) .filter(([, v]) v ! undefined) .map(([k, v]) ${k}${encodeURIComponent(String(v))}) .join() if (qs) url (url.includes(?) ? : ?) qs } // 2) 发送请求拦截器逻辑见 3.3这里保持单纯 const req http.createHttp() try { const resp await req.request(url, { method, connectTimeout: this.timeout, readTimeout: this.timeout, header: { Content-Type: application/json }, extraData: body }) // 3) 状态码归一 if (resp.responseCode ! 200) { throw new ApiException(resp.responseCode, 服务器异常(${resp.responseCode})) } // 4) 业务码归一 const apiResp JSON.parse(resp.result as string) as ApiResponseT if (apiResp.code ! 0) { throw new ApiException(apiResp.code, apiResp.message || 业务处理失败) } return apiResp.data } catch (e) { // 网络层错误也统一为 ApiException if (e instanceof ApiException) throw e throw new ApiException(-1, 网络异常请检查网络连接) } finally { req.destroy() // 铁律用完销毁 } } }3.3 拦截器鉴权与日志拦截器统一做加 Token、打日志、上报错误// net/Interceptors.ets import { ApiException } from ./ApiTypes export class ApiInterceptor { // 请求前注入 Token 等公共头在 HttpClient.request 中调用 onRequest(headers: Recordstring, string): Recordstring, string { const token AppStorage.getstring(token) if (token) { headers[Authorization] Bearer ${token} } return headers } // 请求后401 统一处理如触发重新登录 onResponse(code: number): void { if (code 401) { // 触发全局登出/跳登录页按业务实现 AppStorage.set(token, ) } } // 统一错误上报接打点/崩溃平台 onError(e: ApiException): void { console.error([API] code${e.code} msg${e.message}) // 真实项目上报到监控平台 } }把拦截器接入HttpClient.request在发送前与收尾处各调用一次见 3.4 的组合封装。3.4 ApiClient页面唯一入口缓存/重试/并发限制// net/ApiClient.ets import { http } from kit.NetworkKit import { HttpClient } from ./HttpClient import { ApiInterceptor } from ./Interceptors import { ApiException, NewsItem } from ./ApiTypes export class ApiClient { private client: HttpClient new HttpClient(https://api.example-news.com) private interceptor: ApiInterceptor new ApiInterceptor() // 简单的内存缓存path - { time, data } private cache: Mapstring, { time: number; data: object } new Map() private cacheTtl 5 * 60 * 1000 // 5 分钟 private inflight: Mapstring, Promiseobject new Map() // 去重进行中的请求 // 领域方法页面直接调用 async getNewsList(page: number, useCache true): PromiseNewsItem[] { const path /v1/news/list return this.requestNewsItem[]({ path, method: http.RequestMethod.GET, params: { page, size: 20 }, useCache }) } async getNewsDetail(id: number): PromiseNewsItem { return this.requestNewsItem({ path: /v1/news/${id}, method: http.RequestMethod.GET, useCache: true }) } // 统一调度缓存 → 去重 → 拦截器 → 底层请求 → 重试 private async requestT(cfg: { path: string method: http.RequestMethod params?: Recordstring, string | number | undefined body?: string useCache?: boolean retries?: number }): PromiseT { const { path, method, params, body, useCache false, retries 1 } cfg const cacheKey path ? JSON.stringify(params ?? {}) // 1) 命中缓存直接返回 if (useCache) { const hit this.cache.get(cacheKey) if (hit Date.now() - hit.time this.cacheTtl) { return hit.data as T } } // 2) 同一请求在途则复用防止重复请求 const inflight this.inflight.get(cacheKey) if (inflight) return inflight as PromiseT // 3) 发起带重试 const promise this.doRequestT(cacheKey, path, method, params, body, retries, useCache) this.inflight.set(cacheKey, promise) try { return await promise } finally { this.inflight.delete(cacheKey) } } private async doRequestT( cacheKey: string, path: string, method: http.RequestMethod, params: Recordstring, string | number | undefined | undefined, body: string | undefined, retries: number, useCache: boolean ): PromiseT { // 拦截器请求前注入头 const headers this.interceptor.onRequest({ Content-Type: application/json }) let lastError: Error | undefined for (let attempt 0; attempt retries; attempt) { try { const data await this.client.requestT(path, method, params, body) if (useCache) this.cache.set(cacheKey, { time: Date.now(), data }) return data } catch (e) { lastError e as Error this.interceptor.onError(e as ApiException) if (e instanceof ApiException e.code 401) break // 鉴权失败不重试 // 网络类错误才重试且非最后一次失败前稍等 if (attempt retries) { await new Promise((r) setTimeout(r, 300 * (attempt 1))) } } } throw lastError } // 供下拉刷新手动清缓存 clearCache(): void { this.cache.clear() } }3.5 页面使用干净的业务代码// pages/NewsListPage.ets import { ApiClient } from ../net/ApiClient import { NewsItem } from ../net/ApiTypes Entry Component struct NewsListPage { private api: ApiClient new ApiClient() State news: NewsItem[] [] State errorMsg: string State loading: boolean false private page: number 1 aboutToAppear() { this.loadNews(true) } private async loadNews(refresh: boolean) { if (this.loading) return this.loading true this.errorMsg try { const list await this.api.getNewsList(this.page, refresh ? true : false) this.news refresh ? list : this.news.concat(list) if (refresh) this.page 1 } catch (e) { this.errorMsg (e as Error).message } finally { this.loading false } } build() { Column() { List() { ForEach(this.news, (item: NewsItem) { ListItem() { Column({ space: 4 }) { Text(item.title).fontSize(16).fontWeight(FontWeight.Medium) Text(item.summary).fontSize(13).fontColor(#86909C).maxLines(2) } .alignItems(HorizontalAlign.Start) .padding(12) .width(100%) } }, (item: NewsItem) ${item.id}) } .layoutWeight(1) if (this.errorMsg) { Text(this.errorMsg).fontColor(#E84026).fontSize(13).padding(8) } // 下拉刷新用 Refresh 包裹 List加载更多在最后一项 onAppear 触发 } .width(100%).height(100%) } }四、优化与踩坑问题处理连接泄漏每个请求createHttp()finally destroy()封装层统一保证缓存过期数据TTL 校验 下拉刷新时clearCache详情页返回列表保持一致用同一缓存策略弱网反复失败网络错误重试 1~2 次 退避业务错误4xx/业务码不重试重复请求风暴inflightMap 去重同一 key 在途请求复用同一 Promise401 循环拦截器统一登出并 break 重试刷新 Token 场景用刷新队列一个在刷其余等待内存缓存无限增长限制条数如 LRU 或超 200 条清空五、小结与延伸统一请求层 HttpClient底层 拦截器横切 ApiClient缓存/去重/重试 领域方法页面视角。案例沉淀新闻列表/详情的缓存与刷新闭环模板可直接套到任何列表详情业务。延伸磁盘缓存接第 6 篇 RDB 或 Preferences、上传下载封装进度回调 断点、请求优先级与取消离开页面时 abort 在途请求。下一篇预告数据持久化进阶——关系型数据库 RDB 实战笔记 App 数据层案例。
返回列表