HarmonyOS 平行视界购物模式:商品列表“左点右出“开发拆解【鸿蒙心迹】

👋 你好,欢迎来到我的博客!我是【菜鸟学鸿蒙】
我是一名在路上的移动端开发者,正从传统“小码农”转向鸿蒙原生开发的进阶之旅。为了把学习过的知识沉淀下来,也为了和更多同路人互相启发,我决定把探索 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 的栈内容不会被清空。这意味着用户在展开态看着右侧详情,合上屏,手机态下进入的仍然是刚才的详情页,体验上是连续的。
七、手机态兼容排查顺序
当分栏效果在大屏正常、但手机上出现问题时,按以下顺序检查:
- 确认
mode是AUTO,不是硬编码为SPLIT(硬编码 SPLIT 在手机上会强制分栏,左侧 NavBar 和右侧 Content 同时出现在窄屏里,布局错乱); - 检查
navBarWidth是否设置了固定像素值(px单位),如果是,改为百分比或vp; - 确认手机窗口宽度 < 520vp,如果手机以横屏运行,窗口宽度可能超过 520vp,
AUTO模式会切回分栏,这是预期行为; - 检查
NavDestination是否正确作为根节点,手机单栏下NavDestination的标题栏展示比分栏更明显,如果标题栏缺失,基本就是这里的问题; - 检查返回操作,单栏下返回需要能回到列表页,确认
replacePath没有在初始进入时把列表页本身也替换掉。
开发经验总结
- 购物模式的核心是
replacePath,导航模式的核心是pushPathByName;理清这两者的语义,右侧内容的层级关系就顺了。 NavigationMode.AUTO统一处理了大屏分栏和小屏单栏,不需要在业务代码里手动判断设备类型来切换模式。NavPathStack的状态在折叠/展开切换时保持连续,这是平台保证的行为,可以放心依赖。navDestination构建函数是整个路由体系的"注册表",路由名称建议集中用常量管理,避免字符串散落各处难以维护。- 右侧区域的"初始空白"是已知行为,需要主动给出占位内容或默认推入首个条目,而不是等用户先点击。
如果你正在做折叠屏电商类应用,可以重点观察一下:在展开态下,用户频繁切换商品时,右侧的导航栈深度是否在持续增长——这往往是 pushPathByName 和 replacePath 选错了的最直接信号。
📝 写在最后
如果你觉得这篇文章对你有帮助,或者有任何想法、建议,欢迎在评论区留言交流!你的每一个点赞 👍、收藏 ⭐、关注 ❤️,都是我持续更新的最大动力!
我是一个在代码世界里不断摸索的小码农,愿我们都能在成长的路上越走越远,越学越强!
感谢你的阅读,我们下篇文章再见~👋
✍️ 作者:菜鸟不学编程
🧵 本文原创,转载请注明出处。
更多推荐
所有评论(0)