鸿蒙应用Axios封装实战:从拦截器到请求取消全攻略

发布时间:2026/10/20 8:54:39
鸿蒙应用Axios封装实战:从拦截器到请求取消全攻略 最近在做一个模拟项目的网络层重构把之前散落在十几个页面里的axios.get全都收编进了一层统一的请求封装。先说结论鸿蒙环境里的 Axios 封装底层思路和 Web 端没本质区别无非是实例化、拦截器、错误处理那一套但真正落地的过程中权限声明、页面生命周期、设备信息、文件路径这些鸿蒙特有的东西每一样都会影响封装质量。这篇文章不是官方文档翻译而是一份基于实际项目踩坑经验的封装攻略适合刚开始在鸿蒙应用里做网络层、或者功能已经跑通但被重复代码和无法收场的错误处理折磨的人。1. 为什么“裸用 Axios”很快会在正式项目里翻车1.1 从一段反面教材说起初期的项目里最常见的写法是每个页面各自维护自己的请求逻辑// 最开始的写法能跑但后患无穷 axios.get(/user/info, { baseURL: https://api.example.com, timeout: 10000, headers: { Authorization: Bearer ${getToken()} } }).then(res { if (res.data.code ! 0) { showToast(res.data.message) return } setUserInfo(res.data.data) }).catch(err { showToast(err.message) })单看这一段问题并不明显。但当一个正式项目里出现几十个类似代码块时麻烦就来了。baseURL、timeout在每个请求里重复出现测试环境、生产环境切换时要全局搜索替换鉴权头Authorization从哪个函数拿、拿不到怎么办没有任何统一策略后端返回的code是否为 0 需要业务层自己判断每个页面写一遍if / else错误处理也很随意有的页面showToast有的页面只在控制台打印有的干脆把错误吞掉。一旦后端调整接口字段比如把message改成msg牵扯面大得令人绝望。所以“裸用 Axios”不是不能跑而是没办法在此基础上长期迭代。封装的目的不是炫技而是把网络请求的共性收口到一处让业务页面只关心“我要什么数据”不关心“这个请求带什么 header、失败怎么提示、要不要取消”。1.2 鸿蒙场景比普通 Web 多出来的三个变量在鸿蒙应用里做网络层不能只把 Web 端的经验搬过来还要额外处理三件事。权限声明应用访问网络必须在模块配置文件里声明网络权限。我见过不少新手的第一个 bug 就是忘记加权限真机上一请求就直接失败且报错信息不直观往往要打开系统日志才能看到网络权限被拒绝的记录。这个问题如果出现在封装前期很容易被误判成“Axios 用不了”。页面生命周期鸿蒙的页面有完整的生命周期请求发出后页面可能已经退出。此时异步回调如果继续操作 UI 组件轻则弹一个没人看的 toast重则触发空引用问题。因此网络层必须提供“页面销毁即取消请求”的通道而不是把取消逻辑全部交给业务侧。多设备适配鸿蒙应用会跑在手机、平板、车机等不同形态设备上接口域名可能随环境、设备类型变化。如果没有集中的配置层设备适配会变成一坨条件判断散落在各个请求中。封装层刚好可以在一处做配置分发。1.3 封装到底要解决哪些问题在动手写第一行封装代码前我习惯先列一张表把目标明确下来。以下是我那个模拟项目的对照表问题裸写 Axios 的表现封装后的效果域名与配置每个页面各写各的按环境统一切换配置集中鉴权注入token 取法五花八门请求拦截器统一注入返回结构业务层手动判断 code响应拦截器解包只返回业务数据错误处理页面各自弹窗全局错误处理 可静默标记请求状态页面手动维护 loading封装层按 key 自动合并请求取消基本没有支持页面级/全局级取消重复提交靠业务逻辑硬顶封装层提供去重能力这张表不仅是目标也是后面所有代码的依据。封装层每加一个能力都要能回答一个问题它是否解决了这张表里的一行如果解决不了优先级就得往后放。2. 动手封装前先把环境与边界确认清楚2.1 依赖安装、网络权限与明文策略在鸿蒙工程里使用 Axios通常通过包管理工具引入适配库。我在工程的依赖配置文件里声明dependencies: { ohos/axios: ^2.2.0 }依赖装好只是第一步真正容易漏的是模块配置文件里的网络权限requestPermissions: [ { name: ohos.permission.INTERNET } ]没有这个权限编译能通过运行时请求直接失败。另一个常见问题是在开发环境访问http://明文接口鸿蒙系统默认会限制明文流量。如果后端还没有配置 HTTPS需要在开发阶段按项目实际情况放开明文网络策略。这个开关属于开发便利不是上线配置项目进入提测阶段前一定要关掉。2.2 鸿蒙环境里 Axios 的能力边界很多人以为鸿蒙版 Axios 和 Web 版一模一样实际并不是。它底层走的是鸿蒙系统网络栈不是浏览器 fetch也不是 Node 的 http。基于适配库实际提供的能力我一般会先确认以下几点常见的get/post/put/delete/upload/download都有对应封装可以直接使用。请求拦截器、响应拦截器、取消请求、超时这些核心能力可用。文件上传下载走的是鸿蒙原生能力写法和 Web 端不完全一致。没有浏览器里的 Cookie 自动管理大部分场景需要靠请求头里的 Token 完成鉴权。没有Blob或FormData的完全等价对象传文件时要使用库支持的特定数据结构。这些边界确认非常重要。我在一个阶段走过弯路试图直接在请求体里塞一个 Web 端常用的FormData对象结果真机上请求直接异常。后来确认适配层的文件参数有专门约定才绕出来。封装层的任务之一就是把这些差异吸收掉让业务侧感觉不到底层切换的复杂。2.3 先和后端把响应协议钉死写封装前我和后端同学讨论的第一件事不是接口清单而是统一的响应结构。我推荐这样一个基准格式{ code: 0, message: ok, data: {} }其中code是业务状态码0表示业务成功HTTP 状态码只表示网络层是否通。两者必须分开处理否则会出现“HTTP 200 但是业务失败”和“HTTP 500 但业务层还想尝试”的混乱。如果项目里有一些老接口不遵守这个格式可以在封装层做“响应归一化”把不同结构转成统一格式再往下走function normalizeResponse(raw: any) { if (raw typeof raw object code in raw) { return { success: raw.code 0, code: raw.code, message: raw.message, data: raw.data } } return { success: true, code: 0, message: ok, data: raw } }如果代码里出现res.data.data.data这种层层访问说明响应拦截器的“解包”职责没做好。业务层能拿到的应该是已经解包完的业务对象而不是原始响应外套。2.4 目录结构别把封装代码堆成一坨封装层很忌讳写成“一个文件解决所有问题”。我的推荐结构是这样的src/api/ http/ index.ts // 创建实例、暴露 request 方法 config.ts // 环境域名、超时时间等配置 interceptors.ts // 请求拦截器与响应拦截器逻辑 error.ts // 错误类型与错误文案映射 loading.ts // 全局 loading 计数器 cancel.ts // 请求取消管理 modules/ user.ts order.ts ...modules下只放业务接口定义不放任何axios.get。业务层调用的是封装好的requestUserInfo(...)返回类型就是UserInfo。这个结构不复杂但能让多人协作时可维护性高很多。3. 封装骨架一个实例、两道拦截器、一套类型3.1 实例化与配置分离封装的第一步是创建唯一实例而不要每个模块各自axios.create。原因很简单项目通常只有一个主域名测试环境和生产环境的差异通过配置切换即可没必要在模块层重复创建。import axios from ohos/axios import { AppConfig } from ./config export const http axios.create({ baseURL: AppConfig.baseURL, timeout: 15000, headers: { Content-Type: application/json } })配置单独放config.ts后续只要构建环境或部署环境一变改这一处就行export const AppConfig { baseURL: https://api.example.com, timeout: 15000, uploadTimeout: 60000 }如果项目确实存在多个域名比如网关域名、文件服务域名可以再创建一个专用实例但不要每个功能模块都 new 一个实例。实例多了拦截器就分散了封装也就失去了意义。3.2 请求拦截器token、设备标识、请求ID请求拦截器负责“出发前把共同参数补齐”。我最常做的是三件事http.interceptors.request.use((config) { const token getToken() if (token) { config.headers.Authorization Bearer ${token} } config.headers[X-Device-Id] getDeviceId() config.headers[X-Version] getAppVersion() config.headers[X-Request-Id] generateRequestId() return config })这里有一个很重要的原则请求拦截器不要负责页面跳转。token 失效时拦截器能判断出“需要登录”但页面跳转必须交给统一错误处理中心否则拦截器既要管请求又要管路由职责会越来越混乱。3.3 响应拦截器解包、业务错误、错误标准化响应拦截器是封装层的核心也是差异化最大的落点。我的处理思路是成功回调里解包业务数据业务错误和网络错误统一转成标准错误对象。http.interceptors.response.use( (response) { const raw response.data const normalized normalizeResponse(raw) if (!normalized.success) { const err new BusinessError(normalized.code, normalized.message) triggerGlobalErrorHandlers(err, response.config) return Promise.reject(err) } return normalized.data as any }, (error) { const err normalizeHttpError(error) triggerGlobalErrorHandlers(err, error.config) return Promise.reject(err) } )关键点在于triggerGlobalErrorHandlers内部要不要弹 toast取决于请求配置里的silent标记。比如刷新 token 的静默接口、预加载接口就不应该弹出错误提示打断用户。把判断逻辑放在封装层业务侧不用每次手动处理。3.4 用 TypeScript 把业务层类型钉死封装除了减少重复代码更重要的好处是类型安全。我先定义一套基础类型export interface ApiResultT unknown { code: number message: string data: T } export interface RequestOptions { silent?: boolean retry?: number cancelKey?: string loadingKey?: string }然后对外暴露一个带泛型的request方法export function requestT(config: AxiosRequestConfig RequestOptions): PromiseT { return http.request(config) as PromiseT }业务模块就可以写得非常干净export function fetchUserInfo(userId: string) { return requestUserInfo({ url: /user/info, method: GET, params: { userId }, silent: true }) }调用方拿到的info本身就是UserInfo不会再出现res.data.data。页面的自动补全、编译检查、重构提示都会好用很多。4. 业务级增强loading、取消、重试、并发控制4.1 loading 用“计数器”合并而不是每个请求各自开页面里多个请求同时发起的场景太常见。如果每个请求都showLoading/hideLoading很容易出现“第一个请求结束就把 loading 关了第二个还在跑”的闪烁。我的方案是给 loading 增加 key 和计数const loadingMap new Mapstring, number() function showLoading(key: string) { if (!key) return const count loadingMap.get(key) ?? 0 loadingMap.set(key, count 1) if (count 0) { globalLoading.show() } } function hideLoading(key: string) { if (!key) return const count loadingMap.get(key) ?? 0 const next Math.max(0, count - 1) loadingMap.set(key, next) if (next 0) { globalLoading.hide() } }请求拦截器里如果检测到配置了loadingKey就调用showLoading响应完整回调后调用hideLoading。同一个页面的多个请求共用一个 keyloading 只展示一次关闭时机是最后一个请求结束而不是第一个请求结束。这样既能避免闪屏也能避免加载态提前消失。4.2 取消请求防重复提交和处理“迟到响应”页面销毁后接口返回是鸿蒙应用里很容易遇到又很难复现的 bug。处理方式是在封装层支持统一的取消能力const pendingMap new Mapstring, AbortController() function addPending(config: AxiosRequestConfig RequestOptions) { if (!config.cancelKey) return const controller new AbortController() config.signal controller.signal pendingMap.set(config.cancelKey, controller) } function cancelByKey(key: string, message canceled) { const controller pendingMap.get(key) if (controller) { controller.abort(message) pendingMap.delete(key) } }在页面销毁的生命周期里调用cancelByKey(pageId /all)可以统一取消该页面的所有请求。在按钮防重复提交场景提交前cancelByKey(pay/submit)然后再发起新请求可以避免用户疯狂点击导致的下单重复。重点是不要全局共用一个 controller否则 A 页面的取消会把 B 页面同名接口一起干掉。4.3 失败重试别把 POST 接口拿来重试自动重试看起来很省事但设计不好会变成一个坑。我在封装里只对两种请求自动重试网络错误断网、超时、5xx并且请求方法是 GET。POST 和其他可能产生副作用的请求默认不重试避免重复下单、重复创建数据等事故。async function requestWithRetry(config: AxiosRequestConfig RequestOptions): Promiseany { const retry config.retry ?? 0 let lastError: any for (let i 0; i retry; i) { try { return await http.request(config) } catch (err) { lastError err const canRetry isNetworkOrServerError(err) config.method GET if (!canRetry || i retry) break await delay(300 * (i 1)) } } throw lastError }重试间隔建议用递增退避比如300ms - 600ms - 1200ms避免所有客户端在同一时刻疯狂重试。同时可以在请求头带上重试次数标记方便后端排查。4.4 并发控制给首屏集中请求加一道闸进入某个页面时同时发出六七个请求即使接口压力不大也可能导致页面出现明显的卡顿。在封装层做一个简单的并发闸门是个性价比很高的优化class RequestGate { private queue: Array() void [] private activeCount 0 constructor(private max: number 4) {} async runT(task: () PromiseT): PromiseT { if (this.activeCount this.max) { await new Promisevoid(resolve this.queue.push(resolve)) } this.activeCount try { return await task() } finally { this.activeCount-- const next this.queue.shift() if (next) next() } } } export const requestGate new RequestGate(4)注意这里run的排队逻辑只做“限量”不会改变请求本身的并发语义。真正需要严格控制并发数量时它非常有用但如果每个请求都走闸门可能造成不必要的排队延迟。所以只在首屏批量加载、刷新类场景使用。5. 上传下载进度、落盘与“别过度设计”的原则5.1 上传进度的封装鸿蒙适配库对上传进度的回调不同版本字段可能不一样。封装层要做的第一件事就是“统一成业务向的进度结构”export function uploadFile(fileUri: string, onProgress?: (percent: number) void) { return requestUploadResult({ url: /file/upload, method: POST, data: { file: fileUri }, timeout: 60000, onUploadProgress: (event) { const percent event.progress onProgress?.(Math.round(percent * 100)) } }) }上传接口必须单独给更长的超时时间不能用普通请求的 15 秒。另外不要把进度回调直接绑定到某个组件的局部变量里否则组件在进度过程中被销毁回调就会出错或者进度丢失。建议把进度状态提升到页面级状态或状态管理库中。5.2 下载落盘请求与写文件要解耦鸿蒙应用下载文件和浏览器里的“点击下载”不一样。封装下载时我习惯把“下载网络流”和“写入本地文件”分成两个阶段发起下载请求拿到文件流或字节内容将其写入应用的沙箱目录如filesDir或cacheDir如果需要出现在相册或公共目录再调用媒体库能力写入不能直接放在沙箱里就算了。分阶段的好处是重试逻辑只覆盖网络部分如果写文件失败报错信息能准确指向磁盘路径或权限问题而不是笼统地显示“下载失败”。5.3 断点续传先想清楚值不值得不少项目一提到大文件下载就要求“断点续传”。我的建议比较现实如果文件资源能走内容分发网络优先使用系统下载能力一定要自己实现断点续传的话需要后端支持Range请求头同时前端要记录已下载字节数还涉及临时文件管理、分片校验等一大堆问题。第一版不妨先做“失败后清掉重新下载”的方案等用户反馈和需求验证确实需要断点续传再上。过早做复杂度高的特性往往是在替并不存在的用户场景买单。6. 调试与验证封装完怎么证明自己没写错6.1 全局 Mock 开关封装层应该提供一个低成本的 mock 切入点方便接口未就绪时前端并行开发。我在config.ts里放了一个enableMock开关if (AppConfig.enableMock) { return mockHandler(config) }mock 逻辑统一放在独立目录不要散落在业务页面。这样等真实接口就绪只需要把开关关掉所有请求自动切回真实环境。这个开关对 UI 联调和自动化测试也有帮助。6.2 请求日志与链路追踪我在封装层里内置了一个可开启的请求日志每条请求生成一个requestId打点和后端排查都很方便function logRequest(config: AxiosRequestConfig) { const requestId generateRequestId() console.info([http] ${requestId} ${config.method?.toUpperCase()} ${config.url}, config.params ?? ) config.headers config.headers ?? {} config.headers[X-Request-Id] requestId } function logResponse(requestId: string, data: unknown, cost: number) { console.info([http] response ${requestId} cost ${cost}ms, data) }真机调试时可以在日志面板里过滤[http]关键字快速定位某个请求是否发出、耗时多少、返回什么。上线前记得把日志级别调低或关闭避免无谓的性能损耗和信息暴露。6.3 拦截器写成纯函数顺便接上单测拦截器里的大段逻辑一定要抽成独立纯函数而不是直接写在interceptors.use的回调里。好处是可以做单元测试比如it(should inject auth header, () { const config { headers: {} } as AxiosRequestConfig const result authInterceptor(config) expect(result.headers.Authorization).toBe(Bearer mock-token) })这类测试虽然简单但能保护封装层的稳定性。否则某天改了一下 token 取值方式可能导致全局所有请求带着空 token 上线的线上事故。7. 封装上线前后几个真机怪问题的排查记录7.1 请求发了但响应迟迟不回来有一个真机场景让我印象很深同一个接口在公司网络下很快换到一个公共网络后偶发 20 多秒才返回。一开始怀疑超时配置太小把 timeout 加到 30 秒问题概率下降但没根治。后来配合请求日志和后端日志一起看发现请求根本没到后端才意识到是本地网络解析链路的问题不是封装代码的问题。这件事给我的启发是封装层一定要把超时、网络错误、业务错误分成不同类别排查时才能快速缩小范围。7.2 泛型穿透失败业务层还在用 any我在响应拦截器里用过return normalized.data as any结果下游拿到的类型全是any自动补全全没了。后来改为让对外request方法透传泛型拦截器内部只做运行时过滤不在拦截器里用断言把类型“拍死”。现在编译期能直接发现字段名拼写错误开发效率提高不少。7.3 取消请求互相干扰有一次出现“A 页面退出后B 页面的同名单个接口也被取消”的现象。排查后发现是cancelKey没有拼页面标识而且请求完成后没有从pendingMap里清理。解决方案是cancelKey统一格式为pageId/apiName并保证请求成功、失败、取消三种结局都执行清理。封装层里的“清理”动作一定要放在finally语义里而不是只在一个回调里写。7.4 loading 计数器“转圈关不掉”页面销毁时如果统一取消了请求而取消的异常回调又调了一次hideLoading屏幕上的 loading 就会变成负数计数全局转圈永远关不掉。后来在hideLoading里加了Math.max(0, count - 1)并且页面销毁时只做取消不再重复触发hideLoading问题才彻底消失。这些坑单独看都很小但每一次都是真机实测后才暴露的。封装层作为所有业务的公共底层任何一个细节出问题影响面都是全局的。我最想说的是封装不是越厚越好一定要留一个逃生通道。业务确实有特殊请求时可以通过RequestOptions里透传底层配置而不是为特殊场景反复推翻封装。把大多数场景统一收口把少数例外做成显式开关这才是“全攻略”的尽头。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询