欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net

各位架构师们,我是Moranbika。经过第一阶段从零到一的“筑基”,我们的应用已经拥有了健壮的数据处理和交互能力。现在,是时候从“单一功能”迈向“完整应用”了。在DAY8-DAY13的第二阶段,我们的核心任务是为应用构建坚实的导航骨架——实现底部选项卡(TabBar),并围绕它开发多个功能独立的页面,从而将一个“页面”扩展为一个真正的“产品”。

这不仅是添加几个按钮,更是对应用架构、状态管理、用户体验的一次系统性升级。我将其称为“骨架工程”,因为它直接决定了应用的健壮性和扩展性。在这个过程中,你将直面页面状态丢失、导航跳转混乱、多端UI适配这三大经典挑战。

第一部分:架构设计先行——技术选型与设计规范

在敲下第一行代码前,我们必须做出两个关键决策。

决策一:导航方案选择

  1. 原生导航器(@ohos.router:鸿蒙提供的官方页面路由方案。优势是性能最佳,与系统集成度最高,生命周期管理严谨。劣势是TabBar需要完全自定义,导航栈需要手动管理。

  2. 三方导航库(如React Navigation的鸿蒙适配版):在React Native技术栈中常见。优势是开发模式熟悉,提供现成的TabBar组件和丰富的转场动画。劣势是兼容性风险,包体积增加,且可能无法充分利用鸿蒙的原生特性。

本阶段,我们选择挑战更大的“原生方案”。原因有三:第一,理解原生机制是掌握鸿蒙开发的根本;第二,自定义程度高,能完美契合设计需求;第三,规避三方库的兼容风险。理解此方案后,你将有足够能力去评估和驾驭任何三方库。

决策二:信息架构设计
我们设计一个经典的四Tab应用,每个Tab代表一个独立的功能模块:

  • 首页 (Home):应用入口,展示核心信息流或快速操作。

  • 列表页 (List):承载我们第一阶段开发的动态列表,是核心内容展示区。

  • 我的 (Profile):用户个人中心,涉及登录、设置等。

  • 设置/更多 (Settings):应用配置、关于等静态页面。

第二部分:核心实现——自定义底部选项卡组件

我们的目标是实现一个功能完整、视觉专业的 CustomTabBar 组件。

1. 组件结构与数据驱动
首先,定义导航的数据模型和组件框架。

javascript

// 1. 定义单个Tab的数据类型
interface TabItem {
  index: number; // 索引
  name: string;  // 名称,如“首页”
  icon: Resource; // 未选中图标资源
  selectedIcon: Resource; // 选中图标资源
  page: string;   // 对应的页面路径,如‘pages/Home’
}

// 2. 在构建自定义TabBar的组件中
@Component
struct CustomTabBar {
  // 接收外部传入的Tab配置数组和当前选中索引
  @Link currentIndex: number;
  @Link tabItems: TabItem[];

  build() {
    Row() {
      // 使用Flex布局均匀分布Tab
      ForEach(this.tabItems, (item: TabItem) => {
        Column() {
          // 根据选中状态显示不同图标
          Image(this.currentIndex === item.index ? item.selectedIcon : item.icon)
            .width(24)
            .height(24)
            .margin({ bottom: 4 });

          Text(item.name)
            .fontSize(12)
            // 动态切换文字颜色
            .fontColor(this.currentIndex === item.index ? '#007DFF' : '#666666');
        }
        .flexGrow(1) // 每个Tab均分宽度
        .justifyContent(FlexAlign.Center)
        .padding(10)
        .onClick(() => {
          // 点击时,通知父组件切换索引,而非直接跳转
          this.currentIndex = item.index;
        })
      }, (item: TabItem) => item.index.toString())
    }
    .width('100%')
    .height(60)
    .backgroundColor('#FFFFFF')
    .shadow({ radius: 8, color: '#1A000000', offsetX: 0, offsetY: -2 }) // 顶部投影
  }
}

2. 第一个大坑:状态同步与页面路由
上面代码中,@Link 装饰器是关键。它建立了TabBar与父页面(通常是主容器)之间的双向数据同步。点击Tab时,只改变 currentIndex。父组件监听这个变化,再执行真正的页面路由。

在承载TabBar和页面容器的主页面(MainPage.ets 中,逻辑如下:

javascript

// MainPage.ets
@Entry
@Component
struct MainPage {
  @State currentTabIndex: number = 0; // 当前选中的Tab索引
  private tabItems: TabItem[] = [...]; // 初始化Tab数据
  private controller: TabsController = new TabsController(); // Tabs组件控制器

  build() {
    Column() {
      // 1. 页面内容区域:使用鸿蒙的Tabs组件,但隐藏其自带的TabBar
      Tabs({ barPosition: BarPosition.End, controller: this.controller }) {
        // 对应每一个Tab,加载一个页面
        TabContent() {
          HomePage() // 首页组件
        }
        TabContent() {
          ListPage() // 列表页组件
        }
        // ... 其他TabContent
      }
      .vertical(false)
      .scrollable(false) // 禁用滑动切换,由我们自定义的TabBar控制
      .barWidth(0)       // 关键:隐藏Tabs自带的TabBar
      .barHeight(0)
      .onChange((index: number) => {
        // Tabs内部滑动也会触发,用于同步我们自定义TabBar的状态
        this.currentTabIndex = index;
      })
      .width('100%')
      .flexGrow(1) // 占据除底部TabBar外的所有空间

      // 2. 我们自定义的底部TabBar
      CustomTabBar({
        currentIndex: $currentTabIndex, // 双向绑定语法
        tabItems: $tabItems
      })
    }
    .height('100%')
    .backgroundColor('#F5F5F5')
  }

  aboutToAppear() {
    // 监听currentTabIndex变化,驱动Tabs切换页面
    this.watch(‘currentTabIndex’, (newVal) => {
      this.controller.changeIndex(newVal);
    })
  }
}

这种 “状态驱动” 的架构,将UI交互(点击Tab)与页面路由(切换Tabs)解耦,是构建可维护导航系统的核心。

第三部分:页面实现与“状态保持”难题攻克

每个Tab页都是一个独立的ArkUI组件。如何让用户在切换Tab时,列表页的滚动位置、首页的临时数据不丢失?

1. 页面组件结构
以 ListPage.ets 为例,它独立于之前的 Index.ets

javascript

// ListPage.ets
@Component
export struct ListPage {
  // 关键:@StorageLink持久化状态,即使页面被销毁重建,状态也能恢复
  @StorageLink(‘ListPage_scrollOffset‘) scrollOffset: number = 0;
  @State dataList: object[] = [];

  build() {
    Column() {
      // 你的列表,注意绑定scrollOffset到Scroll组件的offset属性
      Scroll(this.scroller) {
        List() {
          ForEach(this.dataList, (item) => { ... })
        }
      }
      .onScroll((xOffset: number, yOffset: number) => {
        // 实时记录滚动位置
        this.scrollOffset = yOffset;
      })
      .scrollOffset({ x: 0, y: this.scrollOffset }) // 恢复位置
    }
  }
}

2. 第二个大坑:页面生命周期与“保活”策略
鸿蒙的 Tabs 组件,默认在切换非相邻Tab时,会销毁并重建中间页面的组件实例,导致状态丢失。我们有三种策略:

  • 策略A:状态持久化(@StorageLink/AppStorage)
    如上例所示,将需要保持的状态(滚动位置、表单输入)通过 @StorageLink 存入应用级存储。这是最通用、最可靠的方案

  • 策略B:使用 if/else 条件渲染替代 Tabs
    将四个页面组件都写在主页面里,通过 if 判断 currentTabIndex 来决定显示哪一个。这样所有页面实例常驻内存,状态永不丢失。优点是简单直接。缺点是内存占用高,不适合复杂页面。

    javascript
    
    // 在主页面内
    build() {
      Column() {
        if (this.currentTabIndex === 0) {
          HomePage()
        } else if (this.currentTabIndex === 1) {
          ListPage()
        }
        // ... 自定义TabBar
      }
    }
  • 策略C:调整 Tabs 的 cachedCount 属性
    Tabs 组件有一个 cachedCount 属性,表示预加载和缓存的页面数量。设为 3 可以保证四Tab应用所有页面都被缓存。缺点是官方文档指出,过大的 cachedCount 可能影响性能。

我的最终方案组合拳。对于简单的设置页,无需保活。对于复杂的列表页和首页,采用 “策略A(状态持久化)为主,策略C(适当缓存)为辅” 的方式,在体验和性能间取得最佳平衡。

第四部分:多终端适配与第三个大坑

我们的设计在手机上完美,但在折叠屏、平板或开发板上,底部TabBar可能会显得局促或布局错乱。

1. 适配策略:尺寸感知与响应式布局
在 MainPage.ets 中,我们可以注入环境变量,进行条件布局。

javascript

import window from '@ohos.window';

@Entry
@Component
struct MainPage {
  @State currentTabIndex: number = 0;
  // 监听窗口尺寸变化
  @State windowWidth: number = 0;
  @State windowHeight: number = 0;
  private windowClass: window.Window | null = null;

  onWindowSizeChange() {
    // 获取窗口信息,判断设备类型
    this.windowClass.getProperties((properties) => {
      this.windowWidth = properties.windowRect.width;
      this.windowHeight = properties.windowRect.height;
    });
  }

  build() {
    Column() {
      // 内容区
      Tabs(...){ ... }

      // 条件渲染:在宽屏设备上,TabBar可能放在左侧
      if (this.windowWidth > this.windowHeight && this.windowWidth > 600) {
        // 横屏或平板:将TabBar改为左侧垂直布局
        Row() {
          VerticalTabBar({ currentIndex: $currentTabIndex, tabItems: $tabItems })
            .width(80)
          Tabs(...){ ... }.flexGrow(1)
        }
      } else {
        // 竖屏手机:保持原有底部布局
        CustomTabBar({ currentIndex: $currentTabIndex, tabItems: $tabItems })
      }
    }
  }
}

2. 开发板上的特定样式问题
在DAYU200等屏幕上,可能出现点击区域过小、文字看不清。解决方案是在定义 TabItem 时,为开发板环境准备一套更大的图标和字体尺寸,并通过 ohos.system.parameter 接口获取设备型号进行判断。

第五部分:可选拓展——集成三方导航库

如果你坚持使用React Native技术栈,集成 react-navigation 的鸿蒙适配版可能是更高效的选择。但请注意:

  1. 安装:需要安装特定的鸿蒙兼容版本,命令可能类似:

    bash
    
    npm install @react-navigation/core@^6.x @ohos/react-navigation-harmony
  2. 配置:需要在 metro.config.js 中为鸿蒙添加 resolver.platforms

  3. 核心挑战:你可能会遇到 “NativeModule链接失败” 的错误。这是因为导航库的某些原生模块没有为OpenHarmony编译。解决方案通常是:a) 寻找纯JS实现的替代库;b) 联系库维护者;c) 回退到原生方案。

第六部分:提交与阶段总结

完成这个功能完整的应用骨架后,进行一次里程碑式的提交:

bash

git add .
git commit -m “feat: 构建应用底部导航与多页面架构

- 基于@ohos.router与Tabs组件,实现完全自定义的底部选项卡(CustomTabBar)组件
- 采用@Link双向绑定与状态驱动设计,解耦UI交互与页面路由
- 攻克多Tab切换状态丢失难题,综合运用@StorageLink持久化、条件渲染与cachedCount缓存策略
- 实现响应式布局,为折叠屏、平板及开发板等不同设备进行UI适配
- 完成首页、列表页、个人中心、设置页四个核心页面的基础功能与布局”

git push origin main

第二阶段心法:我们构建的远不止一个导航栏。我们搭建的是一个可预测、可维护、可扩展的应用导航架构。我们深入了状态管理、组件通信、生命周期和多端适配这些核心领域。有了这个坚实的骨架,在第三阶段,我们将为其注入“动感”的灵魂,让交互变得生动流畅。我们下次见!

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐