十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

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

HarmonyOS路由跳转选型指南:Router与Navigation深度对比与实战

HarmonyOS路由跳转选型指南:Router与Navigation深度对比与实战 我最早在 HarmonyOS 上做页面跳转时第一反应是找router.pushUrl因为这种写法最接近“给一个地址打开一个页面”的直觉。但页面数量一多我开始意识到路由设计并不是“能跳过去”就完事。页面栈怎么控制、参数怎么传、从详情页返回后列表要不要刷新这些细节比选哪个 API 重要得多。这也是这篇文章想解决的HarmonyOS 里的路由跳转到底该怎么设计Router 和 Navigation 到底该选谁。看完官方文档你会发现Router 和 Navigation 都能实现页面跳转。Router 的入口在kit.ArkUI的 router 模块里Navigation 则是 ArkUI 提供的一个导航容器组件两者并不是同一个维度上的东西。很多开发者的真实困惑是文档说 A 能用、B 也能用为什么社区里越来越多声音说新项目要用 Navigation这篇文章会从实际项目出发把两套方案的设计逻辑、典型写法、维护成本和踩坑点一次讲完帮你做出适合自己团队的选择。我做应用型产品开发最近两年的主力端就是 HarmonyOS。下面这些内容不会停留在 API 层面更多是放在“怎么搭一套能维护两三年的页面导航架构”这个尺度上。如果你正在从 0 搭 HarmonyOS 应用或者考虑把老项目迁移到新版导航架构这篇内容应该有参考价值。1. 先给结论为什么新项目我更推荐 Navigation1.1 一句话版本先放结论后面再展开。新项目直接用 Navigation。老项目如果只是加几个页面用 Router 不会有致命问题但如果老项目正在重构或者页面层次越来越深越早迁到 Navigation 越从容。为什么我敢这么直接因为当前 HarmonyOS 的演进方向已经很明显了。官方文档里 router 模块仍然存在但新的导航能力、新的页面管理特性基本都围绕 Navigation 展开。从“未来演进”的角度看Navigation 是更值得押注的方向。1.2 两套方案出现的背景要理解两套方案得先看它们各自的背景。早期 HarmonyOS 版本里页面之间的跳转用得最多的是 Router。这个模型非常直观先在系统层面维护一份页面清单然后通过一个 URL 字符串告诉系统“我要打开哪个页面”。对新手来说几乎不用理解什么栈和容器照着pushUrl的签名写就能跑通。Navigation 是后来逐渐成熟的方案。它不是简单的 API 替换而是把导航能力真正下沉到 ArkUI 组件体系里。Navigation 组件负责承载一个页面栈NavPathStack负责管理这个栈目标页面用NavDestination包裹。跳转的本质变成了“往我自己的栈里压一个页面项”整个栈生命周期都暴露给开发者。这两种模型的核心差异在于Router 是“全局路由表 系统统一出栈入栈”Navigation 是“页面容器 开发者可控栈”。前者简单直接后者灵活复杂。到了 HarmonyOS 面向复杂应用演进的时候Router 的模型就开始吃力了Navigation 顺势成了更主流的选型。2. Router 的定位与适用场景2.1 Router 的推荐写法先看一段最常规的 Router 跳转以当前主流的kit.ArkUI导入为例。场景是首页列表进入商品详情页需要携带商品 id 和来源。import { router } from kit.ArkUI; router.pushUrl({ url: pages/DetailPage, params: { id: 1001, from: home } });目标页面里这样取参数const params router.getParams() as Recordstring, Object; const id params.id as number; const from params.from as string;返回时直接调用router.back();这套写法本身没什么问题。它在系统内部维护的是一个全局栈pushUrl入栈back出栈页面生命周期由系统统一调度。如果一个应用只有几个页面这个模式是够用的。不过这里有一个细节很多人初学时会忽略params在传递过程中会经历序列化这意味着它并不适合承载复杂对象。传一个Date对象过去接收端拿到的可能已经不是原来的Date而是一个时间戳字符串或别的什么。传Map、Set、自定义 class 实例也是如此。常规做法是只传叶子数据比如时间戳params: { createAt: Date.now() }接收后再自行new Date(createAt)还原。这是注意点不是大坑但提前知道能省不少调试时间。2.2 Router 的核心短板Router 真正的短板不是“能不能跳”而是页面多了以后的可维护性。第一类型不安全。url是字符串params是Recordstring, Object。页面名改了、参数名改了编译期不会报错运行期可能直接取到 undefined。对于一个几十个页面的项目来说这等于在路由层面埋了大量隐性地雷。第二路由表容易膨胀。页面数量越多需要集中维护的页面配置就越杂乱。团队多人协作时经常出现改了一个页面路径忘了另一个地方还在旧路径跳转的情况。第三栈操作能力弱。Router 提供的能力核心就是pushUrl、replaceUrl、back这一套。但真实业务里有大量“回到某个指定页面并刷新”“把中间某个页面移除”“清空整个栈再进入首页”的需求。Router 不是不能做而是要做就得自己维护额外状态很别扭。第四页面状态刷新逻辑容易散落。Router 模式下页面从后台返回前台的刷新主要依赖onPageShow这种页面生命周期。页面简单的时候没问题但一旦页面之间互相影响你会发现onPageShow里要处理的判断条件越来越多最终变成一团乱麻。2.3 适合 Router 的具体场景说了一堆短板但不代表 Router 该被丢弃。我实际开发中的经验是下面这几类场景用 Router 完全没问题页面数量很少大概 10 个以内且后续几乎不扩展。单链路流程比如“启动页 - 首页 - 详情页”没有太多页面组合关系。快速原型、Demo 演示给领导或客户看效果不需要考虑长期维护。团队刚刚接触 HarmonyOS先通过 Router 理解页面生命周期再逐步上 Navigation。一旦发现页面数量超过 20 个或者出现了底部 Tab、二级详情、下单流程回退等复杂结构就该认真考虑 Navigation 了。3. Navigation 的设计思路与核心能力3.1 从组件角度理解 NavigationNavigation 和 Router 最大的不同在于它不再是一个全局跳转 API而是一个页面容器组件。打个比方Router 是你告诉前台“我要去 302 房间”由全局系统帮你开门Navigation 是你自己手里握着一串钥匙想把哪个房间打开、把哪扇门关上完全由自己决定。每个 Navigation 组件都会绑定一个NavPathStack页面切换就是对栈的压入和弹出操作。先看最基本的用法。首页作为入口页import { Navigation, NavPathStack } from kit.ArkUI; Entry Component struct Index { private pageStack: NavPathStack new NavPathStack(); Builder pageMap(name: string, param: unknown) { if (name product_detail) { ProductDetailPage({ id: (param as ProductParam).id, from: (param as ProductParam).from }); } } build() { Navigation(this.pageStack) { Button(打开商品详情) .onClick(() { this.pageStack.pushPath({ name: product_detail, param: { id: 1001, from: home } }); }) } .navDestination(this.pageMap) } }目标页面Component export struct ProductDetailPage { id: number 0; from: string ; build() { NavDestination() { Text(商品ID${this.id}) } .title(商品详情) .onShown(() { console.info(detail shown); }) } }这里有几个关键点需要理解Navigation(this.pageStack)把导航容器和具体的栈绑定。.navDestination(this.pageMap)是页面映射表pageMap根据 name 返回对应的页面组件。目标页面必须用NavDestination作为根节点否则页面不会正确进入导航层级。页面和页面之间的跳转关系由 Navigation 自己管理不再依赖集中式的路由表。这意味着页面跳转从“改全局配置 写 URL”变成了“定义栈 映射组件 压栈”。前期多了一点理解成本但后续扩展的灵活性明显更强。3.2 栈操作能力是核心优势NavPathStack最重要的价值是提供了一整套可编程的栈操作能力。我在项目里常用的大概有这些pushPath压入一个新页面最常规的跳转。replacePath替换当前页面常用于“登录页跳首页”这类场景。pop返回上一页等价于系统返回。popToName返回到指定 name 的页面并把中间的页面弹出去。removeByIndexes按索引移除某个或某几个页面适合清理流程中的中间页。clear清空整个页面栈常用于回到根并重置状态。有了这些操作“回到第 N 层页面”“下单完成后清掉中间页”这类需求就不再是绕来绕去的 hack而是直接调用 API 就能实现的设计。3.3 与页面生命周期结合Navigation 模式下的页面生命周期和 Router 模式不太一样。Router 依赖的onPageShow在 Navigation 里不是主角更常用的是NavDestination上提供的onShown、onHidden回调。什么意思当你从详情页返回列表页时列表页如果是一个NavDestination它的onShown就会触发。利用这个时机刷新数据比在全局页面生命周期里猜“现在到底该不该刷新”要精准得多。NavDestination() { List({ space: 12 }) { // 列表内容 } } .onShown(() { this.loadList(); })不过要注意onShown每次页面可见都会触发如果每次都重新请求接口用户快速来回切换时会看到频繁 loading。一个比较实用的做法是加“过期时间”标记比如 30 秒内不重新拉取超过 30 秒才真正请求。这个是我实际维护列表页时常用的优化手段。4. 从真实场景看两种方案落地差异4.1 一个业务场景列表进详情再下单只看 API 很难体会差异我用一个相对完整的业务场景来对比。场景是这样首页是商品列表点击某个商品进入商品详情页详情页里可以继续进入下单确认页。用户从详情页返回时列表页需要刷新最新数据用户到达下单确认页后如果确认完成希望直接回到一个干净的状态而不是一层层返回。这个场景里包含了列表页、详情页、下单确认页三个节点涉及普通跳转、返回刷新、清理中间页三个核心诉求。用它来对比 Router 和 Navigation 最有说服力。4.2 Router 版本怎么落用 Router 实现这个场景时典型的代码路径是这样的首页通过router.pushUrl跳到pages/DetailPage。详情页通过router.pushUrl跳到pages/OrderPage。列表刷新依赖首页的onPageShow因为从详情页返回首页时会触发。如果下单完成后不希望回到详情页可以用router.replaceUrl把下单确认页替换成“支付结果页”。看起来都能实现但问题在于返回逻辑和页面业务耦合得很深。你要时刻记住当前页面栈里有哪些页面然后决定该用back、replaceUrl还是pushUrl。一旦页面层级加深维护成本会指数级上升。4.3 Navigation 版本怎么落同样场景用 Navigation 时的结构更清晰首页pageStack.pushPath到product_detail。详情页pageStack.pushPath到order_confirm。列表刷新绑定在首页NavDestination的onShown上返回时自然触发。下单确认完成时可以replacePath成pay_success或者直接popToName(product_list)回到列表并刷新。popToName这种“指哪打哪”的能力是 Router 模式下很难优雅做到的事。流程无论走到多深只要目标页面还在栈里就能一次返回到位。4.4 一张表看清各自定位我把两个方案的核心差异整理成一张表方便快速对照。对比维度RouterNavigation页面注册依赖集中页面配置路由关系靠 URL 维护页面映射在 Navigation 内部管理系统级配置负担更轻参数传递params 序列化类型不安全直接传对象可定义类型类型可控页面栈能力push/replace/back 为主栈操作有限push/replace/pop/popToName/clear 等栈能力丰富返回刷新主要靠 onPageShow作用范围偏全局可用 NavDestination 的 onShown/onHidden粒度更细复杂流程需要自己维护很多额外状态页面栈本身就是状态设计上更自然新项目推荐度简单场景可用不作为长期推荐当前和未来的主流方向这张表不是在说 Router 一无是处而是想说明两套方案在“可维护性”上的差异。页面只有三四个时差异很小页面二十个以上差异会被放大得非常明显。选型要看你项目的未来而不仅仅是眼前的功能。5. 踩坑清单和排查方法5.1 Navigation 页面白屏我第一次切 Navigation 的时候遇到过最诡异的问题就是白屏。pushPath执行后页面栈确实变了但新页面内容一片空白。排查到最后原因很简单目标页面没有用NavDestination作为根组件。Navigation 需要靠NavDestination来识别这是一个导航目标页如果目标页面直接写Column或Stack内容不会进入导航层级自然就白屏了。还有一个类似场景pageMap里的 name 和pushPath传的 name 不一致。比如跳转时写product_detail映射里写productDetail匹配不上页面也不会渲染。遇到白屏时优先检查这两处。5.2 返回后列表不刷新从详情页返回列表页列表还是旧数据。这个问题在我从 Router 迁移到 Navigation 时反复出现根因是惯性思维。Router 模式下页面返回时onPageShow会触发大家习惯在onPageShow里刷新数据。但 Navigation 模式下列表页根节点是NavDestination需要绑定onShown才能真正感知“页面重新可见”。NavDestination() { List({ space: 12 }) { } } .onShown(() { this.loadList(); })如果你发现返回后数据不刷新先检查是不是还在沿用 Router 的onPageShow而不是 Navigation 的onShown。5.3 Tabs 切换页面状态丢失底部 Tab 是另一个容易翻车的点。很多人会自然地把整个首页在一个 Navigation 里做结果切换 Tab 后部分页面状态被重置。原因是多个 Tab 共用同一个页面栈时切换过程可能涉及栈的清理或重建状态自然保不住。一个相对成熟的方案是每个 Tab 的内容区使用独立的 Navigation各自管理自己的NavPathStack。这样 Tab 和 Tab 之间的页面栈互不干扰业务上也更清晰。这种结构需要一开始就设计好后期再拆的成本会比较高。所以如果你的应用确定有底部 Tab导航骨架必须提前规划。5.4 参数里的 Date 和 Map 悄悄变形这个问题在 Router 和 Navigation 里都存在只要参数经过序列化和反序列化复杂类型就可能变形。我在一个项目里传过Date对象下一页拿到的时区不对传过Map下一页发现变成普通对象。这类问题最隐蔽因为控制台不报错只是运行结果不对。处理办法也很简单时间类数据传时间戳不要传Date实例。集合类数据先转数组再作为参数传递。自定义 class 实例不要直接传传一个普通 DTO 结构。路由参数的定位应该是“轻量标识”而不是“数据搬运工”。这个原则不仅能避开序列化问题还能让代码更简洁。5.5 Router 和 Navigation 混用的返回栈问题迁移过程中最容易遇到的就是混用。一部分页面用router.pushUrl另一部分用pageStack.pushPath用着用着发现返回逻辑完全乱掉。原因是两套导航体系各自维护各自的栈系统并不知道 Navigation 页面栈里当前有哪些页面。从 Navigation 页面调router.pushUrl跳到 Router 页面再按返回可能直接退出了应用或者回到一个意想不到的页面。所以一个工程里尽量统一导航体系。迁移期如果不得不混用最好把跳转入口收敛到一个服务里避免各页面直接调底层 API。下面一节我会专门讲这个过渡方案。6. 从 Router 迁到 Navigation 的过渡方案6.1 用一个 NavService 统一跳转入口老项目迁 Navigation最忌讳的是改到一半发现页面之间互相撕裂。一个可行的做法是先抽象一个导航服务 NavService所有业务页面只调用它不直接碰 Router 或 NavPathStack。import { router } from kit.ArkUI; import { NavPathStack } from kit.ArkUI; type RouteParam { name: string; param?: unknown; }; export class NavService { private static stack?: NavPathStack; static bind(stack: NavPathStack) { NavService.stack stack; } static push(route: RouteParam) { if (NavService.stack) { NavService.stack.pushPath({ name: route.name, param: route.param }); } else { router.pushUrl({ url: route.name, params: route.param as object }); } } static back() { if (NavService.stack) { NavService.stack.pop(); } else { router.back(); } } }入口页创建 Navigation 时执行绑定NavService.bind(this.pageStack);这样做的核心价值是业务页面不关心底层是 Router 还是 Navigation只关心NavService.push和NavService.back。后续 Navigation 覆盖到位后把 Router 分支删掉即可业务代码完全不用动。6.2 迁移顺序从叶子页面开始NavService 只是过渡方案真正切换 Navigation 还是要有阶段计划。我的经验是不要一次性大爆炸式重建按“叶子页 - 中间页 - 容器页”的顺序来。意思是先迁流程末端页面比如详情页、支付成功页、结果页。这些页面被业务引用比较分散单独迁风险最低。迁完叶子页再迁中间承接页最后把首页改成 Navigation 容器。每次迁移一个页面后要重点回归两个点返回栈是否正常参数传递是否一致。混用期容易出现“从 Navigation 页面跳到 Router 页面后返回异常”所以每一轮迁移后都要跑一遍完整链路不要等到全部迁完再统一测试。6.3 到底按什么标准选型发展到这一步你一定想知道一个相对明确的判断标准。我按自己做项目的经验给一个可以“抄作业”的清单页面少于 10 个后续业务也不复杂Router 完全够用不用折腾。页面在 10 到 20 个之间相互关联不深两套都能用我更倾向 Navigation因为传参类型更安全。有底部 Tab、多级详情、复杂订单流程必须 Navigation没有悬念。团队刚开始接触 ArkUI可以先让团队用 Router 快速跑通需求但架构上必须预留 NavService 这一层方便后续切换。老项目要大改版直接切 Navigation别继续给 Router 打补丁。选型的核心不是“哪个 API 更高级”而是“项目未来到底会长多大”。这一点想清楚Router 和 Navigation 的选择就不会再摇摆。7. 路由设计时容易被忽略的几件小事7.1 路由只传轻量标识不要传整块大数据很多新手喜欢把列表页已经拿到的整个对象塞进路由参数比如把整个商品对象传进详情页。这个做法有两个问题一是序列化成本高复杂对象容易变形。 二是业务耦合变重。如果详情页只依赖 id那数据可以从缓存或数据层拉取可一旦依赖整个对象后续数据源变化路由参数这里也要跟着改。正确做法是只传轻量标识this.pageStack.pushPath({ name: product_detail, param: { id: product.id, from: home } });详情页拿到 id 后自行加载。这个原则对 Router 同样适用只是 Navigation 在类型安全上更友好更容易帮助团队养成好习惯。7.2 给路由起业务名而不是组件名Navigation 的 name 不要求等于页面组件类名这一点很容易被忽略。把它定义为“业务路径”而不是“页面类名”后续收益很大。比如详情页的组件叫ProductDetailPage路由名可以叫product_detail。将来组件改名、重构路由逻辑不会受影响。如果后续要接统一跳转入口业务名也更方便对外暴露和映射。为了避免字符串魔法值到处出现建议集中定义常量export const RouteName { ProductList: product_list, ProductDetail: product_detail, OrderConfirm: order_confirm } as const;跳转时引用RouteName.ProductDetail拼写错误在编译期就能发现。7.3 提前想清楚“终点页面回到哪”业务里有一类需求很常见用户从 A 页面进入 B 页面再进入 C 页面在 C 完成某个操作后希望回到的不是上一页 B而是 A 或某个指定页面。这种返回链路最忌在代码里硬编码。比较优雅的做法是在进入流程时把“回退到哪”作为参数传入让最后一个页面知道自己的终点在哪里。this.pageStack.pushPath({ name: order_confirm, param: { orderId: xxx, backTo: RouteName.ProductList } });完成操作后确认目标页面还在栈里再执行popToName回到希望的位置。这样整个流程看起来就像一张清晰的地图而不是一条被写死的回退线。我在实际项目中默认用 Navigation 搭骨架但也不会把 Router 当作必须删掉的代码。两套方案并存期间我用 NavService 屏蔽底层。踩过几次坑之后最大的体会是路由跳转的表层问题是“用哪个 API”底层问题其实是“页面之间的依赖怎么治理”。如果让我给一个最实用的建议那就是在新项目的第 0 天就引入一个统一跳转入口无论底层选什么后续调整都不会伤筋动骨。希望这些经验能帮你少踩几个我踩过的坑。
返回列表