ARTICLE DETAIL

资讯详情

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

鸿蒙HMRouter封装进阶:请求模型、拦截链路与降级策略实战

鸿蒙HMRouter封装进阶:请求模型、拦截链路与降级策略实战 鸿蒙开发HMRouter封装教程三老读者应该都知道HMRouter这套封装我前两篇分别讲了基础初始化和注解注册评论区后台也一直有朋友催更说想看拦截器体系、请求上下文封装和降级兜底这几个重头戏怎么落地。正好项目里最近又跑了一遍线上压测几个封装的细节也打磨得差不多了我这就把第三篇整理出来。这篇核心就干三件事把路由跳转的请求模型做得更顺手、把拦截和生命周期从业务代码里剥离干净、把异常降级做得像样。适合已经用上HMRouter但还在用裸API写跳转的开发者也适合刚完成前两篇封装、正打算往工程化方向深挖的朋友。1. 封装设计的整体思路与前置约定1.1 为什么一定要做封装很多开发者第一次接触HMRouter时觉得它已经足够简洁跳转代码无非就是调用一下RouterService.getService().pushUrl(...)再配合注解标识目标页面似乎不需要再包一层。但真正做过中大型鸿蒙应用后你会发现裸用HMRouter会带来几个非常现实的问题。第一是参数传递的混乱。HMRouter底层使用HarmonyOS的Want作为载体业务页面通过getParam方法自行解析。如果项目里没有统一约定参数名很容易出现字符串硬编码不一致页面间传对象还需要手动序列化和反序列化长传内容一旦改动编译期根本发现不了问题。第二是拦截逻辑没法复用。登录校验、隐私合规弹窗、数据埋点这些需求几乎每个页面跳转都涉及如果分散在各页面onPageShow或跳转前判断代码会到处重复而且后续加一个全局开关或者调整拦截顺序成本非常高。第三是贬降级链路缺失。HMRouter本身提供了目标页面不存在时的兜底逻辑但业务上往往需要的是“页面不存在时跳转到统一错误页”或者“功能未开放时弹Toast”这些都需要围绕路由层做一层统一的收口。封装这件事本质上是把“路由能力”抽象成“业务能力”让调用方不再关心页面路径、参数容器、生命周期钩子和异常处理细节只需要表达“我要去哪里、带什么数据、期望得到什么结果”。这层设计的收益在项目页面超过20个之后会特别明显。1.2 这套封装的核心目标这篇文章里的封装方案我定下来的核心目标是四个路径收敛所有页面路径集中管理不允许业务代码里出现裸字符串。请求上下文化每一次路由跳转都对应一个请求对象参数、拦截、回调、降级都挂载在这个请求对象上。拦截可编排多个拦截器按优先级组成链路单个拦截器职责单一可插拔支持全局拦截和页面级拦截。失败可感知路由跳转的最终结果以回调方式返回给调用方成功或失败都有明确收口不再出现“点了没反应、也不知道去哪看日志”的情况。围绕这四个目标我后续所有的封装代码都以 ArkTS 语言编写工程基于 API 12 及以上版本HMRouter 使用 1.x 版本。如果你用的是更早的 API 版本个别接口名可能需要微调但整体设计思想是通用的。2. 路由表与路由请求的封装实现2.1 路由表统一管理的最佳实践HMRouter 虽然支持注解方式自动发现页面但注解里填的路径字符串如果散布在各页面文件中依然维护困难。我的做法是在项目里单独建一个RouterTable.ets文件用静态常量集中定义所有路径。比如// RouterTable.ets export class RouterTable { static readonly HOME: string home/index; static readonly LOGIN: string login/index; static readonly ORDER_DETAIL: string order/detail; static readonly PROFILE: string profile/index; static readonly WEBVIEW: string common/webview; static readonly ERROR_PAGE: string common/error; }页面注解里直接引用静态常量Router({ path: RouterTable.HOME }) Component export struct HomePage { // ... }这样做的优势在于全局搜索路径引用时只需要看RouterTable一个文件重命名路径时也能通过编译错误快速找到所有引用点。路径收敛之外还要解决参数对象化的问题。HMRouter 的Want只支持基础数据类型数组传递对象传参通常需要JSON.stringify后放入Want的params再由目标页解析。为了让调用方零感知我在请求封装里内置了一套参数序列化工具核心逻辑是这样的export namespace RouterParamUtil { export function putParam(want: Want, key: string, value: Object): void { if (value null || value undefined) { return; } if (typeof value object) { want.parameters[key json] JSON.stringify(value); } else { want.parameters[key] value; } } export function getParamT(want: Want, key: string, defaultValue: T): T { const raw want.parameters?.[key]; if (raw ! null raw ! undefined) { return raw as T; } const json want.parameters?.[key json]; if (json) { try { return JSON.parse(json as string) as T; } catch (e) { return defaultValue; } } return defaultValue; } }这里我用了json后缀来区分对象类型参数避免与基本类型参数冲突。实际使用中目标页面统一调用getParam方法获取数据调用方统一使用putParam写入数据序列化细节完全收敛在工具层。2.2 路由请求对象的完整定义有了路由表和参数工具接下来就是把“一次跳转”封装成一个完整的请求对象。我定义了一个RouterRequest类它承载一次路由跳转的全部信息export class RouterRequest { path: string ; paramMap: Recordstring, Object {}; needLogin: boolean false; showLoading: boolean true; loadingText: string 加载中...; timeout: number 5000; onResult: (code: number, data?: Object) void () {}; onError: (code: number, msg: string) void () {}; }调用方式设计成链式组装风格接近 OkHttp 的 RequestBuilderRouterService.getInstance() .buildRequest(RouterTable.ORDER_DETAIL) .put(orderId, 123456) .put(source, home_banner) .needLogin(true) .showLoading(true) .onResult((code, data) { // 处理页面返回结果 }) .onError((code, msg) { // 统一处理失败 }) .execute();这里execute方法内部做几件事首先是组装Want把paramMap里所有参数写入Want.parameters然后再检查全局拦截器链和页面级拦截器所有检查通过后调用RouterService.getService().pushUrl执行真实跳转。跳转完成后根据返回结果回调onResult或onError。需要特别说明的是onResult回调不只是页面跳转成功后触发而是页面通过finish返回并携带结果数据时触发。这就解决了跨页面回传数据的类型安全问题。3. 拦截器链路与页面生命周期钩子3.1 拦截器职责划分与优先级编排拦截器是路由框架的灵魂也是封装中最容易失控的部分。我在工程里按照职责划分了五类拦截器LoginInterceptor检查页面是否需要登录态未登录则跳转登录页并挂起原跳转。PrivacyInterceptor隐私合规声明弹窗未确认时拦截所有跳转并弹出合规弹窗。NetworkInterceptor对需要网络请求的页面做网络状态预检无网络时给出友好提示。TrackInterceptor统一记录页面访问埋点不阻断跳转。RouterDegradeInterceptor捕获路由异常进入降级流程。每个拦截器实现同一个接口export interface RouterInterceptor { priority: number; process(context: RouterContext, chain: RouterInterceptorChain): void; }RouterContext中保存了当前请求对象、目标页面信息、登录态等上下文数据。RouterInterceptorChain负责按优先级顺序逐个执行拦截器并允许拦截器终止链路export class RouterInterceptorChain { private interceptors: RouterInterceptor[]; private index: number 0; private request: RouterRequest; proceed(context: RouterContext): void { if (this.index this.interceptors.length) { const interceptor this.interceptors[this.index]; interceptor.process(context, this); } else { // 所有拦截器执行完成真正跳转 RouterExecuteHelper.execute(this.request, context); } } abort(code: number, msg: string): void { this.request.onError(code, msg); } }拦截器每秒执行时有两个选择调用chain.proceed(context)放行给下一个或调用chain.abort终止流程。用这个模式实现“登录后继续原跳转”就非常简单登录成功后再手动调用一次chain.proceed即可。3.2 全局拦截与页面级拦截的组合策略在实际项目中某些拦截器只对特定页面生效。比如订单详情页要求登录但首页不需要支付页除了登录还需要实名认证。如果每个拦截器内部都写大量的页面判断逻辑拦截器会迅速臃肿。我的方案是将拦截器分为两种GlobalInterceptor和PageInterceptor。全局拦截器在路由表初始化时统一注册作用于所有跳转页面级拦截器通过自定义注解作用在页面组件上Router({ path: RouterTable.ORDER_DETAIL }) PageInterceptors([LoginInterceptor, RealNameInterceptor]) Component export struct OrderDetailPage { // ... }请求执行前框架会扫描目标页面注册的页面级拦截器与全局拦截器合并按照优先级统一排序后执行。这样每个页面的额外约束都能声明式表达而不是散落在跳转调用里。页面级拦截器实例通过反射创建也支持通过依赖注入替换实现类便于单元测试时打桩。3.3 生命周期钩子的封装HMRouter 本身对页面生命周期有一定支持但页面业务中常见到的场景是“从A页面跳B页面B页面操作完成返回后A页面需要刷新数据”。传统写法是A页面在onPageShow里加标志位判断非常容易遗漏。我封装了一个RouterCallbackManager在跳转时向目标页面传递一个callbackId目标页面操作完成后通过RouterService通知// A页面发起跳转 RouterService.getInstance() .buildRequest(RouterTable.ORDER_DETAIL) .put(callbackId, this.callbackId) .execute(); // 回调注册 RouterCallbackManager.register(callbackId, (data) { this.refreshOrderList(); });B页返回时只需要按需调用const callbackId RouterParamUtil.getParam(this.want, callbackId, ); RouterCallbackManager.invoke(callbackId, { status: updated });这个方案把跨页面通信从“监听生命周期 全局变量”中解放出来只要回调Id不丢失后续扩展传参和超时回收都很方便。我建议在实际项目里给callbackId加个时间戳前缀防止多次跳转生成重复ID导致回调被错误触发。4. 参数传递、返回结果与降级路由的完整封装4.1 参数安全传递的边界情况处理前面提到的putParam和getParam解决了对象传递的大部分场景但边界情况仍然很多。这里我把我踩过坑集中列出来每条都是真实线上遇到的问题。Date对象序列化后会变成字符串目标页反序列化后得到的是 string 而不是 Date。如果业务层强依赖时间对象建议在putParam之前先约定好格式用时间戳传递。ArrayBuffer和TypedArray在部分版本上不能直接放进Want.parameters需要转成Arraynumber或 base64 字符串再传递。undefined值在JSON.stringify中会被忽略但null会被序列化为null。我的putParam里对undefined单独做了跳过处理避免目标页拿到字符串undefined。传入参数对象中包含循环引用时JSON.stringify会抛异常这个异常不能被路由链吞掉需要在putParam里加一层 try-catch 并记录日志。Want.parameters的 key 命名不能包含点号或特殊字符项目统一用驼峰命名且前后端联调时的字段名不能直接复用为 param key需要有一层映射。针对这些边界我最后整理成了一个RouterParamRule校验器在buildRequest阶段对paramMap做遍历遇到不支持的类型直接通过onError返回错误码从源头避免了“页面打开后参数解析异常”这类隐蔽问题。4.2 页面返回数据与 Promise 化封装跨页面传值如果只用回调会出现回调地狱。特别是连续跳转多个页面收集数据时代码的嵌套层级会非常难看。为此我在回调版execute基础上又补了一个 Promise 化的executeForResultpublic executeForResultT(request: RouterRequest): PromiseRouterResultT { return new Promise((resolve, reject) { request.onResult (code, data) { resolve({ code: code, data: data as T }); }; request.onError (code, msg) { reject(new RouterException(code, msg)); }; this.execute(request); }); }调用方可以这样写const result await RouterService.getInstance() .buildRequest(RouterTable.SELECT_SKU) .put(goodsId, goodsId) .executeForResultSelectedSku(); if (result.code RouterResultCode.OK) { this.selectedSku result.data; }使用 Promise 化封装后页面回传数据类型通过泛型约束编译期能够校验。唯一要注意的是executeForResult创建的 Promise 不能被 GC 提前回收所以在RouterCallbackManager里必须维护一个未完成 Promise 的引用列表在结果返回或超时后统一清理。4.3 降级路由与故障自愈机制HMRouter 在目标页面不存在时会有默认行为但默认行为和业务诉求往往不匹配。我封装了一个三层降级策略。第一层目标页面未注册或路径错误时跳转到统一的ErrorPage并在页面上展示真实原因。第二层目标页面存在但跳转过程中发生异常如组件初始化失败捕获异常后返回错误码不跳转由调用方决定是否展示 Toast。第三层URL 路由如 WebView 链接跳转到 App 内部解析失败时走浏览器能力或应用市场这个场景一般配合深链使用。降级流程的触发点在RouterExecuteHelper中统一处理try { const result await RouterService.getService().pushUrl(want); if (!result) { this.handleDegrade(new RouterException(RouterResultCode.NAV_FAIL, pushUrl返回false)); } } catch (e) { this.handleDegrade(new RouterException(RouterResultCode.EXCEPTION, e.message)); }handleDegrade内部先判断是否配置了全局降级页面如果没有配置再回退到错误码回调。这样既保证了兜底的可视化又不会让错误信息流丢失。5. 性能优化与踩坑记录5.1 跳转速度优化实测我在联调环境里做过一组对比测试同一台测试机上分别用“裸 HMRouter”和“封装后的 RouterService”各跳转100次取平均耗时。测试结果是封装后单次跳转耗时比裸用增加约1~3毫秒主要开销集中在拦截器链创建、参数序列化检查和回调注册。这完全在可接受范围内因为页面跳转的耗时主体其实是页面渲染和组件初始化路由层多出来的这几毫秒用户感知不到。性能上真正要注意的反而是滥用拦截器导致的串行等待。某些拦截器如果内部有网络请求会让每次跳转都变慢。我的建议是拦截器里只做本地内存/偏好存储判断网络请求校验放到目标页面的异步加载流程中。如果实在需要网络校验拦截器内部也要做超时控制和缓存不能让网络抖动阻塞路由链路。5.2 实际问题排查速查表我把这段时间遇到的问题整理成表格方便大家对照排查问题现象可能原因解决思路跳转后页面空白页面未注册到路由表或路径大小写不一致检查注解中的 path 与 RouterTable 常量是否完全一致登录拦截后无法恢复原跳转登录页返回时未保存 RouterRequest 副本登录完成前将原请求暂存在拦截器上下文或单例中回调结果丢失目标页面在 callbackId 生效前就 finish回调注册时机提前到请求创建阶段而非 execute 阶段内存持续上涨未完成 Promise 未被回收在超时和跳转完成后主动删除回调引用参数解析得到字符串Want 对象序列化与解析版本不一致统一走 RouterParamUtil不要直接操作 parameters页面级拦截器未生效注解扫描时未加入拦截器包名确认 HMRouter 注解扫描配置中包含拦截器所在模块5.3 这套封装的后续扩展建议目前这套封装已经把路由请求、拦截、回调、降级四块串起来了但有几个扩展方向我认为值得尝试。一个是集成“路由分发中心”把深链映射、外部链接统一收口到 RouterService这样扫码跳转、推送跳转、WebView 跳 App 都能复用同一套拦截和降级能力。另一个是增加路由级别的单元测试框架因为拦截器链路一旦复杂人工验证成本会成倍增加。最后是配合性能监控平台把每一次路由跳转的时间、成功率和耗时统计透明化方便尽早发现跳转链路的劣化点。我在我们团队的落地经验是封装永远不是一次到位的先解决 80% 的通用场景跑通后再根据实际痛点迭代。HMRouter 只是一个路由引擎真正让它变得好用的是包裹在其外的这层业务语义。如果你也已经在项目里实践这套封装欢迎在评论区交流你遇到的边界场景一起把鸿蒙路由体系做得更扎实。
返回列表