HarmonyOS NEXT 实战:SideBar + Navigation 侧边导航布局完全指南

项目演示

前言:为什么侧边导航在 HarmonyOS 应用中如此重要

在移动应用设计中,导航结构决定了用户体验的核心走向。从 iOS 的 UITabBar + UINavigationController,到 Android 的 BottomNavigationView + Navigation,再到 HarmonyOS NEXT 全场景智慧生态的 SideBar + Navigation,每一种导航方案都承载着各自平台的设计哲学。

HarmonyOS NEXT 作为面向多端的新一代操作系统,不仅延续了经典的导航模式,更通过 SideBarContainerNavigation 的组合,为开发者提供了一套灵活、高效且跨端一致的侧边导航解决方案。

本文将深入剖析这一方案的核心原理,系统讲解 24 个关键 API 的使用技巧,并结合实战代码,帮助你从零构建一个完整的侧边导航应用。无论你是 HarmonyOS 新手还是经验丰富的开发者,都能从中获得有价值的参考。


第一章:核心架构解析

1.1 三大支柱组件

SideBar + Navigation 布局由三个核心组件协同工作:

组件 职责定位 类比理解
SideBarContainer 侧边栏容器,管理侧边栏的显示状态、位置、尺寸 抽屉式侧边栏管理器
Navigation 导航容器,负责主内容区的页面栈管理 页面路由容器
NavPathStack 导航路径栈,记录页面跳转历史 后进先出的栈结构

1.2 布局层级关系图

复制代码
┌─────────────────────────────────────────────────────┐
│                   SideBarContainer                   │
│  ┌───────────────┬─────────────────────────────────┐ │
│  │               │          Navigation              │ │
│  │               │  ┌─────────────────────────────┐ │ │
│  │   SideBar     │  │  NavDestination             │ │ │
│  │   (侧边栏)    │  │  ┌───────────────────────┐  │ │ │
│  │               │  │  │  页面内容              │  │ │ │
│  │  - 用户头像   │  │  │  (HomePage/...)        │  │ │ │
│  │  - 导航菜单   │  │  │                       │  │ │ │
│  │  - 设置入口   │  │  └───────────────────────┘  │ │ │
│  │               │  └─────────────────────────────┘ │ │
│  └───────────────┴─────────────────────────────────┘ │
└─────────────────────────────────────────────────────┘

1.3 数据流向详解

复制代码
用户点击侧边栏菜单项
        │
        ▼
navPathStack.pushPathByName('detail', param)
        │
        ▼
Navigation 检测到 NavPathStack 变化
        │
        ▼
触发 navDestination('detail', param) 回调
        │
        ▼
根据 'detail' 名称构建对应的 NavDestination
        │
        ▼
NavDestination 内部渲染具体页面组件

1.4 与传统方案的对比

特性 TabBar 导航 抽屉式导航 SideBar + Navigation
适用场景 2-5 个主要功能 功能较多、层级复杂 平板/大屏、内容密集
屏幕利用 底部固定占用 隐藏式抽屉 常驻或按需显示
交互方式 点击切换 手势/按钮唤起 点击、滑动、状态控制
跨端适配 手机优先 手机优先 手机/平板/PC 自适应

第二章:24 个关键 API 完全手册

第一部分:SideBarContainer 系列(6 个)

API 1:SideBarContainer() ------ 侧边栏容器

基本语法:

typescript 复制代码
SideBarContainer() {
  // 第一个子组件:侧边栏内容
  SideBarContent()
  // 第二个子组件:主内容区
  MainContent()
}

核心职责: SideBarContainer 是整个侧边导航布局的根容器,它管理着侧边栏和主内容区的布局关系。组件内部必须包含且仅能包含两个子组件,第一个为侧边栏,第二个为主内容区。

使用示例:

typescript 复制代码
@Builder
SideBarBuilder() {
  // 侧边栏内容
  Column() {
    Text('侧边栏')
  }
}

@Builder
MainBuilder() {
  // 主内容区
  Column() {
    Text('主内容')
  }
}

build() {
  SideBarContainer() {
    this.SideBarBuilder()
    this.MainBuilder()
  }
}

注意事项:

  • 侧边栏和主内容区的顺序固定,不可调换
  • 容器会自动处理侧边栏的显示/隐藏动画
  • 嵌套使用时需注意层级关系

API 2:.sideBarPosition() ------ 设置侧边栏位置

基本语法:

typescript 复制代码
.sideBarPosition(position: SideBarPosition)

枚举值说明:

枚举值 说明 适用场景
SideBarPosition.Start 左侧显示(默认) 大多数应用场景
SideBarPosition.End 右侧显示 特殊布局、RTL 语言

使用示例:

typescript 复制代码
SideBarContainer() {
  // ...
}
.sideBarPosition(SideBarPosition.Start)  // 左侧
// .sideBarPosition(SideBarPosition.End)  // 右侧

动态切换:

typescript 复制代码
@State position: SideBarPosition = SideBarPosition.Start

SideBarContainer() {
  // ...
}
.sideBarPosition(this.position)

// 点击按钮切换位置
Button('切换位置')
  .onClick(() => {
    if (this.position === SideBarPosition.Start) {
      this.position = SideBarPosition.End
    } else {
      this.position = SideBarPosition.Start
    }
  })

API 3:.sideBarWidth() ------ 配置侧边栏宽度

基本语法:

typescript 复制代码
.sideBarWidth(width: number | string)

参数类型:

  • 数值类型: 以 vp 为单位的固定宽度
  • 字符串类型: 百分比或带单位的字符串(如 '30%'、'260vp')

使用示例:

typescript 复制代码
// 固定宽度
.sideBarWidth(260)

// 百分比宽度
.sideBarWidth('30%')

// 响应式宽度(根据屏幕自适应)
.sideBarWidth('40%')  // 小屏幕 40%

最佳实践:

  • 手机端建议 240-280vp
  • 平板端建议 300-400vp
  • 可配合 .sideBarMinWidth().sideBarMaxWidth() 使用

API 4:.showSideBar() ------ 控制侧边栏显隐

基本语法:

typescript 复制代码
.showSideBar(isShow: boolean)

核心作用: 这是实现侧边栏开关的核心 API,通常配合状态变量和用户交互事件使用。

基础用法:

typescript 复制代码
@State isShow: boolean = false  // 默认隐藏

SideBarContainer() {
  // ...
}
.showSideBar(this.isShow)

// 点击切换
Text('☰')
  .onClick(() => {
    this.isShow = !this.isShow
  })

进阶技巧:

typescript 复制代码
// 动画过渡
@State isShow: boolean = false

// 在按钮上添加动画效果
Text('☰')
  .rotate({ angle: this.isShow ? 90 : 0 })
  .transition(TransitionEffect.OPACITY)
  .onClick(() => {
    animateTo({ duration: 300 }, () => {
      this.isShow = !this.isShow
    })
  })

与手势结合:

typescript 复制代码
SideBarContainer() {
  // ...
}
.showSideBar(this.isShow)
.gesture(
  PanGesture()
    .onActionUpdate((event: GestureEvent) => {
      // 右滑显示,左滑隐藏
      if (event.offsetX > 50) {
        this.isShow = true
      } else if (event.offsetX < -50) {
        this.isShow = false
      }
    })
)

API 5:.sideBarMinWidth() 和 .sideBarMaxWidth() ------ 宽度约束

基本语法:

typescript 复制代码
.sideBarMinWidth(minWidth: number | string)
.sideBarMaxWidth(maxWidth: number | string)

使用场景: 当侧边栏宽度需要动态调整时,设置最小/最大宽度约束可以避免极端情况。

使用示例:

typescript 复制代码
@State dynamicWidth: number = 280

SideBarContainer() {
  // ...
}
.sideBarWidth(this.dynamicWidth)
.sideBarMinWidth(200)   // 最小 200vp
.sideBarMaxWidth(360)   // 最大 360vp

实际应用:

typescript 复制代码
// 根据内容动态调整宽度
@State itemCount: number = 4
@State dynamicWidth: number = 280

Column() {
  Slider({ value: this.itemCount, min: 2, max: 8 })
    .onChange((value: number) => {
      this.itemCount = Math.floor(value)
      // 根据菜单项数量调整侧边栏宽度
      this.dynamicWidth = 200 + this.itemCount * 20
    })
}

API 6:.sideBarAutoHide() ------ 自动隐藏控制

基本语法:

typescript 复制代码
.sideBarAutoHide(autoHide: boolean)

默认值: true

功能说明: 当用户在主内容区滑动或点击时,是否自动隐藏侧边栏。

使用示例:

typescript 复制代码
SideBarContainer() {
  // ...
}
.showSideBar(this.isShow)
.sideBarAutoHide(true)   // 允许自动隐藏

不同场景的设置建议:

场景 设置 理由
手机竖屏 true 节省屏幕空间
平板横屏 false 充分利用大屏
PC 应用 false 常驻侧边栏

第二部分:Navigation 系列(6 个)

API 7:Navigation() ------ 导航容器

基本语法:

typescript 复制代码
Navigation(navPathStack: NavPathStack) {
  // 初始页面内容(可选)
}

参数说明: Navigation 必须接收一个 NavPathStack 实例,用于管理页面跳转历史。

创建 NavPathStack:

typescript 复制代码
@State navPathStack: NavPathStack = new NavPathStack()

完整示例:

typescript 复制代码
@Entry
@Component
struct Index {
  @State navPathStack: NavPathStack = new NavPathStack()

  build() {
    Navigation(this.navPathStack) {
      // 初始页面(可选)
      Text('首页')
    }
    .title('我的应用')
  }
}

与 SideBarContainer 结合:

typescript 复制代码
@Entry
@Component
struct Index {
  @State navPathStack: NavPathStack = new NavPathStack()

  build() {
    SideBarContainer() {
      SideBar() {
        // 侧边栏
      }
      Navigation(this.navPathStack) {
        // 主内容区
      }
    }
  }
}

API 8:.navDestination() ------ 路由目标构建器

基本语法:

typescript 复制代码
.navDestination(builder: (name: string, param: Object) => void)

参数说明: builder 回调接收两个参数:

  • name:路由名称,与 pushPathByName 的 name 对应
  • param:路由参数对象

核心作用: 这是 Navigation 实现页面路由的关键 API,通过回调函数根据不同的路由名称构建对应的 NavDestination。

基础使用:

typescript 复制代码
Navigation(this.navPathStack)
  .navDestination((name: string, param: Object) => {
    NavDestination() {
      if (name === 'home') {
        HomePage()
      } else if (name === 'detail') {
        DetailPage()
      }
    }
  })

推荐写法(使用 @Builder 分离):

typescript 复制代码
// 定义 @Builder
@Builder
PageRouter(name: string, param: Object) {
  NavDestination() {
    if (name === 'home') {
      HomePage()
    } else if (name === 'detail') {
      DetailPage()
    } else if (name === 'profile') {
      ProfilePage()
    } else {
      Text('未知页面')
    }
  }
}

// 使用
Navigation(this.navPathStack)
  .navDestination((name: string, param: Object) => {
    this.PageRouter(name, param)
  })

注意事项:

  • ArkTS 不支持匿名类型,所有页面组件需预定义
  • 复杂路由建议使用枚举或常量管理路由名称
  • 参数传递需注意类型安全

API 9:.title() ------ 配置导航标题

基本语法:

typescript 复制代码
.title(title: string | Resource | CustomBuilder)

三种配置方式:

方式一:字符串标题

typescript 复制代码
Navigation(this.navPathStack)
  .title('我的应用')

方式二:资源引用标题

typescript 复制代码
Navigation(this.navPathStack)
  .title($r('app.string.app_title'))

方式三:自定义标题栏

typescript 复制代码
@Builder
CustomTitle() {
  Row() {
    Text('返回')
      .fontSize(16)
      .fontColor('#007DFF')
      .onClick(() => {
        this.navPathStack.pop()
      })
    Text('详情页')
      .fontSize(18)
      .fontWeight(FontWeight.Bold)
      .layoutWeight(1)
    Text('更多')
      .fontSize(16)
      .fontColor('#666666')
  }
  .width('100%')
  .height(56)
  .padding({ left: 16, right: 16 })
}

Navigation(this.navPathStack)
  .title(this.CustomTitle)

动态标题:

typescript 复制代码
@State currentTitle: string = '首页'

Navigation(this.navPathStack)
  .title(this.currentTitle)
  .navDestination((name: string, param: Object) => {
    // 根据路由名称更新标题
    if (name === 'home') {
      this.currentTitle = '首页'
    } else if (name === 'detail') {
      this.currentTitle = '详情页'
    }
  })

API 10:.titleMode() ------ 标题显示模式

基本语法:

typescript 复制代码
.titleMode(mode: NavigationTitleMode)

枚举值说明:

模式 说明 效果
NavigationTitleMode.Mini 小标题模式 固定在顶部,紧凑显示
NavigationTitleMode.Full 大标题模式 可滚动收起,首次显示较大
NavigationTitleMode.Auto 自动模式(默认) 根据内容自动选择

使用示例:

typescript 复制代码
// 小标题模式
Navigation(this.navPathStack)
  .title('首页')
  .titleMode(NavigationTitleMode.Mini)

// 大标题模式(适合内容丰富的页面)
Navigation(this.navPathStack)
  .title('探索发现')
  .titleMode(NavigationTitleMode.Full)

效果对比:

复制代码
Mini 模式:固定高度 56vp,标题始终可见
┌─────────────────────┐
│  ← 探索发现    📧 ⚙️ │ 56vp
├─────────────────────┤
│                     │
│     页面内容         │
│                     │
└─────────────────────┘

Full 模式:初始高度 128vp,滚动收起至 56vp
┌─────────────────────┐ ← 初始 128vp
│                     │
│  探索发现           │
│  发现精彩内容        │
│                     │
├─────────────────────┤ ← 滚动后收起至 56vp
│  ← 探索发现    📧 ⚙️ │
├─────────────────────┤
│     页面内容         │
└─────────────────────┘

API 11:.hideTitleBar() ------ 隐藏标题栏

基本语法:

typescript 复制代码
.hideTitleBar(isHide: boolean)

使用场景:

  • 需要完全自定义顶部区域
  • 沉浸式全屏页面
  • 使用系统状态栏

使用示例:

typescript 复制代码
@State hideTitle: boolean = false

Navigation(this.navPathStack)
  .title('首页')
  .hideTitleBar(this.hideTitle)

// 详情页隐藏标题栏,使用自定义导航
NavDestination() {
  DetailContent()
  CustomNavigationBar()
}

隐藏后自定义导航栏:

typescript 复制代码
@Builder
CustomNavBar() {
  Row() {
    Text('← 返回')
      .fontSize(16)
      .fontColor(Color.White)
      .onClick(() => {
        this.navPathStack.pop()
      })
    Text('商品详情')
      .fontSize(18)
      .fontWeight(FontWeight.Bold)
      .fontColor(Color.White)
      .layoutWeight(1)
    Text('分享')
      .fontSize(16)
      .fontColor(Color.White)
  }
  .width('100%')
  .height(56)
  .padding({ left: 16, right: 16 })
  .backgroundColor('#333333')
}

API 12:.hideToolBar() ------ 隐藏工具栏

基本语法:

typescript 复制代码
.hideToolBar(isHide: boolean)

功能说明: Navigation 默认可能提供底部工具栏,可通过此 API 控制显隐。

使用示例:

typescript 复制代码
Navigation(this.navPathStack)
  .title('设置')
  .hideToolBar(true)  // 隐藏底部工具栏

常见场景:

  • 设置页面:不需要工具栏
  • 表单页面:避免遮挡输入
  • 全屏浏览:沉浸式体验

第三部分:NavPathStack 系列(6 个)

基本语法:

typescript 复制代码
let navPathStack: NavPathStack = new NavPathStack()

状态声明: 在 ArkTS 中,通常需要使用 @State 装饰器来确保状态变化能正确触发 UI 更新。

正确写法:

typescript 复制代码
@State navPathStack: NavPathStack = new NavPathStack()

完整示例:

typescript 复制代码
@Entry
@Component
struct Index {
  @State navPathStack: NavPathStack = new NavPathStack()

  aboutToAppear() {
    // 初始化:推入首页
    this.navPathStack.pushPathByName('home', null, true)
  }

  build() {
    Navigation(this.navPathStack)
      // ...
  }
}

注意事项:

  • NavPathStack 是引用类型,不要重新赋值
  • 所有操作通过方法调用进行(push、pop、clear)
  • 生命周期方法中初始化路由

API 14:.pushPathByName() ------ 推入新页面

基本语法:

typescript 复制代码
pushPathByName(name: string, param: Object, clearStack: boolean): void

参数详解:

参数 类型 说明
name string 路由名称,需与 navDestination 对应
param Object 传递给目标页面的参数
clearStack boolean 是否清空栈后再推入

三种典型用法:

用法一:首次加载(清空栈)

typescript 复制代码
aboutToAppear() {
  // 应用启动,清空所有历史,推入首页
  this.navPathStack.pushPathByName('home', null, true)
}

用法二:页面跳转(保留历史)

typescript 复制代码
// 点击商品进入详情页
ItemClick(item: Product) {
  this.navPathStack.pushPathByName('detail', { id: item.id }, false)
}

用法三:嵌套跳转

typescript 复制代码
// 详情页 → 评价页 → 评价详情页
GoToReview() {
  this.navPathStack.pushPathByName('review', { productId: 123 }, false)
}
GoToReviewDetail() {
  this.navPathStack.pushPathByName('reviewDetail', { reviewId: 456 }, false)
}

参数传递最佳实践:

typescript 复制代码
// 定义参数类(推荐)
class DetailParam {
  productId: number
  fromPage: string
  constructor(id: number, from: string) {
    this.productId = id
    this.fromPage = from
  }
}

// 传递参数
let param: DetailParam = new DetailParam(123, 'home')
this.navPathStack.pushPathByName('detail', param, false)

// 接收参数
.navDestination((name: string, param: Object) => {
  if (name === 'detail') {
    let detailParam: DetailParam = param as DetailParam
    DetailPage({ id: detailParam.productId })
  }
})

API 15:.pop() ------ 返回上一页

基本语法:

typescript 复制代码
pop(): void

功能说明: 从导航栈中弹出顶部页面,返回上一级。

基础用法:

typescript 复制代码
// 在自定义返回按钮中
Text('← 返回')
  .onClick(() => {
    if (this.navPathStack.size() > 1) {
      this.navPathStack.pop()
    }
  })

进阶:双击返回键退出应用

typescript 复制代码
@State lastBackTime: number = 0

.onBackPress(() => {
  if (this.navPathStack.size() > 1) {
    this.navPathStack.pop()
    return true
  }
  
  let currentTime: number = Date.now()
  if (currentTime - this.lastBackTime < 2000) {
    return false  // 退出应用
  } else {
    this.lastBackTime = currentTime
    promptAction.showToast({ message: '再按一次退出' })
    return true  // 拦截返回
  }
})

pop 与 clear 的区别:

复制代码
pop():    返回上一页,栈中还有历史记录
clear():  清空所有页面,回到初始状态(相当于重启)

pushPathByName(name, param, true):
           清空栈后推入新页面(推荐初始化用)

API 16:.clear() ------ 清空导航栈

基本语法:

typescript 复制代码
clear(): void

使用场景:

  • 退出登录,返回登录页
  • 完成支付,回到首页
  • 流程结束,重置状态

使用示例:

typescript 复制代码
// 退出登录
Logout() {
  // 清除用户数据
  PreferencesUtil.clear()
  // 清空导航栈
  this.navPathStack.clear()
  // 推入登录页
  this.navPathStack.pushPathByName('login', null, true)
}

clear + push 组合:

typescript 复制代码
// 完成订单流程后返回首页
CompleteOrder() {
  // 1. 清空购物车
  ShoppingCart.clear()
  // 2. 清空导航栈
  this.navPathStack.clear()
  // 3. 回到首页
  this.navPathStack.pushPathByName('home', null, true)
}

API 17:.size() ------ 获取栈大小

基本语法:

typescript 复制代码
size(): number

功能说明: 返回当前导航栈中的页面数量。

实用场景:

场景一:控制返回按钮显示

typescript 复制代码
Column() {
  if (this.navPathStack.size() > 1) {
    Text('← 返回')
      .onClick(() => {
        this.navPathStack.pop()
      })
  }
  // 页面内容...
}

场景二:调试导航状态

typescript 复制代码
Button('查看栈状态')
  .onClick(() => {
    let size: number = this.navPathStack.size()
    promptAction.showToast({ message: '当前栈大小: ' + size })
  })

场景三:埋点统计

typescript 复制代码
.onPageShow(() => {
  let depth: number = this.navPathStack.size()
  HiLog.info(LABEL, '页面深度: %{public}d', depth)
})

API 18:.get() ------ 获取栈中元素

基本语法:

typescript 复制代码
get(index: number): NavPathInfo

参数说明: index 从 0 开始,0 表示栈底(第一个页面),size()-1 表示栈顶(当前页面)。

NavPathInfo 结构:

typescript 复制代码
interface NavPathInfo {
  name: string      // 路由名称
  param: Object     // 路由参数
  index: number     // 在栈中的位置
}

使用示例:

typescript 复制代码
// 获取当前页面信息
let topIndex: number = this.navPathStack.size() - 1
let currentPage: NavPathInfo = this.navPathStack.get(topIndex)
console.log('当前页面:', currentPage.name)

// 获取历史记录
for (let i: number = 0; i < this.navPathStack.size(); i++) {
  let page: NavPathInfo = this.navPathStack.get(i)
  console.log('历史页面 ' + i + ':', page.name)
}

实用场景:恢复页面状态

typescript 复制代码
onPageShow() {
  let topIndex: number = this.navPathStack.size() - 1
  let currentPage: NavPathInfo = this.navPathStack.get(topIndex)
  
  // 从参数恢复状态
  if (currentPage.param) {
    let param = currentPage.param as DetailParam
    this.restoreState(param)
  }
}

第四部分:NavDestination 系列(6 个)

API 19:NavDestination() ------ 导航目标组件

基本语法:

typescript 复制代码
NavDestination() {
  // 页面内容
}

核心作用: NavDestination 是 Navigation 内部单个页面的容器,每个通过 pushPathByName 推入的页面都是一个 NavDestination。

使用示例:

typescript 复制代码
.navDestination((name: string, param: Object) => {
  NavDestination() {
    Column() {
      Text('页面内容')
    }
  }
})

与自定义组件结合:

typescript 复制代码
@Component
struct HomePage {
  build() {
    Column() {
      Text('首页')
    }
  }
}

// 在 navDestination 中使用
.navDestination((name: string, param: Object) => {
  NavDestination() {
    HomePage()
  }
})

NavDestination vs 普通组件:

特性 NavDestination 普通组件
生命周期 有(onReady/onShow/onHide) 有(aboutToAppear/aboutToDisappear)
导航控制 可调用 pop() 返回 不可
标题设置 可覆盖 Navigation 标题 不可
参数接收 从 param 获取 从构造函数获取

API 20:NavDestination.title() ------ 覆盖导航标题

基本语法:

typescript 复制代码
NavDestination() {
  // ...
}
.title(title: string | Resource | CustomBuilder)

功能说明: NavDestination 的 title 会覆盖 Navigation 的全局标题,实现每个页面独立的标题配置。

使用示例:

typescript 复制代码
.navDestination((name: string, param: Object) => {
  if (name === 'home') {
    NavDestination() {
      HomePage()
    }
    .title('首页')
  } else if (name === 'detail') {
    NavDestination() {
      DetailPage()
    }
    .title('商品详情')  // 覆盖为页面专属标题
  }
})

动态标题:

typescript 复制代码
.navDestination((name: string, param: Object) => {
  let dynamicTitle: string = this.getTitleByName(name)
  
  NavDestination() {
    // 页面内容
  }
  .title(dynamicTitle)
})

自定义标题栏(NavDestination 专属):

typescript 复制代码
@Builder
DetailTitle() {
  Row() {
    Text('← 返回')
      .fontSize(16)
      .fontColor('#007DFF')
      .onClick(() => {
        this.navPathStack.pop()
      })
    Text('商品详情')
      .fontSize(18)
      .fontWeight(FontWeight.Bold)
      .layoutWeight(1)
    Text('分享')
      .fontSize(16)
      .fontColor('#666666')
  }
  .width('100%')
  .height(56)
  .padding({ left: 16, right: 16 })
  .backgroundColor(Color.White)
}

.navDestination((name: string, param: Object) => {
  NavDestination() {
    DetailPage()
  }
  .title(this.DetailTitle)
})

API 21:.onReady() ------ 页面初始化回调

基本语法:

typescript 复制代码
.onReady(callback: () => void)

触发时机: NavDestination 创建完成时触发,仅执行一次。

使用场景:

  • 页面数据首次加载
  • 初始化状态
  • 注册监听器

使用示例:

typescript 复制代码
.navDestination((name: string, param: Object) => {
  NavDestination() {
    ProductDetailPage()
  }
  .onReady(() => {
    console.log('详情页已准备好')
    this.loadProductData(param)
  })
})

onReady vs aboutToAppear:

复制代码
onReady:           NavDestination 生命周期,每次新页面创建时执行
aboutToAppear:     组件生命周期,每次组件实例创建时执行

区别:
- NavDestination 可能复用组件实例(性能优化)
- aboutToAppear 一定会执行,onReady 可能不会

建议:数据加载放 aboutToAppear,导航相关放 onReady

API 22:.onShow() ------ 页面显示回调

基本语法:

typescript 复制代码
.onShow(callback: () => void)

触发时机: NavDestination 每次显示时触发(包括首次显示和从后台恢复)。

使用场景:

  • 刷新页面数据
  • 恢复动画/播放
  • 更新 UI 状态

使用示例:

typescript 复制代码
.navDestination((name: string, param: Object) => {
  NavDestination() {
    CartPage()
  }
  .onShow(() => {
    // 每次显示购物车时刷新数据
    this.refreshCartData()
  })
})

onShow vs onReady:

复制代码
onReady:   仅首次创建时触发(一次)
onShow:    每次显示时触发(多次)

典型组合:
onReady:   初始化、注册监听
onShow:    刷新数据、恢复状态
onHide:    暂停动画、保存状态

API 23:.onHide() ------ 页面隐藏回调

基本语法:

typescript 复制代码
.onHide(callback: () => void)

触发时机: NavDestination 被其他页面覆盖时触发。

使用场景:

  • 暂停视频/音频播放
  • 保存草稿/临时数据
  • 取消订阅/定时器

使用示例:

typescript 复制代码
.navDestination((name: string, param: Object) => {
  NavDestination() {
    VideoPlayerPage()
  }
  .onShow(() => {
    this.videoController.play()
  })
  .onHide(() => {
    // 页面隐藏时暂停播放,节省资源
    this.videoController.pause()
  })
})

三生命周期完整示例:

typescript 复制代码
.navDestination((name: string, param: Object) => {
  NavDestination() {
    EditorPage()
  }
  .onReady(() => {
    // 准备编辑器
    this.editor.init()
  })
  .onShow(() => {
    // 恢复编辑状态
    this.editor.resume()
  })
  .onHide(() => {
    // 保存草稿并暂停
    this.editor.saveDraft()
    this.editor.pause()
  })
})

API 24:NavDestination.pop() ------ 从目标页返回

基本语法:

typescript 复制代码
NavDestination() {
  // ...
}
.pop()

功能说明: NavDestination 内置的返回方法,效果等同于 navPathStack.pop()

使用示例:

typescript 复制代码
.navDestination((name: string, param: Object) => {
  NavDestination() {
    Column() {
      Button('返回')
        .onClick(() => {
          // 方式一:调用 NavDestination.pop()
          // 方式二:调用 navPathStack.pop()
          this.navPathStack.pop()
        })
    }
  }
})

两种返回方式对比:

typescript 复制代码
// 方式一:navPathStack.pop()(推荐)
this.navPathStack.pop()

// 方式二:NavDestination 组件的 .pop() 方法
// 需要在 NavDestination 内部调用,灵活性稍差

第三章:完整实战项目

3.1 项目架构设计

复制代码
EntryAbility/
└── pages/
    └── Index.ets              # 主入口(SideBar + Navigation)
        │
        ├── SideBar            # 侧边栏(@Builder)
        │   ├── 用户信息区
        │   ├── 导航菜单列表
        │   └── 底部入口
        │
        └── Navigation         # 主内容区
            ├── HomePage       # 首页
            ├── CategoryPage  # 分类页
            ├── CartPage      # 购物车页
            └── ProfilePage    # 个人中心

3.2 路由名称管理

typescript 复制代码
// 使用类管理路由名称,避免硬编码
class RouteName {
  static readonly HOME: string = 'home'
  static readonly CATEGORY: string = 'category'
  static readonly CART: string = 'cart'
  static readonly PROFILE: string = 'profile'
  static readonly DETAIL: string = 'detail'
}

3.3 参数传递类

typescript 复制代码
// 详情页参数
class DetailParam {
  productId: number
  productName: string
  fromPage: string
  
  constructor(id: number, name: string, from: string) {
    this.productId = id
    this.productName = name
    this.fromPage = from
  }
}

3.4 完整主页面实现

typescript 复制代码
import { promptAction } from '@kit.ArkUI'

class RouteName {
  static readonly HOME: string = 'home'
  static readonly CATEGORY: string = 'category'
  static readonly CART: string = 'cart'
  static readonly PROFILE: string = 'profile'
  static readonly DETAIL: string = 'detail'
}

class MenuItem {
  icon: string
  title: Resource
  route: string
  
  constructor(icon: string, title: Resource, route: string) {
    this.icon = icon
    this.title = title
    this.route = route
  }
}

@Entry
@Component
struct Index {
  @State navPathStack: NavPathStack = new NavPathStack()
  @State showSideBar: boolean = false
  @State currentRoute: string = RouteName.HOME
  
  private menuItems: MenuItem[] = [
    new MenuItem('🏠', $r('app.string.home'), RouteName.HOME),
    new MenuItem('📂', $r('app.string.category'), RouteName.CATEGORY),
    new MenuItem('🛒', $r('app.string.cart'), RouteName.CART),
    new MenuItem('👤', $r('app.string.profile'), RouteName.PROFILE)
  ]

  aboutToAppear() {
    this.navPathStack.pushPathByName(RouteName.HOME, null, true)
  }

  @Builder
  SideBarBuilder() {
    Column() {
      // 用户信息区
      Column() {
        Text('🛍️')
          .fontSize(40)
          .margin({ bottom: 12 })
        Text($r('app.string.app_name'))
          .fontSize(18)
          .fontWeight(FontWeight.Bold)
          .fontColor(Color.White)
      }
      .width('100%')
      .height(180)
      .justifyContent(FlexAlign.Center)
      .backgroundColor('#667EEA')

      // 菜单列表
      Column() {
        ForEach(this.menuItems, (item: MenuItem) => {
          Row() {
            Text(item.icon)
              .fontSize(24)
              .margin({ right: 12 })
            Text(item.title)
              .fontSize(15)
              .fontColor(this.currentRoute === item.route ? '#667EEA' : '#333333')
              .fontWeight(this.currentRoute === item.route ? FontWeight.Bold : FontWeight.Normal)
          }
          .width('100%')
          .padding({ left: 20, right: 20, top: 16, bottom: 16 })
          .borderRadius(8)
          .backgroundColor(this.currentRoute === item.route ? '#667EEA20' : Color.Transparent)
          .onClick(() => {
            if (this.currentRoute !== item.route) {
              this.navPathStack.pushPathByName(item.route, null, false)
              this.currentRoute = item.route
            }
            this.showSideBar = false
          })
        }, (item: MenuItem) => item.route)
      }
      .width('100%')
      .layoutWeight(1)
      .padding({ top: 8 })

      // 底部入口
      Column() {
        Row() {
          Text('⚙️').fontSize(20).margin({ right: 10 })
          Text('设置').fontSize(14).fontColor('#666666')
        }
        .width('100%')
        .padding({ left: 20, top: 16, bottom: 16 })

        Text('Version 1.0.0')
          .fontSize(12)
          .fontColor('#999999')
          .width('100%')
          .textAlign(TextAlign.Center)
          .margin({ bottom: 20 })
      }
      .width('100%')
      .border({ width: { top: 1 }, color: '#EEEEEE' })
    }
    .width('100%')
    .height('100%')
    .backgroundColor(Color.White)
  }

  @Builder
  PageRouter(name: string, param: Object) {
    NavDestination() {
      Column() {
        if (name === RouteName.HOME) {
          this.HomePageBuilder()
        } else if (name === RouteName.CATEGORY) {
          this.CategoryPageBuilder()
        } else if (name === RouteName.CART) {
          this.CartPageBuilder()
        } else if (name === RouteName.PROFILE) {
          this.ProfilePageBuilder()
        }
      }
      .width('100%')
      .height('100%')
    }
  }

  @Builder
  HomePageBuilder() {
    Scroll() {
      Column() {
        Column() {
          Text('欢迎回来')
            .fontSize(24)
            .fontWeight(FontWeight.Bold)
            .fontColor(Color.White)
            .margin({ bottom: 8 })
          Text('发现精彩内容')
            .fontSize(16)
            .fontColor(Color.White)
            .opacity(0.9)
        }
        .width('100%')
        .height(140)
        .justifyContent(FlexAlign.Center)
        .backgroundColor('#4FACFE')
        .borderRadius(16)
        .margin({ bottom: 24 })

        Text('快捷功能')
          .fontSize(18)
          .fontWeight(FontWeight.Bold)
          .width('100%')
          .margin({ bottom: 16 })

        Grid() {
          GridItem() {
            Column() {
              Text('🛒').fontSize(32)
              Text('立即选购').fontSize(14).margin({ top: 8 })
            }
            .width('100%')
            .aspectRatio(1)
            .justifyContent(FlexAlign.Center)
            .backgroundColor('#FF6B6B20')
            .borderRadius(12)
          }
          GridItem() {
            Column() {
              Text('🔥').fontSize(32)
              Text('热销推荐').fontSize(14).margin({ top: 8 })
            }
            .width('100%')
            .aspectRatio(1)
            .justifyContent(FlexAlign.Center)
            .backgroundColor('#FFA50220')
            .borderRadius(12)
          }
        }
        .columnsTemplate('1fr 1fr')
        .rowsGap(12)
        .columnsGap(12)
        .width('100%')
      }
      .width('100%')
      .padding(16)
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#F5F5F5')
  }

  @Builder
  CategoryPageBuilder() {
    Column() {
      Text('📂')
        .fontSize(64)
        .margin({ bottom: 16 })
      Text('分类页面')
        .fontSize(24)
        .fontWeight(FontWeight.Bold)
      Text('功能开发中...')
        .fontSize(14)
        .fontColor('#999999')
        .margin({ top: 8 })
    }
    .width('100%')
    .height('100%')
    .justifyContent(FlexAlign.Center)
    .alignItems(HorizontalAlign.Center)
    .backgroundColor('#F5F5F5')
  }

  @Builder
  CartPageBuilder() {
    Column() {
      Text('🛒')
        .fontSize(64)
        .margin({ bottom: 16 })
      Text('购物车')
        .fontSize(24)
        .fontWeight(FontWeight.Bold)
      Text('功能开发中...')
        .fontSize(14)
        .fontColor('#999999')
        .margin({ top: 8 })
    }
    .width('100%')
    .height('100%')
    .justifyContent(FlexAlign.Center)
    .alignItems(HorizontalAlign.Center)
    .backgroundColor('#F5F5F5')
  }

  @Builder
  ProfilePageBuilder() {
    Column() {
      Text('👤')
        .fontSize(64)
        .margin({ bottom: 16 })
      Text('个人中心')
        .fontSize(24)
        .fontWeight(FontWeight.Bold)
      Text('功能开发中...')
        .fontSize(14)
        .fontColor('#999999')
        .margin({ top: 8 })
    }
    .width('100%')
    .height('100%')
    .justifyContent(FlexAlign.Center)
    .alignItems(HorizontalAlign.Center)
    .backgroundColor('#F5F5F5')
  }

  build() {
    SideBarContainer() {
      this.SideBarBuilder()
      Navigation(this.navPathStack)
        .title($r('app.string.app_name'))
        .navDestination((name: string, param: Object) => {
          this.PageRouter(name, param)
        })
    }
    .sideBarPosition(SideBarPosition.Start)
    .sideBarWidth(280)
    .showSideBar(this.showSideBar)
    .width('100%')
    .height('100%')
  }
}

3.5 避坑指南

坑一:ArkTS 不支持匿名对象类型

错误代码:

typescript 复制代码
// ❌ 不可用
@State menuItems: { icon: string, title: string }[] = [...]

正确写法:

typescript 复制代码
// ✅ 定义显式类
class MenuItem {
  icon: string
  title: string
  constructor(icon: string, title: string) {
    this.icon = icon
    this.title = title
  }
}

@State menuItems: MenuItem[] = [
  new MenuItem('🏠', '首页'),
  new MenuItem('📂', '分类')
]
坑二:条件语句位置错误

错误代码:

typescript 复制代码
// ❌ 不可用:if 在 UI 组件内部
Column() {
  if (condition) {
    Text('是')
  }
}
.onClick(() => {})

正确写法:

typescript 复制代码
// ✅ 可用:if 在 @Builder 中
@Builder
ContentBuilder() {
  if (condition) {
    Text('是')
  } else {
    Text('否')
  }
}

Column() {
  this.ContentBuilder()
}
.onClick(() => {})
坑三:状态更新不生效

错误代码:

typescript 复制代码
// ❌ 错误:数组引用没变
@State items: string[] = ['a', 'b']

UpdateItem() {
  this.items[0] = 'c'  // 修改元素,但数组引用不变
}

正确写法:

typescript 复制代码
// ✅ 正确:创建新数组引用
@State items: string[] = ['a', 'b']

UpdateItem() {
  this.items = ['c', 'b']  // 新数组触发更新
  // 或
  this.items = [...this.items]  // 展开创建新引用
}

第四章:性能优化与最佳实践

4.1 减少不必要的组件重建

typescript 复制代码
// ❌ 避免在 build() 中创建新对象
build() {
  let data: string[] = this.processData()  // 每次重建
  Column() {
    ForEach(data, (item: string) => {...})
  }
}

// ✅ 使用 @Computed 或缓存
@Computed
get processedData(): string[] {
  return this.processData()
}

build() {
  Column() {
    ForEach(this.processedData, (item: string) => {...})
  }
}

4.2 使用 renderGroup 减少绘制

typescript 复制代码
// 对于复杂的子组件树
Column() {
  // 复杂内容...
}
.renderGroup(true)  // 合并绘制,减少 GPU 负担

4.3 合理使用条件渲染

typescript 复制代码
// 避免同时渲染多个页面
Column() {
  if (currentTab === 'home') {
    HomePage()  // 只渲染当前页
  } else if (currentTab === 'category') {
    CategoryPage()
  }
}

// 而不是
Column() {
  HomePage().visibility(currentTab === 'home' ? Visibility.Visible : Visibility.Hidden)
  CategoryPage().visibility(currentTab === 'category' ? Visibility.Visible : Visibility.Hidden)
  // Hidden 只是隐藏,组件仍在渲染
}

4.4 避免在动画中修改布局属性

typescript 复制代码
// ❌ 不要动画修改 width/height/padding/margin
animateTo({ duration: 300 }, () => {
  this.width = this.expanded ? '300' : '100'  // 会触发重新布局
})

// ✅ 使用 opacity/scale/translate
animateTo({ duration: 300 }, () => {
  this.opacity = this.show ? 1 : 0  // 只改变透明度,不重新布局
})

第五章:常见问题 FAQ

Q1: 侧边栏点击空白区域不关闭怎么办?

解决方案:

typescript 复制代码
SideBarContainer() {
  this.SideBarBuilder()
  Column() {
    // 主内容
  }
  .onClick(() => {
    // 点击主内容区关闭侧边栏
    if (this.showSideBar) {
      this.showSideBar = false
    }
  })
}
.showSideBar(this.showSideBar)

Q2: 如何实现页面切换动画?

方案一:使用 animateTo

typescript 复制代码
SwitchPage(route: string) {
  animateTo({ duration: 200 }, () => {
    this.navPathStack.pushPathByName(route, null, false)
  })
}

方案二:NavDestination 自定义过渡

typescript 复制代码
NavDestination() {
  PageContent()
}
.transition(TransitionEffect.OPACITY.animation({ duration: 200 }))

Q3: 侧边栏能否支持手势滑动?

可以,使用 PanGesture:

typescript 复制代码
@State translateX: number = 0
@State isShow: boolean = false

SideBarContainer() {
  Column() {
    // 侧边栏
  }
  .translate({ x: this.isShow ? 0 : -280 + this.translateX })
  
  Column() {
    // 主内容
  }
  .gesture(
    PanGesture()
      .onActionUpdate((event: GestureEvent) => {
        this.translateX = event.offsetX
      })
      .onActionEnd(() => {
        if (this.translateX > 100) {
          this.isShow = true
        }
        this.translateX = 0
      })
  )
}

使用 Preferences:

typescript 复制代码
// 保存状态
SaveState() {
  let topPage: NavPathInfo = this.navPathStack.get(this.navPathStack.size() - 1)
  preferences.put('lastPage', topPage.name)
  preferences.put('stackSize', this.navPathStack.size())
  preferences.flush()
}

// 恢复状态
RestoreState() {
  let lastPage: string = preferences.get('lastPage', 'home')
  this.navPathStack.pushPathByName(lastPage, null, true)
}

结语:持续探索,不断进步

HarmonyOS NEXT 的 SideBar + Navigation 布局方案为我们提供了强大的导航管理能力。通过本文对 24 个核心 API 的系统讲解,以及完整的实战代码和避坑指南,相信你已经能够熟练地在项目中应用这一布局模式。

技术之路永无止境,建议你:

  1. 动手实践:将示例代码运行起来,观察实际效果
  2. 深入源码:研究 HarmonyOS 的实现原理
  3. 关注更新:HarmonyOS 持续迭代,API 也在进化
  4. 分享交流:加入开发者社区,与同行讨论

期待看到你基于这一方案打造出精彩的 HarmonyOS 应用!


相关推荐
xd1855785558 小时前
家电选购参谋 —— 鸿蒙AI智能助手开发全流程解析
人工智能·华为·harmonyos·鸿蒙
FrameNotWork8 小时前
HarmonyOS 6.0 相机开发——拍照与录像
数码相机·华为·harmonyos
FrameNotWork9 小时前
HarmonyOS 6.0 自定义TabBar——从基础到动效
华为·harmonyos
世人万千丶9 小时前
鸿蒙Flutter Flex布局性能优化
学习·flutter·性能优化·harmonyos·鸿蒙·鸿蒙系统
●VON9 小时前
鸿蒙 PC Markdown 编辑器质量流水线:Web 构建、回归与 Release 门禁
前端·华为·编辑器·harmonyos·鸿蒙
ZZZMMM.zip9 小时前
断舍离清单 —— 鸿蒙AI智能助手开发全流程解析
人工智能·华为·harmonyos·鸿蒙·鸿蒙系统
listening7779 小时前
HarmonyOS 6.1 跨设备数据库实战:分布式账本的落地与一致性校验
数据库·harmonyos·分布式账本
●VON9 小时前
鸿蒙 PC Markdown 编辑器文件系统:Core File Kit 与安全保存
安全·华为·编辑器·harmonyos·鸿蒙
YM52e10 小时前
鸿蒙 Flutter 渐变效果详解:LinearGradient、RadialGradient、SweepGradient
android·学习·flutter·华为·harmonyos·鸿蒙