👋 你好,欢迎来到我的博客!我是【菜鸟学鸿蒙】
   我是一名在路上的移动端开发者,正从传统“小码农”转向鸿蒙原生开发的进阶之旅。为了把学习过的知识沉淀下来,也为了和更多同路人互相启发,我决定把探索 HarmonyOS 的过程都记录在这里。
  
  🛠️ 主要方向:ArkTS 语言基础、HarmonyOS 原生应用(Stage 模型、UIAbility/ServiceAbility)、分布式能力与软总线、元服务/卡片、应用签名与上架、性能与内存优化、项目实战,以及 Android → 鸿蒙的迁移踩坑与复盘。
  🧭 内容节奏:从基础到实战——小示例拆解框架认知、专项优化手记、实战项目拆包、面试题思考与复盘,让每篇都有可落地的代码与方法论。
  💡 我相信:写作是把知识内化的过程,分享是让生态更繁荣的方式。
  
   如果你也想拥抱鸿蒙、热爱成长,欢迎关注我,一起交流进步!🚀

前言

在商城、订票、酒店预订这类 App 里,用户的使用习惯是:快速浏览列表、反复切换商品对比、最终进入下单页面。如果用传统全屏跳转,每次切换都要导航一次,在折叠屏展开态或平板的大屏空间里,右侧大半屏幕就这样被浪费掉了。

HarmonyOS 的 Navigation 组件提供了分栏能力,配合平行视界场景,可以让左侧列表保持常驻、右侧实时展示商品详情——这就是"左点右出"的核心。这篇文章从官方接口出发,把这个模式拆开来看。

一、为什么电商场景特别适合平行视界

普通页面跳转是全局替换:用户离开列表页、进入详情页、再返回列表。这个流程没问题,但在展开态折叠屏或平板这样的大屏设备上,每次全屏跳转会让整个屏幕只展示一个页面的内容,信息密度极低。

平行视界的分栏能力解决的正是这个问题:左侧固定显示列表(NavBar),右侧动态加载目标页(Content 区域)。点击不同商品,右侧内容更新,左侧列表不跳走。对比模式、快速切换、反复浏览都变得自然。

商城、票务、酒店三类场景的共同特征是:

  • 列表项数量多,用户需要横向比较;
  • 详情内容丰富,适合占据大屏右半部分;
  • 频繁进出详情,列表常驻有实际价值。

这些特征正好和 Navigation 组件的 SPLIT 模式匹配。

二、先把 Navigation 的分栏规则弄清楚

HarmonyOS 的 Navigation 组件(@ohos.arkui.advanced.Navigation 实际属于 ArkUI 框架内置组件,直接通过 Navigation 使用)支持三种模式,通过 mode 属性控制:

mode 枚举值含义
NavigationMode.STACK纯栈模式,始终全屏展示单个页面
NavigationMode.SPLIT强制分栏,左侧 NavBar + 右侧 Content
NavigationMode.AUTO自适应,由系统根据窗口宽度自动切换

AUTO 模式的切换阈值 由官方定义:窗口宽度 ≥ 520vp 时自动切换为分栏,< 520vp 时退化为单栏(STACK 行为)。这个数值来自官方文档,实际开发时不要硬编码这个数字,交给 AUTO 模式处理就可以了。

与导航模式的区别: 普通内容导航(例如设置项、菜单项)适合 NavigationMode.SPLIT,点击后右侧替换;购物场景的"购物模式"本质上也是分栏,但区别在于:

  • 导航模式:每次点击都是"进入下一层级",有明确的层级关系,返回键回溯层级;
  • 购物模式:左侧列表是平级内容,点击不同商品只是"切换右侧内容",不增加导航栈深度;从右侧进入下单等下一级页面时,才会真正入栈。

这个区别决定了如何调用 NavPathStack。

相关 API:NavigationMode 枚举、Navigation 组件的 mode 属性、navBarWidth 属性。
支持的最低 API Level:NavigationMode 自 API 10(HarmonyOS 4.0)起支持。HarmonyOS 7(API Level 14/15)完整支持。

三、搭一个最小实践场景

目标:

  • 左侧展示商品列表;
  • 点击任意商品,右侧更新展示对应详情;
  • 右侧可以继续跳转到下单页(入栈);
  • 手机窄屏自动退化为单栏全屏跳转。

需要的结构:

Navigation
├── NavBar(左侧,商品列表)
│   └── List + ListItem(各商品条目)
└── NavDestination(右侧,商品详情 / 下单页)

页面由一个 Navigation 组件承载,通过 NavPathStack 管理右侧导航栈。

四、核心代码实现

4.1 共享 NavPathStack

购物模式中,左侧列表和右侧详情需要共用同一个 NavPathStack 实例,通常通过状态管理在父组件中创建,传递给子组件。

// ShopPageStack.ets
// 用 AppStorage 或直接在根 Navigation 页面持有 stack 实例
// 这里用组件内 @State 演示最小结构

@Entry
@Component
struct ShopMainPage {
  // 创建共享导航栈,传递给 Navigation
  @Provide('shopStack') shopStack: NavPathStack = new NavPathStack();

  build() {
    Navigation(this.shopStack) {
      // NavBar 区域:左侧商品列表
      ProductListView()
    }
    .mode(NavigationMode.AUTO)          // 自适应:大屏分栏、小屏单栏
    .navBarWidth('40%')                 // 左侧 NavBar 占 40% 宽度
    .hideTitleBar(true)
    .navDestination(ShopNavDestBuilder) // 注册 NavDestination 构建函数
  }
}

navDestination 接收一个构建函数,用于根据路由名称渲染右侧页面内容。

4.2 注册 NavDestination 路由映射

// 路由构建函数,注册商品详情页和下单页
@Builder
function ShopNavDestBuilder(name: string, param: Object) {
  if (name === 'ProductDetail') {
    ProductDetailPage({ param: param as ProductDetailParam })
  } else if (name === 'OrderPage') {
    OrderPage({ param: param as OrderParam })
  }
}

这里 name 是路由名称字符串,和后续 pushPathByName / replacePath 调用时保持一致。

4.3 左侧商品列表:点击时替换右侧内容

"购物模式"的关键:点击列表项时,用 replacePath 替换右侧当前页面,而不是 pushPathByName(后者会在栈里新增一层)。

// ProductListView.ets
@Component
struct ProductListView {
  @Consume('shopStack') shopStack: NavPathStack;

  // 模拟商品数据
  private products: ProductItem[] = [
    { id: '001', name: '商品A', price: 199 },
    { id: '002', name: '商品B', price: 299 },
    { id: '003', name: '商品C', price: 399 },
  ];

  build() {
    List() {
      ForEach(this.products, (item: ProductItem) => {
        ListItem() {
          Row() {
            Text(item.name).fontSize(16)
            Blank()
            Text(`¥${item.price}`).fontSize(14).fontColor('#999')
          }
          .width('100%')
          .padding(16)
        }
        .onClick(() => {
          // 购物模式:替换右侧内容,不增加栈深度
          // 分栏时:替换右侧 Content 区域
          // 单栏时:等效于全屏跳转
          this.shopStack.replacePath({
            name: 'ProductDetail',
            param: { productId: item.id, productName: item.name } as ProductDetailParam
          });
        })
      })
    }
    .width('100%')
    .height('100%')
  }
}

replacePath 是这里的核心选择。它替换栈顶页面,使得左侧列表不会因为多次点击而在右侧积累多层历史,这正是购物模式和普通导航模式的本质区别。

NavPathStack.replacePath 自 API 11 起支持。HarmonyOS 7 可用。

4.4 右侧商品详情页:继续进入下一级

从详情页进入下单页,属于真正的层级跳转,用 pushPathByName:

// ProductDetailPage.ets
@Component
struct ProductDetailPage {
  param: ProductDetailParam = { productId: '', productName: '' };
  @Consume('shopStack') shopStack: NavPathStack;

  build() {
    NavDestination() {
      Column() {
        Text(this.param.productName)
          .fontSize(24)
          .margin({ bottom: 16 })

        Text(`商品ID:${this.param.productId}`)
          .fontSize(14)
          .fontColor('#666')

        Blank()

        Button('立即购买')
          .width('80%')
          .onClick(() => {
            // 进入下单页:入栈,产生新的导航层级
            this.shopStack.pushPathByName('OrderPage', {
              productId: this.param.productId
            } as OrderParam);
          })
      }
      .width('100%')
      .height('100%')
      .padding(20)
    }
    .title(this.param.productName)
    .hideTitleBar(false)
  }
}

NavDestination 是右侧内容区域的承载容器。每个路由目标页面都需要用 NavDestination 包裹。

4.5 返回操作

Navigation 组件会根据 NavPathStack 的栈深度自动管理返回行为:

  • 如果栈中有多层(例如从详情进入了下单页),点击返回回到详情页;
  • 如果栈只剩最后一层,系统会根据 NavigationMode 决定行为:分栏时右侧清空回到"无选中"初始状态,单栏时退出页面。

如果需要自定义"返回到初始态"的逻辑(例如右侧清空时左侧取消高亮),可以监听 NavPathStack 的变化:

// 在 ShopMainPage 中监听栈变化
this.shopStack.setInterception({
  willShow: (from: NavDestinationContext | NavBar, to: NavDestinationContext | NavBar,
    operation: NavigationOperation, animated: boolean) => {
    // 当导航至 NavBar(即右侧清空)时,清除列表选中状态
    if (to instanceof NavBar) {
      this.selectedProductId = '';
    }
  }
});

NavPathStack.setInterception 自 API 12 起支持。如果项目最低版本低于 API 12,需要另行判断。

五、几个关键点拆开看

1. replacePath vs pushPathByName,一定要选对

购物场景列表点击用 replacePath,这样右侧始终只有一层"当前商品详情"。如果用 pushPathByName,用户在多个商品之间点击后,右侧栈会积累多层详情页,返回行为就会变成"回到上一个商品详情",而不是"回到列表"。这是购物模式和导航模式最直观的区别。

2. navDestination 构建函数必须在 Navigation 所在文件或正确引用位置注册

@Builder 修饰的路由构建函数需要和 Navigation 组件在同一构建上下文中可见。如果把构建函数放在单独文件里但没有正确引入,路由会匹配不到,右侧页面显示为空白。

3. navBarWidth 在分栏时控制左侧宽度,单栏时无效

navBarWidth 只对 SPLIT 分栏生效。在 AUTO 模式下退化为单栏时,这个属性自动失效,不需要手动处理。

4. NavigationMode.AUTO 的 520vp 阈值是系统行为,不要自行实现切换逻辑

不少开发者会在 AUTO 模式之外,额外用 mediaQuery 或 windowSizeChange 手动判断窗口宽度再切换 mode。这样做是多余的,而且可能导致切换时机不一致。AUTO 模式已经处理好了,直接用即可。

5. 初始状态下右侧内容为空

在 NavigationMode.SPLIT 分栏时,如果 NavPathStack 是空的(没有任何路由记录),右侧 Content 区域会显示空白。实际项目中通常有两种处理方式:

  • 应用进入时自动推入默认商品详情(pushPathByName,先推后不再 replace);
  • 在 Navigation 的 content 区域提供一个占位提示,如"请选择商品"。

六、容易踩坑的地方

NavDestination 必须直接作为路由目标组件的根节点

NavDestination 不能嵌套在其他容器内部,它必须是路由目标组件 build() 返回的根节点。如果在 NavDestination 外面再套一层 Column 或 Stack,标题栏和返回行为都会失效。

@Consume 的 key 必须和 @Provide 完全一致

NavPathStack 通过 @Provide/@Consume 在组件树中传递。key 字符串区分大小写,拼错会导致运行时找不到绑定,栈操作失效。可以用常量代替裸字符串来规避这类问题。

手机态(单栏)下,replacePath 行为等同于全屏替换跳转

在 NavigationMode.AUTO 退化为单栏时,replacePath 会替换当前全屏页面,这是预期行为。不需要为手机态单独写跳转逻辑,Navigation 组件已经统一处理了两种形态下的导航语义。

折叠态和展开态之间切换时,NavPathStack 保持不变

设备在折叠/展开时触发窗口尺寸变化,NavigationMode.AUTO 会相应切换分栏/单栏,但 NavPathStack 的栈内容不会被清空。这意味着用户在展开态看着右侧详情,合上屏,手机态下进入的仍然是刚才的详情页,体验上是连续的。

七、手机态兼容排查顺序

当分栏效果在大屏正常、但手机上出现问题时,按以下顺序检查:

  1. 确认 mode 是 AUTO,不是硬编码为 SPLIT(硬编码 SPLIT 在手机上会强制分栏,左侧 NavBar 和右侧 Content 同时出现在窄屏里,布局错乱);
  2. 检查 navBarWidth 是否设置了固定像素值(px 单位),如果是,改为百分比或 vp;
  3. 确认手机窗口宽度 < 520vp,如果手机以横屏运行,窗口宽度可能超过 520vp,AUTO 模式会切回分栏,这是预期行为;
  4. 检查 NavDestination 是否正确作为根节点,手机单栏下 NavDestination 的标题栏展示比分栏更明显,如果标题栏缺失,基本就是这里的问题;
  5. 检查返回操作,单栏下返回需要能回到列表页,确认 replacePath 没有在初始进入时把列表页本身也替换掉。

开发经验总结

  • 购物模式的核心是 replacePath,导航模式的核心是 pushPathByName;理清这两者的语义,右侧内容的层级关系就顺了。
  • NavigationMode.AUTO 统一处理了大屏分栏和小屏单栏,不需要在业务代码里手动判断设备类型来切换模式。
  • NavPathStack 的状态在折叠/展开切换时保持连续,这是平台保证的行为,可以放心依赖。
  • navDestination 构建函数是整个路由体系的"注册表",路由名称建议集中用常量管理,避免字符串散落各处难以维护。
  • 右侧区域的"初始空白"是已知行为,需要主动给出占位内容或默认推入首个条目,而不是等用户先点击。

如果你正在做折叠屏电商类应用,可以重点观察一下:在展开态下,用户频繁切换商品时,右侧的导航栈深度是否在持续增长——这往往是 pushPathByName 和 replacePath 选错了的最直接信号。

📝 写在最后

如果你觉得这篇文章对你有帮助,或者有任何想法、建议,欢迎在评论区留言交流!你的每一个点赞 👍、收藏 ⭐、关注 ❤️,都是我持续更新的最大动力!

我是一个在代码世界里不断摸索的小码农,愿我们都能在成长的路上越走越远,越学越强!

感谢你的阅读,我们下篇文章再见~👋

✍️ 作者:菜鸟不学编程
🧵 本文原创,转载请注明出处。

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐