ArkTS环境下的Axios封装:类型安全与拦截器实战
发布时间:2026/10/2 10:04:33来源:尧图网络
HarmonyOS 应用开发跑到一定阶段网络请求这块就成了绕不过去的坎。ArkTS 的语法约束比普通 TypeScript 严格得多类型安全基本是硬性要求Axios 作为最成熟的请求库自然也成了首选。但直接裸用 Axios 写业务代码很快就会发现重复代码、错误处理、类型断言越来越多等堆到一定量级再回头整理成本高得让人想重写。我在几个正式项目里反复调整过这套请求工具把类型安全、拦截器、上传下载、错误处理都揉到一起之后业务侧写接口的体验才算真正顺了。这篇文章不聊虚的直接说封装思路、完整代码、以及实测中踩过的 Content-Type 和版本升级的坑适合已经在 HarmonyOS 项目里打转、想给团队成员一套统一请求规范的开发者参考。1. 为什么非要在 ArkTS 里封装请求工具1.1 裸用 Axios 写业务代码痛在哪儿先从最真实的场景说起。假设你直接在 Page 里写axios.get每写一个接口都要重复指定 baseURL、token、超时时间、loading 开关、错误提示。短项目还能忍一旦页面超过十个你会发现同样的代码复制粘贴了十几次后端的 code 变了你要去十几个页面里改判断逻辑。这还只是表面问题。在 ArkTS 环境里这个问题会被放大。ArkTS 对类型的检查比 TS 更挑剔它不允许你随便用 any 去绕开类型问题。裸用 Axios 时response.data 的类型基本就是 unknown你想拿到业务字段就得一层层自己断言。写少了编译不过写多了全是 as。时间一长接口列表和实际返回的数据结构一旦对不上排查起来真的头疼。我在第一次把 Web 端的请求代码迁移到 HarmonyOS 项目时就吃过这个亏后端接口返回值明明是{ code, message, data }我在页面里直接用res.data.list结果跑起来才报 undefined编译期一点提示都没有。还有个被很多人忽视的点取消请求。HarmonyOS 页面有明确的生命周期页面销毁之后网络请求还在飞回调里再去操作 UI 就会出现各种诡异问题。如果不做统一封装每个页面都要记得自己去管取消逻辑漏一次就是一次线上问题。我做开发这几年因为忘记取消请求导致的内存泄漏和空指针报错遇到的不止一次两次这都是裸用请求库的真实代价。1.2 类型安全不是做减法是把错误提前到编译期类型安全听起来很玄拿装修来类比就好懂。你装修前先画好水电图哪里能走管、哪里不能走设计阶段就定死了后面施工照着走就行。如果不画图师傅都是边装边想装完才发现水管和电线打架返工成本极高。类型安全的请求工具就是那张水电图。具体到代码层面收益有三块。第一接口返回的数据结构有自动提示。你定义好 UserInfo 之后res.list[0].name敲下去的时候编译器能告诉你这个字段存不存在。第二参数约束不容易传错。getT的 params 是Recordstring, string | number你传一个对象进去字段名拼错了编译期就会报错。第三业务错误码可以被收窄成联合类型比如你定义type BizCode 0 | 401 | 403 | 500判断 code 的时候 IDE 会智能提示。这些收益在普通 TS 里是加分项在 ArkTS 里基本是刚需。因为 ArkTS 不允许你在业务代码里偷偷加 any 逃课你必须有意识地把所有接口的数据结构都定义出来。既然定义都定义了顺手做一套统一的泛型封装反而是最省力的做法。很多同学一开始觉得封装麻烦总想先上车后补票等接口写了几十个再回到函数签名里去补泛型那个痛苦程度足以让人怀疑人生。2. 动手前先想清楚接口规范和请求层架构2.1 统一返回结构先和后端把 code 口径聊明白封装之前第一件事不是写代码是先把接口返回结构定下来。市面上绝大部分后端接口都会包一层统一结构典型长这样{ code: 0, message: success, data: { ... } }code 为 0 表示成功非 0 表示业务异常message 给用户看data 才是真正要用的数据。我建议新项目直接按这个结构和后端对齐。如果你们后端已经有一套自己的结构那也行但一定要在公司内部统一不能在 A 接口返回{ code, data }B 接口返回{ errCode, result }C 接口直接给你裸数据。没有统一结构再好的类型封装也救不了你。定了统一结构以后前端这边的泛型设计就很清晰了。data 部分留给业务方自己去指定其他部分由封装工具统一处理。分页接口再包装一层data 里面的内容是 list、total、page 这些。把这两层定义好整个封装的地基就稳了。这里还要特别注意和团队对齐一个约定业务失败不要用 HTTP 状态码表达而是用业务 code。比如用户未登录返回 code 401HTTP 状态码仍然可以是 200。这样做的好处是传输层错误和业务错误彻底分开拦截器里可以统一判断业务 code 做 toastHTTP 状态码只处理断网、超时、服务器崩溃这类传输级错误。我在实际项目里见过很多前后端联调吵架的场面根因都是这两层没有分清楚。2.2 四层划分实例、拦截器、API 定义、调用方各管一段我在项目里习惯把整个请求体系拆成四层每一层只干一件事Axios 实例层负责创建 instance配置 baseURL、timeout、公共 headers。拦截器层负责请求发出前的 token、loading响应回包后的状态码判断、错误统一提示。API 定义层每个业务模块一个文件只存放接口函数和数据类型的定义。调用方页面或 ViewModel 里直接 await 接口函数拿到的就是解包后的业务数据。这样分层的理由很简单。后端接口调整时大部分情况下只需要动 API 定义层token 过期逻辑调整时只需要动拦截器谁也不会跑到页面里去翻网络代码。我在一个中大型项目里实测过页面里基本看不到 axios 相关代码所有的网络处理都收口在框架层排查问题的时候一行定位。如果你让页面里到处散落着 axios 调用那这个封装基本等于没做。3. 核心实现从零封装一个类型安全的 Axios 工具3.1 公共类型定义与泛型约束设计先建一个types.ts把公共类型都放在这里。第一步定义后端返回的最外层结构// types.ts export interface ApiResponseT unknown { code: number; message: string; data: T; } export interface PageResultT { list: T[]; total: number; page: number; pageSize: number; }ApiResponse的泛型默认值给了unknown这样那些还没有严格定义 data 类型的接口也能先跑起来后面再逐步收窄。PageResult单独定义分页接口直接复用。然后是请求配置。我自定义了一个RequestConfig它把 Axios 的配置和业务控制项合并在一起export interface RequestConfigT unknown { url: string; method?: GET | POST | PUT | DELETE; params?: Recordstring, string | number; data?: unknown; headers?: Recordstring, string; timeout?: number; // 是否显示全局 loading默认 false showLoading?: boolean; // 是否只返回 data 字段默认 true unwrap?: boolean; }我在早期版本里直接extends了 Axios 的AxiosRequestConfig后来发现一个麻烦自定义的 showLoading、unwrap 会随着 config 透传到 axios 实例里axios 不认识这些字段虽然不报错但语义上很脏还容易踩到类型声明冲突。所以我后来干脆自己声明了一套精简配置在封装函数内部再手动映射成 axios 认识的配置。这个取舍在 ArkTS 里尤为重要因为它的类型声明检查格外严格跨库透传自定义字段很容易在编译期炸出一堆问题。3.2 创建 Axios 实例与拦截器接着是request.ts核心是创建实例和拦截器。// request.ts import { axios, AxiosInstance, AxiosResponse } from ohos/axios; import { ApiResponse } from ./types; const instance: AxiosInstance axios.create({ baseURL: https://api.yourapp.com, timeout: 15000, headers: { Content-Type: application/json, }, }); instance.interceptors.request.use((config) { const token getTokenFromStorage(); if (token) { config.headers { ...config.headers, Authorization: Bearer ${token}, }; } return config; }); instance.interceptors.response.use( (response: AxiosResponse) { const res response.data as ApiResponseunknown; if (res.code ! 0) { showToast(res.message); return Promise.reject(new Error(res.message)); } return response; }, (error: Error) { // 网络错误、超时、HTTP 错误统一处理 return Promise.reject(error); } );三个细节需要强调。第一在 HarmonyOS 的ohos/axios里拦截器的类型和 Web 端 axios 不完全一样错误对象的类型也不一样。我之前把 Web 端的代码直接搬过来发现 catch 里拿到的 error 并不是AxiosError而是一个带自定义字段的错误对象几个字段的路径对不上。建议你在写统一错误处理前先console.log打印一次真实错误对象照着真实结构去处理。第二请求拦截器里修改 headers 的时候一定要先展开再赋值。如果你直接config.headers[Authorization] xxx在部分版本里会因为对象字面量类型推断导致编译不过或者拦截器返回后 headers 没有生效。展开之后再赋值实测最稳。第三响应拦截器里我先判断了业务 code。这里有个取舍把业务错误放在拦截器统一弹 toast页面里只需要处理 reject 的情况如果你希望某些页面自己处理错误提示可以在RequestConfig里加一个silent字段拦截器发现silenttrue就只 reject 不弹 toast。后续有需求再加一开始别做太复杂。3.3 统一 Request 方法让返回值直接变成业务数据类型接下来是核心的 request 方法。它做的事情很纯粹接收业务配置剔除自定义字段调用 axios 发请求返回值类型由泛型推导。// request.ts import { RequestConfig } from ./types; export function requestT(config: RequestConfigT): PromiseT { const { showLoading false, unwrap true, ...axiosConfig } config; if (showLoading) { showGlobalLoading(); } return instance.request(axiosConfig).then((response) { // 返回整体结构还是只拿 data由调用方按需决定 if (unwrap) { return (response.data as ApiResponseT).data; } return response.data as ApiResponseT; }); }然后基于 request 封装 get、post 这些常用方法export function getT( url: string, params?: Recordstring, string | number, config?: PartialRequestConfigT ): PromiseT { return requestT({ url, params, method: GET, ...config }); } export function postT( url: string, data?: unknown, config?: PartialRequestConfigT ): PromiseT { return requestT({ url, data, method: POST, ...config }); }这里的泛型 T 就是这个接口最终返回的业务数据类型。调用方传不传都行不传就推断成unknown传了就有全链路提示。注意...axiosConfig的解构方式这么写就是为了把 showLoading 和 unwrap 这两个自定义字段从 axios 配置里剥掉避免透传到实例上引起类型告警。3.4 业务 API 定义与调用示例到了 API 定义层体验就完全不一样了。举个例子用户模块// api/user.ts import { get, post } from ../request; import { PageResult } from ../types; export interface UserInfo { id: string; name: string; avatar: string; } export function fetchUserList(page: number, pageSize: number) { return getPageResultUserInfo(/api/user/list, { page, pageSize }); } export function updateUserInfo(data: PartialUserInfo) { return postUserInfo(/api/user/info, data); }调用的时候const listData await fetchUserList(1, 20); console.log(listData.list[0].name); // 有自动补全写错字段编译期就报错这套写法和原来差了多远呢早前用 Axios 原生写法的时候拿一个 res 要先做四五层断言才能访问到listData.list[0].name中间任意一层字段拼错运行到那行才炸。现在用这个封装接口定义写一次之后所有页面都吃这套类型省下来的时间非常可观。而且不需要额外引入代码生成器或者复杂的工具链就是一个纯手写的小工具几十分钟就能落进项目里。4. 上传下载与 Content-Type最容易翻车的地方4.1 multipart/form-data 的正确打开方式上传文件时最容易出问题的是 Content-Type。很多同学手动设置headers[Content-Type] multipart/form-data结果后端报错解析不到文件。原因很典型multipart/form-data 需要带 boundary 分隔符这个 boundary 是随请求体随机生成的。你手动写死 Content-Type 之后如果 header 里的 boundary 和实际请求体的 boundary 对不上后端就拿不到完整的分隔信息自然解析失败。我在项目里的做法是先构建 FormData然后尽量别手动指定 multipart 的 Content-Type让 axios 根据 data 类型自动去设置。如果不放心就抓一次包确认实际发送的 Content-Type 长什么样再决定要不要固定。示例export function uploadAvatar(filePath: string) { const formData new FormData(); formData.append(file, filePath as unknown as string); formData.append(scene, avatar); return requestUploadResult({ url: /api/upload, method: POST, data: formData, timeout: 60000, }); }注意里头的as unknown as string。ArkTS 的 FormData.append 对类型要求比较严格文件路径和真实 File 对象在不同版本里表现不一样写成 unknown 过渡一下再 as 过去可以避免编译期一堆类型报错。上传接口超时时间一定要单独放大默认 15 秒在弱网环境下大概率不够用。如果你确实需要显式指定表单模式比如后端要求application/x-www-form-urlencoded那就用URLSearchParams或者手动拼 Query String并设置好对应的 Content-Type。这里没有对象自动序列化字段名拼错编译期也发现不了所以我在项目里通常更推荐 JSON 模式逼后端也多走 JSON 接口。4.2 Axios 升级前后请求报文变化排查步骤实录还有一个很邪门的坑是升级 Axios 版本之后遇到的。某次我把项目的 axios 依赖升了一个版本回归测试发现之前好好的接口后端突然就收不到参数了。抓包看到升级前客户端发送的报文里 Content-Type 是application/json请求体是 JSON 字符串升级之后发送的报文里 Content-Type 变成了application/x-www-form-urlencoded请求体变成了a1b2这种 Query String 格式。后端是按 JSON 解析的全部解析失败。这个问题的根因是 axios 的 transformRequest 默认行为当 data 是普通对象时axios 会根据 Content-Type 选择合适的序列化方式。新版对 Content-Type 的推断逻辑更激进如果你的拦截器里或者业务代码里没有显式设置application/json它就可能走表单序列化。排查顺序我整理成了一套固定动作抓包对比实际发送的报文看 Content-Type 和 body 结构到底长什么样。检查所有拦截器里有没有对 headers 做过修改一旦重新赋值 headers可能导致默认 Content-Type 丢失。检查有没有自定义 transformRequest自定义之后 axios 不会再走默认 JSON 序列化逻辑你得自己处理一切。看传入 data 的实际类型如果传的是字符串而不是对象axios 不会自动序列化也需要手动指定 Content-Type。我把这个教训总结成了一句口诀只要上报文异常先看 Content-Type再看序列化先改 header再查拦截器。实际上 Content-Type 相关的坑还有另一种post 请求想用表单模式但后端收到的却是 JSON原因正好相反axios 默认把对象序列化成 JSON 了。解决方式就是上面说的先用 URLSearchParams 或手动拼 Query String再设置表单 Content-Type。4.3 超时、取消与错误码兜底策略统一封装里我还会加三个不太起眼但关键时刻救命的能力。第一个是超时。我的习惯是普通接口 10 秒上传下载 60 秒具体接口可以在 RequestConfig 里单独覆盖。timeout 值不要全局一刀切否则用户拿弱网测一次体验直接崩掉。第二个是取消请求。HarmonyOS 页面有 onPageHide、onPageUnload 这类生命周期页面销毁时要把还没结束的请求 cancel 掉。Axios 支持 CancelToken 和 AbortController在 ArkTS 里实测下来 AbortController 写法更直观封装成 helper 之后页面里只需要在初始化时创建、销毁时调用取消即可。不做这一步页面来回跳转几次回调堆积起来轻则报错重则内存泄漏。第三个是错误码兜底。响应拦截器里除了业务 code 判断还要对 HTTP 状态码做一层兜底401 跳登录并清 token403 提示无权限500 提示服务器繁忙。这层放在拦截器而不是页面里是为了确保任何业务接口都覆盖得到不至于有些接口报错之后毫无提示用户以为自己断网了。5. 实战中踩过的坑与排查清单5.1 高频问题速查表把我在多个项目里实际遇到的高频问题整理成一个速查表遇到直接对号入座现象原因处理方式返回值类型总是 unknown泛型没传或者接口层没定义类型在 API 定义层明确写getT/postT上传文件后后端收不到文件Content-Type 被手动写死boundary 丢失让 axios 根据 FormData 自动设置 header升级 axios 后参数解析失败transformRequest 默认序列化逻辑变化抓包对比显式设置 application/json拦截器里修改的 headers 没生效直接给 headers 属性赋值被类型推断卡住先展开 config.headers 再赋值页面销毁后请求回调仍执行未做取消逻辑用 AbortController在 onPageUnload 里 cancel自定义 showLoading 字段被传给 axiosRequestConfig 没有剥离业务字段在 request 函数里解构剔除再传泛型嵌套过多编译报 TS2589类型递归过深减少多层泛型必要时用 unknown 截断5.2 几个我后来才想明白的设计细节第一点是 unwrap 这个开关。我一开始把解包写死所有接口返回 data 字段。后来发现有些接口确实需要拿到整个 ApiResponse比如登录接口要同时更新 token 和用户信息只返回 data 很别扭。后来加了 unwrap 开关默认 true需要完整结构的接口手动关掉就行。这个开关看起来多余实际上避免了后期大规模返工。第二点是拦截器里该管什么、不该管什么。早期我把 loading 的显隐也放在拦截器里页面调用确实很爽但问题是我没法知道某个接口是不是需要 loading只能一刀切。后来改成 RequestConfig 里的 showLoading 标识默认 false需要 loading 的接口显式传 true全局 loading 和局部 loading 都能控制。这比拦截器一刀切灵活得多。第三点是类型定义要和后端字段严格对齐但不要盲目信任后端的文档。后端字段改了前端编译期不会报错运行期才会炸。我的做法是在开发环境加了一层返回结构校验用简单的运行时断言发现 code 非 0 或者关键字段缺失时在日志里打醒目告警尽早暴露问题。这种校验不用全接口覆盖挑核心接口做就行成本低收益高。我个人在实际项目中把这一套封装跑了两三个版本最大的体会是类型安全的请求工具真正的价值不是让你少写几行代码而是让团队里每个人都遵循同一套接口规范和错误处理方式新人上手也只需要看一个 api 目录就能了解全部网络请求。如果你是在现有项目里改造别一次性把所有页面都迁过来先选一两个模块试点跑通之后再逐步铺开。最后一个小技巧ApiResponse 里建议把成功的 code 值约定为 0 而不是 200因为业务接口通常用 0 表示成功HTTP 200 留给网络层这样业务错误码和传输层状态码能彻底分开排查问题时会省很多力气。
网站建设高端定制企业官网