ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

HarmonyOS开发:ArkTS封装Axios网络请求工具类全指南

HarmonyOS开发:ArkTS封装Axios网络请求工具类全指南 做HarmonyOS应用开发这段时间我踩得最多的坑反而不在UI而在网络层。业务逻辑跑通了结果每个页面都得写一模一样的axios请求、loading逻辑、错误提示代码越堆越乱。后来我干脆花了一下午用ArkTS把Axios封装成了一个通用的网络请求工具类从此所有页面统一入口加token、弹错误、处理超时都走同一套逻辑。不管你是刚接触鸿蒙开发的新人还是已经在业务里被重复代码折磨的开发者这篇文章里的思路和代码都可以直接搬走用。这篇文章不会只丢一段代码给你我会把封装前怎么想、封装时怎么写、封装后怎么用、踩坑了怎么查全部拆开讲清楚。重点是让你能用ArkTS写出一套真正适合HarmonyOS项目的网络请求层而不是简单把Web端axios代码复制过来。1. 为什么我不直接用ohos.net.http1.1 原生http模块的痛点刚开始做鸿蒙应用的时候我第一反应是用官方推荐的ohos.net.http。这玩意儿不是不能用而是用起来太“原始”了。你想统一处理登录失效得在每个请求成功回调里判断statusCode再手动跳转登录页你想在请求头里加一个token每个页面都要把token从存储里拿出来拼一次你想做个请求超时提示还得给每个模块都写一遍try-catch。项目一大了这些重复劳动特别消磨人而且很容易漏。更麻烦的是ohos.net.http的回调风格是(err, data)一旦业务嵌套多了回调地狱马上出现。虽然有Promise封装方案但原生模块本身没有“拦截器”这种概念你想在发出请求前统一做点事几乎没有优雅的入口。1.2 为什么选Axios而不是自己造轮子选择ohos/axios最直接的原因就是它和Web端axios的API基本一致。团队里做过Web或小程序开发的同事几乎零成本上手。它的Promise风格、拦截器机制、取消请求机制、上传下载进度回调都是现成的不用我再实现一遍“请请求 → 收到数据 → 转成对象”这条链路。另外一个关键点是ohos/axios已经适配了HarmonyOS的网络栈底层走的是系统的HTTP能力不是简单的Web能力套壳。在API 9及以上的鸿蒙设备上支持HTTP/HTTPS、支持FormData文件上传、支持AbortController取消这些对我们封装一个通用工具类来说已经足够了。对比项ohos.net.httpohos/axios编程风格回调为主Promise/async-await拦截器无请求/响应拦截器取消请求手动管理AbortController/CancelToken上传进度需要自己封装内置onUploadProgress统一错误处理靠业务层自己写响应拦截器集中处理生态熟悉度鸿蒙特有全平台通用API1.3 封装这个工具类到底为了什么封装不是炫技是为了让业务层写代码的时候不需要关心“token怎么带”“超时怎么处理”“后端返回code401怎么办”这些事。我希望达成这几个目标第一所有请求都走同一个入口想在入口加日志、加埋点、加公共参数只改一处第二把网络异常、HTTP错误、业务错误统一成一种HttpError业务层catch的时候只管处理不用判断来源第三用泛型把返回数据直接映射成业务类型调用方拿到手就是对象不用自己再JSON.parse一次。说白了封装完以后业务代码里一行“await getUserInfo()”背后发生什么调用方不用管。这就是网络请求层的价值。2. 工具类设计思路先想清楚再写代码2.1 封装前先列功能清单我习惯先列需求再动手不然写着写着容易跑偏。我给这个工具类定的功能清单是这样的统一的baseURL和超时时间配置请求拦截器里自动注入token并支持某个请求跳过鉴权响应拦截器里统一处理网络错误、超时错误、HTTP状态码错误后端返回结构统一为{ code, message, data }根据code判断业务是否成功提供get、post、put、delete四个常用方法并且全部返回PromiseT支持上传文件时的进度回调支持按key取消请求避免页面退出后回调还在执行保留日志打印能力方便开发和排查问题这个清单看起来很长但每一项目标都很具体。实际写的时候你不需要一次全做完可以先实现核心的请求封装后面再按需加取消和上传进度。2.2 后端响应结构得先约定好封装能不能通用很大程度取决于后端返回结构是否统一。我这里默认后端返回的是这样一个JSON结构{ code: 0, message: success, data: {} }其中code0表示成功非0表示业务失败message是给用户看的提示data才是真正的业务数据。如果你的项目后端不是这个结构也没有关系把工具类里解析code的那一段改掉就可以了。我见过有些后端喜欢直接用HTTP状态码表达业务状态那就在响应拦截器里直接把response.data抛出来让调用方自己判断。2.3 目录结构规划建议把网络相关的代码单独放一个目录不要散落在页面里。我一般这样组织src/main/ets/ ├── config/ │ └── AppConfig.ets ├── http/ │ ├── HttpErrorCode.ets │ ├── Toast.ets │ └── HttpRequest.ets └── services/ └── UserApi.etsconfig目录放全局配置比如baseURL、默认超时时间http目录放错误码定义和HttpRequest核心类services目录放各业务模块的API集合。页面里只依赖services里的接口方法完全不直接碰axios对象这样以后想换网络库只需要改http目录。3. 完整代码从依赖安装到工具类落盘3.1 准备阶段安装依赖与权限配置先安装ohos/axios在DevEco Studio的终端里执行ohpm install ohos/axios安装完成后再检查module.json5里有没有配网络权限。HarmonyOS应用默认没有网络访问权限必须在module节点下加上{ module: { requestPermissions: [ { name: ohos.permission.INTERNET } ] } }这一步漏了的话请求发出去直接报“网络连接失败”而且控制台不会给你特别明显的提示。我第一次踩这个坑的时候排查了半天后来才发现是权限没加。另外HarmonyOS从API 9开始默认禁止HTTP明文请求。如果你只是本地开发调试用的是http://的测试地址会直接被拦截。有一个临时办法是在resources/base/profile/network_config.json里配置{ network-config: { cleartextTrafficPermitted: true } }然后在module.json5里声明{ module: { deviceTypes: [phone], metadata: [ { name: network_security_config, value: $profile:network_config } ] } }这个配置只建议开发调试阶段用上生产环境一定要走HTTPS并且把cleartextTrafficPermitted改回false。3.2 定义统一错误码与HttpError错误码和错误类是整个工具类的地基。我先定义一套错误码把网络错误、超时、取消、HTTP错误、业务错误都区分开// http/HttpErrorCode.ets export enum ErrorCode { NETWORK_ERROR NETWORK_ERROR, TIMEOUT TIMEOUT, CANCELED CANCELED, HTTP_ERROR HTTP_ERROR, BIZ_ERROR BIZ_ERROR, } export class HttpError extends Error { code: ErrorCode ErrorCode.NETWORK_ERROR; status: number 0; data: unknown null; constructor(code: ErrorCode, message: string, status?: number, data?: unknown) { super(message); this.name HttpError; this.code code; if (status ! undefined) { this.status status; } if (data ! undefined) { this.data data; } } }这里继承Error是为了让业务层可以用instanceof判断比如error instanceof HttpError。code字段用来区分错误类型status存HTTP状态码data放后端返回的原始数据方便排查问题。3.3 HttpRequest核心类实现接下来是重头戏HttpRequest核心类。这个类的代码会比较长我会分段贴出来并且说明每一段在干什么。先看整体框架// http/HttpRequest.ets import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse, AxiosError } from ohos/axios; import { ErrorCode, HttpError } from ./HttpErrorCode; export interface ApiRequestConfigT unknown { url: string; method?: GET | POST | PUT | DELETE; data?: T; params?: Recordstring, string | number | boolean | null; headers?: Recordstring, string; timeout?: number; skipAuth?: boolean; cancelKey?: string; onUploadProgress?: (loaded: number, total: number) void; } export interface ApiResponseT unknown { code: number; message: string; data: T; } export class HttpRequest { private static instance: HttpRequest new HttpRequest(); private client: AxiosInstance; private cancelMap: Mapstring, AbortController new Map(); private constructor() { this.client axios.create({ baseURL: https://api.example.com, timeout: 15000, headers: { Content-Type: application/json } }); this.setupInterceptors(); } static getInstance(): HttpRequest { return HttpRequest.instance; } }我把baseURL先写死成示例地址实际项目里建议单独放到AppConfig里。单例模式保证整个应用只有一个axios实例这样可以避免重复创建实例造成资源浪费也让拦截器只注册一次。接着是拦截器private setupInterceptors(): void { this.client.interceptors.request.use( (config: AxiosRequestConfig) { const token AppStorage.getstring(token); if (token !config.headers) { const header: Recordstring, string {}; header[Authorization] Bearer ${token}; config.headers header as Object; } else if (token config.headers) { const headers config.headers as Recordstring, string; headers[Authorization] Bearer ${token}; } return config; }, (error: AxiosError) { return Promise.reject(this.normalizeError(error)); } ); this.client.interceptors.response.use( (response: AxiosResponse) { return response; }, (error: AxiosError) { return Promise.reject(this.normalizeError(error)); } ); }请求拦截器做的事很简单从AppStorage里取token如果存在就塞到请求头里。你可能注意到了我用了config.headers as Object这种写法这是因为ArkTS对类型比较严格不能像Web端那样随便给对象加属性只能先转成Recordstring, string再赋值。等下踩坑部分我会详细讲这个。响应拦截器里把错误统一交给normalizeError处理。这里没有把所有错误都打平而是继续Promise.reject让上层使用try-catch或.catch去接收。接着是核心的request方法requestT(config: ApiRequestConfig): PromiseT { return new PromiseT((resolve, reject) { let controller: AbortController | undefined undefined; if (config.cancelKey) { controller new AbortController(); this.cancelMap.set(config.cancelKey, controller); } const requestConfig: AxiosRequestConfig { url: config.url, method: config.method ?? GET, data: config.data, params: config.params, headers: config.headers, timeout: config.timeout, signal: controller ? controller.signal : undefined }; this.client.requestApiResponseT(requestConfig) .then((response: AxiosResponseApiResponseT) { const res response.data; if (res.code 0) { resolve(res.data); } else { reject(new HttpError(ErrorCode.BIZ_ERROR, res.message, res.code)); } }) .catch((error: AxiosError | HttpError) { if (error instanceof HttpError) { reject(error); } else { reject(this.normalizeError(error)); } }); }); }这里最关键的是泛型T。业务层调用getUserInfo的时候T就变成了UserInfo最终resolve出去的就是UserInfo类型。ApiResponseT是后端响应结构从response.data里取出data字段返回给调用方。如果后端返回的code不是0就抛业务错误。然后是normalizeError和四个常用方法private normalizeError(error: AxiosError): HttpError { if (error.code ECONNABORTED) { return new HttpError(ErrorCode.TIMEOUT, 请求超时请稍后重试); } if (error.code ERR_CANCELED) { return new HttpError(ErrorCode.CANCELED, 请求已取消); } if (error.response error.response.status) { const status error.response.status; let message 请求失败; if (status 401) { message 登录已过期请重新登录; } else if (status 403) { message 当前账号没有权限访问; } else if (status 404) { message 请求的资源不存在; } else if (status 500) { message 服务器开小差了请稍后重试; } return new HttpError(ErrorCode.HTTP_ERROR, message, status); } return new HttpError(ErrorCode.NETWORK_ERROR, 网络连接失败请检查网络设置); } getT(url: string, params?: Recordstring, string | number | boolean | null, config?: ApiRequestConfig): PromiseT { return this.requestT({ ...config, url, method: GET, params }); } postT(url: string, data?: unknown, config?: ApiRequestConfig): PromiseT { return this.requestT({ ...config, url, method: POST, data }); } putT(url: string, data?: unknown, config?: ApiRequestConfig): PromiseT { return this.requestT({ ...config, url, method: PUT, data }); } deleteT(url: string, params?: Recordstring, string | number | boolean | null, config?: ApiRequestConfig): PromiseT { return this.requestT({ ...config, url, method: DELETE, params }); } cancelRequest(cancelKey: string): void { const controller this.cancelMap.get(cancelKey); if (controller) { controller.abort(); this.cancelMap.delete(cancelKey); } } }normalizeError根据axios返回的code字段和HTTP状态码把错误包装成统一的HttpError。注意这里ERR_CANCELED是axios在取消请求时会带的错误码这个判断能让取消操作不弹出错误提示。3.4 API层泛型映射工具类本身不关心业务真正的业务接口写在services里。举个例子登录接口长这样// services/UserApi.ets import { HttpRequest } from ../http/HttpRequest; export interface LoginParams { username: string; password: string; } export interface UserInfo { id: number; nickname: string; avatar: string; } export class UserApi { private static http: HttpRequest HttpRequest.getInstance(); static async login(params: LoginParams): PromiseUserInfo { return this.http.postUserInfo(/user/login, params); } static async getUserInfo(): PromiseUserInfo { return this.http.getUserInfo(/user/info); } }这样设计的好处是页面里调用的时候代码非常清爽const userInfo: UserInfo await UserApi.login({ username: xxx, password: yyy });你不必关心token、错误码、超时这些都在HttpRequest内部处理好了。4. 实操演示在项目里怎么用起来4.1 发起GET和POST请求先看最常用的GET请求。比如首页要展示用户信息import { UserApi, UserInfo } from ../services/UserApi; async function loadUserInfo() { try { const info: UserInfo await UserApi.getUserInfo(); console.info(用户昵称: ${info.nickname}); } catch (e) { const err e as HttpError; if (err.code ErrorCode.TIMEOUT) { // 单独处理超时 } else if (err.code ErrorCode.NETWORK_ERROR) { // 网络异常展示错误提示 } } }POST请求和GET类似只是第二个参数传对象。比如提交表单const result await http.post{ id: number }(/order/create, { goodsId: 1001, count: 2 });这里http是HttpRequest.getInstance()的实例。4.2 文件上传和进度监听HarmonyOS的ohos/axios对FormData的支持已经比较完善上传文件时可以这么写import { FormData } from ohos/axios; const formData new FormData(); formData.append(file, { uri: file://docs/avatar.jpg, name: avatar.jpg, type: image/jpeg }); const http HttpRequest.getInstance(); const result await http.post{ url: string }(/upload/avatar, formData, { headers: { Content-Type: multipart/form-data }, onUploadProgress: (loaded: number, total: number) { const percent Math.round((loaded / total) * 100); console.info(上传进度: ${percent}%); } });这里的uri一般是文件选择器返回的file://路径需要配合ohos.filepicker或者ohos.file.fs获取。上传进度的回调可以在页面里更新进度条体验比傻等好很多。4.3 Token过期自动刷新实际项目中登录过期是个绕不开的问题。我的思路是在HttpRequest里增加一个“刷新token”的状态标记。当响应拦截器捕获到HTTP 401时不是立刻抛错而是先尝试调用刷新token接口拿到新token后重新请求原来的接口。实现思路大概长这样private isRefreshing: boolean false; private pendingQueue: Array(token: string) void []; private async handleRefreshToken(): Promisestring { if (this.isRefreshing) { return new Promisestring((resolve) { this.pendingQueue.push(resolve); }); } this.isRefreshing true; try { const token await this.poststring(/auth/refresh, {}, { skipAuth: true }); AppStorage.setOrCreatestring(token, token); this.isRefreshing false; this.pendingQueue.forEach((callback) callback(token)); this.pendingQueue []; return token; } catch (e) { this.isRefreshing false; this.pendingQueue []; throw e; } }这个写法核心是pendingQueue多个请求同时401的时候只需要发一次刷新token请求其他请求排队等着新token刷新完成再重放。代码里的post方法多传了一个{ skipAuth: true }表示刷新token的请求本身不需要带旧的Authorization头否则会死循环。4.4 并发请求与取消页面跳走之后之前发出去的请求还在跑容易引发“在page里setState但page已经销毁”的崩溃。我们封装的cancelKey就是来干这个的const http HttpRequest.getInstance(); try { await http.getUserInfo(/user/info, {}, { cancelKey: userInfo }); } catch (e) { const err e as HttpError; if (err.code ErrorCode.CANCELED) { console.info(请求已取消); } } // 页面onPageHide的时候取消 http.cancelRequest(userInfo);多个请求并发的时候可以给每个请求不同的cancelKey再按key统一取消。5. 常见问题排查与经验速查5.1 超时设置不生效如果你发现明明设置了timeout: 15000但请求还是迟迟不报错先排查是不是在创建axios实例之后又覆盖了timeout。我在封装里允许每个请求单独传timeout但如果忘记给ApiRequestConfig里的timeout转进AxiosRequestConfig就会出现“看起来设了超时实际没生效”的怪问题。另一个常见原因是鸿蒙系统的底层HttpClient在弱网环境下连接阶段的耗时可能不计入timeout这种情况建议在业务层额外加一个“兜底取消”的逻辑。5.2 HTTP明文请求被拦截这个问题在前面权限配置里提过。如果你用http://192.168.x.x:8080这种局域网地址调试控制台可能会报“caused by: java.io.IOException: Cleartext HTTP traffic to xxx not permitted”不用怀疑就是网络安全配置的问题。开发阶段可以在network_config.json里设cleartextTrafficPermitted: true也可以用https://的测试域名。注意这个配置只对SDK API 9以上的版本有效老版本默认可能直接允许明文存在安全隐患。5.3 大文件上传和代码包大小限制有个比较隐蔽的坑用同一个HttpRequest实例上传超大文件时如果不在拦截器里对FormData做特殊处理默认的Content-Type: application/json可能会把文件流破坏掉。所以我在上传时手动覆盖了Content-Type: multipart/form-data这是经验之谈。另外HarmonyOS应用上传到应用市场时代码包大小是有限制的。如果项目里引用了ohos/axios后包体积超标优先检查有没有把示例代码里的完整axios实例打包进正式包。正常情况下ohos/axios只包含运行时必要的模块体积不大但如果你在代码里同时引用了多个网络库比如axios和原生http混用体积就会上去。可以开启代码混淆和资源压缩再把基础库拆分成HSP或HAR分发能有效减小主包体积。更直接的做法是把网络请求工具类放进一个HAR里业务模块依赖HAR而不是直接拷贝源码。这样既能复用又方便后续更新不至于每个模块都塞一份axios。5.4 ArkTS类型检查报错处理ArkTS对类型的严格程度比TypeScript还要高最常见的问题有三个。第一不能用any。我在封装时把回调参数写成unknown然后在代码里用instanceof收窄或者显式断言类型。如果你在DevEco Studio里看到“Type any is not supported”的报错基本就是哪里偷偷混入了any。第二对象字面量不能直接赋给一个没有索引签名的接口。比如config.headers {}这种写法在Web端没问题在ArkTS里就会报错。我的处理方法是显式声明一个Recordstring, string再整体赋值。第三axios拦截器回调的参数类型在不同版本里名字可能不一样。有些版本导出了InternalAxiosRequestConfig有些版本没有。如果编译报找不到这个类型直接改成AxiosRequestConfig就行功能不受影响。5.5 真机调试上传失败很多人会遇到error: 上传失败:网络请求错误注意这个报错不一定是你代码的问题。真机调试时IDE要把HAP包通过调试通道传到手机上如果手机和电脑不在同一个局域网或者调试端口被占用就会在安装阶段报这个错。常规排查步骤是确认手机和电脑连接了同一个网络进入开发者模式重新插拔USB线或无线调试连接重启DevEco Studio。如果还不行就在Build菜单里Clean Project一下再重新运行。这个报错和网络请求工具类的实现没有关系别在一开始就怀疑自己的axios配置。5.6 日志排查技巧排查请求问题最简单的方式是打印日志。我在拦截器里会打印请求URL、请求参数、响应数据和错误信息。HarmonyOS的console.info在DevEco Studio的Log窗口里可以看到记得不要用console.log鸿蒙不认。日志级别我建议请求参数用console.info错误信息用console.error这样过滤的时候方便。我个人在实际项目中的体会是网络层真的是App开发的“基础设施”前期规划越仔细后期越轻松。如果你也在做鸿蒙应用别急着在页面里到处写axios先花半天把这样一个工具类搭好后面所有接口开发都会顺畅很多。后续我打算再给这个工具类加上请求缓存和自动重试机制到时候再写一篇实操分享出来。
返回列表