《新闻资讯》八、产品定制层实现指南

HarmonyOS NEXT 新闻资讯应用 · 产品定制层 product/phone 实现指南

开发环境 :DevEco Studio 6.1.0 Release

SDK版本 :HarmonyOS SDK 6.1.0(23) / API 23

开发语言 :ArkTS

状态管理 :V2(@ComponentV2系列装饰器)

前置阅读common 指南 | news 指南 | video 指南 | live 指南 | personal 指南 | service 指南 | 整体架构指南

本篇详细讲解产品定制层 product/phone 的实现。作为应用的唯一 HAP 入口模块 (可直接运行的模块),product/phone 是整个应用的顶层容器------集成 HdsTabs 沉浸光感悬浮页签Navigation + NavPathStack 路由管理@Provider + @Param 显式传参 三大核心能力,将所有 feature 模块组装为完整应用。


效果


一、模块定位与架构角色

产品定制层在三层架构中处于最顶层,编译为 HAP 包(Harmony Ability Package),是唯一可独立安装运行的模块:

复制代码
产品定制层 product/phone (HAP) ← 本模块,唯一可运行
    ↓ 依赖
基础特性层 features/news | features/video | features/live | features/personal | features/service (HAR)
    ↓ 依赖
公共能力层 common (HAR)

模块职责

  1. 提供应用启动入口(Index.ets 启动页)
  2. 集成 HdsTabs 悬浮底部导航,嵌入 4 个 feature 模块的 Home 组件
  3. 提供 Navigation 路由容器,统一映射 5 个 NavDestination 子页面
  4. 通过 @Provider + pageMap 显式传参将 NavPathStack 注入子组件

与其他模块的根本区别

对比维度 product/phone (HAP) features/* (HAR) common (HAR)
模块类型 "entry" "har" "har"
独立运行 ✅ 可以 ❌ 不可以 ❌ 不可以
@Entry 装饰器 ✅ 有 ❌ 没有 ❌ 没有
依赖数量 6 个(common + 5 features) 1 个(common) 0 个

二、模块配置详解

2.1 oh-package.json5(16行)

json5 复制代码
{
  "name": "phone",
  "version": "1.0.0",
  "description": "Please describe the basic information.",
  "main": "",
  "author": "",
  "license": "",
  "dependencies": {
    "common": "file:../../common",
    "news": "file:../../features/news",
    "video": "file:../../features/video",
    "live": "file:../../features/live",
    "personal": "file:../../features/personal",
    "service": "file:../../features/service"
  }
}

逐字段讲解

字段 说明
name "phone" 包标识符
main "" HAP 模块无入口文件,入口由 module.json5 指定
dependencies 6 个本地依赖 common + 5 个 features 模块

依赖路径 :所有路径从 product/phone 向上两级到项目根目录,再进入对应模块:

  • "file:../../common"common/
  • "file:../../features/news"features/news/
  • 以此类推

2.2 module.json5(50行)

json5 复制代码
{
  "module": {
    "name": "phone",
    "type": "entry",
    "description": "$string:module_desc",
    "mainElement": "EntryAbility",
    "deviceTypes": ["phone"],
    "deliveryWithInstall": true,
    "installationFree": false,
    "pages": "$profile:main_pages",
    "abilities": [
      {
        "name": "EntryAbility",
        "srcEntry": "./ets/entryability/EntryAbility.ets",
        "description": "$string:EntryAbility_desc",
        "icon": "$media:layered_image",
        "label": "$string:EntryAbility_label",
        "startWindowIcon": "$media:startIcon",
        "startWindowBackground": "$color:start_window_background",
        "exported": true,
        "skills": [
          {
            "entities": ["entity.system.home"],
            "actions": ["ohos.want.action.home"]
          }
        ]
      }
    ],
    "extensionAbilities": [
      {
        "name": "EntryBackupAbility",
        "srcEntry": "./ets/entrybackupability/EntryBackupAbility.ets",
        "type": "backup",
        "exported": false,
        "metadata": [
          {
            "name": "ohos.extension.backup",
            "resource": "$profile:backup_config"
          }
        ]
      }
    ]
  }
}

关键字段详解

字段 说明
type "entry" HAP 入口模块(对比 HAR 的 "har"
mainElement "EntryAbility" 入口 Ability 名称
deviceTypes ["phone"] 仅支持手机设备
pages "$profile:main_pages" 引用 main_pages.json 路由配置
deliveryWithInstall true 随应用一起安装
installationFree false 不可免安装

abilities 配置

字段 说明
name: "EntryAbility" Ability 名称
srcEntry Ability 入口文件路径
exported: true 可被系统调用(必须为 true,否则无法启动)
skills.entities "entity.system.home" --- 声明为系统首页
skills.actions "ohos.want.action.home" --- 响应系统首页启动意图

extensionAbilitiesEntryBackupAbility 是系统自动生成的备份能力,type 为 "backup",用于应用数据备份恢复。

2.3 main_pages.json(7行)

json 复制代码
{
  "src": [
    "pages/Index",
    "pages/MainPage"
  ]
}

注册 2 个路由页面:

  1. pages/Index --- 启动页(Splash),应用入口
  2. pages/MainPage --- 应用主页(HdsTabs + Navigation)

注意:NavDestination 子页面(NewsDetail、VideoPlayer 等)不需要 在此注册,它们由 Navigation 的 navDestination 回调动态渲染。

2.4 HAP vs HAR 配置差异对比

配置项 HAP (product/phone) HAR (features/*)
type "entry" "har"
main "" (空) "Index.ets"
mainElement ✅ 有 ❌ 无
deviceTypes ["phone"] ["default"]
abilities ✅ 有 ❌ 无
pages ✅ 有 ❌ 无
依赖数量 6 1

三、完整文件结构树

复制代码
product/phone/
├── oh-package.json5                         (16行) 模块配置,6个依赖
├── src/main/
│   ├── module.json5                         (50行) 模块元数据
│   ├── ets/
│   │   ├── entryability/
│   │   │   └── EntryAbility.ets             (系统生成,Ability入口)
│   │   ├── entrybackupability/
│   │   │   └── EntryBackupAbility.ets       (系统生成,备份能力)
│   │   ├── pages/
│   │   │   ├── Index.ets                    (49行) 启动页 @Entry
│   │   │   └── MainPage.ets                 (82行) 应用主页 @Entry
│   │   ├── constants/
│   │   │   └── PageConstants.ets            (29行) 路由常量
│   │   └── viewmodel/
│   │       └── MainPageData.ets             (30行) Tab按钮数据
│   └── resources/
│       └── base/profile/
│           ├── main_pages.json              (7行) 路由注册
│           └── backup_config.json           (备份配置)

四、Index.ets 启动页

启动页是应用打开后第一个展示的页面,显示品牌 Logo 和 Slogan,1.5 秒后自动跳转到主页。

4.1 完整源码

typescript 复制代码
import { StyleConstants } from 'common';
import { PageConstants } from '../constants/PageConstants';

/**
 * 启动页(Splash)
 * 应用启动后展示品牌页面,延迟后自动跳转到主页
 * 使用 @ComponentV2 + @Local 管理倒计时状态
 */
@Entry
@ComponentV2
struct Index {
  @Local countdown: number = 3;

  aboutToAppear() {
    // 延迟后跳转到主页
    setTimeout(() => {
      this.getUIContext().getRouter().replaceUrl({
        url: 'pages/MainPage'
      });
    }, PageConstants.SPLASH_DELAY);
  }

  build() {
    Stack() {
      // 背景图
      Image($r('app.media.splash_bg'))
        .width('100%')
        .height('100%')
        .objectFit(ImageFit.Cover)

      // 应用名称
      Column() {
        Text('华为日报')
          .fontSize(32)
          .fontWeight(FontWeight.Bold)
          .fontColor(Color.White)

        Text('每日精选,为你呈现')
          .fontSize(14)
          .fontColor('rgba(255, 255, 255, 0.8)')
          .margin({ top: 8 })
      }
      .alignItems(HorizontalAlign.Center)
    }
    .width('100%')
    .height('100%')
  }
}

4.2 分段深度讲解

@Entry + @ComponentV2 双装饰器
typescript 复制代码
@Entry
@ComponentV2
struct Index {
  • @Entry:标记此组件为页面入口,可以独立运行。只有被 @Entry 标记的组件才能在 main_pages.json 中注册和路由
  • @ComponentV2:V2 组件声明,支持 @Local 等新装饰器

注意struct 而非 export struct------Index 是 @Entry 页面,由系统直接加载,不需要 export。

aboutToAppear 自动跳转
typescript 复制代码
aboutToAppear() {
  setTimeout(() => {
    this.getUIContext().getRouter().replaceUrl({
      url: 'pages/MainPage'
    });
  }, PageConstants.SPLASH_DELAY);  // 1500ms
}

跳转链路

  1. setTimeout --- 延迟 1500ms
  2. this.getUIContext() --- V2 获取 UI 上下文(V1 用 getContext(this)
  3. .getRouter() --- 获取路由器实例
  4. .replaceUrl({ url: 'pages/MainPage' }) --- 替换当前页面为 MainPage

replaceUrl vs pushUrl

方法 路由栈效果 适用场景
replaceUrl 替换当前页,栈深度不变 启动页→主页(不可返回启动页)
pushUrl 推入新页,栈深度+1 页面间跳转(可返回上一页)

启动页使用 replaceUrl,用户无法通过返回键回到启动页。

build() 品牌展示
typescript 复制代码
Stack() {
  Image($r('app.media.splash_bg'))    // 全屏背景图
    .objectFit(ImageFit.Cover)

  Column() {                           // 居中文字
    Text('华为日报')                     // 品牌名,32号白色加粗
    Text('每日精选,为你呈现')            // Slogan,14号半透明白色
  }
  .alignItems(HorizontalAlign.Center)
}

使用 Stack 叠加全屏背景图和居中文字。ImageFit.Cover 确保背景图等比缩放裁切填满屏幕。


五、MainPage.ets 核心主页

MainPage 是整个应用的核心枢纽------集成 HdsTabs 悬浮底部导航、Navigation 路由容器和 @Provider 状态注入。

5.1 完整源码

typescript 复制代码
import { HdsTabs } from '@kit.UIDesignKit';
import { SymbolGlyphModifier } from '@kit.ArkUI';
import { NewsHome, NewsDetail, NewsCategory } from 'news';
import { VideoHome, VideoPlayer } from 'video';
import { LiveHome } from 'live';
import { PersonalHome, LoginPage, MyComments } from 'personal';
import { ServiceHome } from 'service';

/**
 * 应用主页
 * 核心页面,集成以下关键能力:
 * 1. HdsTabs 沉浸光感悬浮页签
 * 2. Navigation + NavPathStack - 页面路由管理
 * 3. @Provider('pageStack') - 跨组件状态共享(V2)
 *
 * 架构说明:
 * - HdsTabs + BottomTabBarStyle(全局类)实现悬浮底部导航
 * - 每个 TabContent 对应一个 feature 模块的 Home 组件
 * - navDestination 统一映射所有子页面路由
 * - @Provider 将 pageStack 注入组件树,pageMap 中显式传参给各 NavDestination 组件
 */
@Entry
@ComponentV2
struct MainPage {
  @Local currentTabIndex: number = 0;
  @Provider('pageStack') pageStack: NavPathStack = new NavPathStack();

  /**
   * 构建页签图标
   * @param symbol 系统 SymbolGlyph 资源
   * @param selected 是否选中状态
   */
  private buildTabIcon(symbol: Resource, selected: boolean): SymbolGlyphModifier {
    return new SymbolGlyphModifier(symbol)
      .fontColor([selected ? $r('app.color.focus_color') : $r('app.color.placeholder_color')]);
  }

  /**
   * 构建页签样式(图标 + 文本 + 文本颜色)
   * @param symbol 系统 SymbolGlyph 资源
   * @param label 页签文字
   */
  private buildTabBar(symbol: Resource, label: string): BottomTabBarStyle {
    return new BottomTabBarStyle({
      normal: this.buildTabIcon(symbol, false),
      selected: this.buildTabIcon(symbol, true)
    }, label).labelStyle({
      unselectedColor: $r('app.color.placeholder_color'),
      selectedColor: $r('app.color.focus_color')
    });
  }

  build() {
    Navigation(this.pageStack) {
      Column() {
        HdsTabs({ index: this.currentTabIndex }) {
          TabContent() {
            NewsHome()
          }
          .tabBar(this.buildTabBar($r('sys.symbol.house'), '首页'))

          TabContent() {
            VideoHome()
          }
          .tabBar(this.buildTabBar($r('sys.symbol.video'), '视频'))

          TabContent() {
            LiveHome()
          }
          .tabBar(this.buildTabBar($r('sys.symbol.video_badge_adiowaves'), '直播'))

          TabContent() {
            PersonalHome()
          }
          .tabBar(this.buildTabBar($r('sys.symbol.person'), '我的'))
        }
        .barOverlap(true)
        .barPosition(BarPosition.End)
        .vertical(false)
        .barFloatingStyle({
          barBottomMargin: 16
        })
        .onChange((index: number) => {
          this.currentTabIndex = index;
        })
        .layoutWeight(1)
      }
      .width('100%')
      .height('100%')
    }
    .hideTitleBar(true)
    .navDestination(this.pageMap)
    .mode(NavigationMode.Stack)
  }

  /**
   * 路由映射表
   * 根据路由名称渲染对应的 NavDestination 页面
   * 每个组件通过 { pageStack: this.pageStack } 显式传参
   */
  @Builder
  pageMap(name: string, param: Object) {
    if (name === 'NewsDetail') {
      NewsDetail({ pageStack: this.pageStack })
    } else if (name === 'NewsCategory') {
      NewsCategory({ pageStack: this.pageStack })
    } else if (name === 'VideoPlayer') {
      VideoPlayer({ pageStack: this.pageStack })
    } else if (name === 'LoginPage') {
      LoginPage({ pageStack: this.pageStack })
    } else if (name === 'MyComments') {
      MyComments({ pageStack: this.pageStack })
    }
  }
}

5.2 分段深度讲解

段落1 --- 导入语句(第1-7行)
typescript 复制代码
import { HdsTabs } from '@kit.UIDesignKit';                   // HdsTabs 悬浮页签组件
import { SymbolGlyphModifier } from '@kit.ArkUI';              // 系统 SymbolGlyph 图标渲染器
import { NewsHome, NewsDetail, NewsCategory } from 'news';  // 新闻模块3个
import { VideoHome, VideoPlayer } from 'video';       // 视频模块2个
import { LiveHome } from 'live';                       // 直播模块1个
import { PersonalHome, LoginPage, MyComments } from 'personal';  // 个人中心3个
import { ServiceHome } from 'service';                  // 服务模块1个

导入说明

  • HdsTabs@kit.UIDesignKit 导入
  • SymbolGlyphModifier@kit.ArkUI 导入,用于渲染系统 SymbolGlyph 图标
  • BottomTabBarStyle全局类,不需要导入(这是常见误区)
  • 4 个 Home 组件:嵌入 4 个 TabContent
  • 5 个 NavDestination 组件:注册在 pageMap 路由表
  • ServiceHome 被导入但未在当前页面使用(预留给后续扩展)
段落2 --- @Provider 状态注入(第25-26行)
typescript 复制代码
@Local currentTabIndex: number = 0;
@Provider('pageStack') pageStack: NavPathStack = new NavPathStack();
装饰器 变量 说明
@Local currentTabIndex 当前选中 Tab 索引
@Provider('pageStack') pageStack 将 NavPathStack 注入组件树,key 为 'pageStack'

@Provider('pageStack') 的核心作用

  1. 创建一个 NavPathStack 实例
  2. 以 key 'pageStack' 注入整个组件树
  3. pageMap 中通过 { pageStack: this.pageStack } 显式传参给各 NavDestination 组件
  4. 子组件通过 @Param pageStack 接收,获得 pushPathByName/pop 等路由能力

重要变更 :NavDestination 组件(NewsDetail、VideoPlayer 等)不再使用 @Consumer('pageStack') 从组件树获取,而是由 pageMap 显式传参 { pageStack: this.pageStack },子组件使用 @Param pageStack 接收。这种方式更加明确,避免了 @Consumer 在非 @ComponentV2 上下文中的兼容性问题。

typescript 复制代码
Navigation(this.pageStack) {
  // 默认内容:Tabs
}
.hideTitleBar(true)                  // 隐藏Navigation默认标题栏
.navDestination(this.pageMap)        // 路由映射回调
.mode(NavigationMode.Stack)          // 栈模式(默认)

Navigation 三大配置

配置 说明
hideTitleBar true 隐藏默认导航栏,各 NavDestination 自定义标题栏
navDestination this.pageMap @Builder 回调,根据路由名渲染对应页面
mode NavigationMode.Stack 栈模式,支持 push/pop 操作
段落4 --- HdsTabs 悬浮底部导航(第53-86行)
typescript 复制代码
HdsTabs({ index: this.currentTabIndex }) {
  TabContent() { NewsHome() }.tabBar(this.buildTabBar($r('sys.symbol.house'), '首页'))
  TabContent() { VideoHome() }.tabBar(this.buildTabBar($r('sys.symbol.video'), '视频'))
  TabContent() { LiveHome() }.tabBar(this.buildTabBar($r('sys.symbol.video_badge_adiowaves'), '直播'))
  TabContent() { PersonalHome() }.tabBar(this.buildTabBar($r('sys.symbol.person'), '我的'))
}
.barOverlap(true)
.barPosition(BarPosition.End)
.vertical(false)
.barFloatingStyle({ barBottomMargin: 16 })
.onChange((index: number) => { this.currentTabIndex = index; })
.layoutWeight(1)

HdsTabs 关键配置

配置 说明
index this.currentTabIndex 当前选中 Tab 索引
.barOverlap(true) true 页签栏悬浮覆盖在内容之上
.barPosition BarPosition.End Tab 栏位于底部
.vertical(false) false 水平排列(非垂直)
.barFloatingStyle { barBottomMargin: 16 } 悬浮样式,底部间距 16vp
.onChange() 回调 监听 Tab 切换事件

4 个 TabContent 与 feature 模块对应关系

Tab 索引 标题 系统图标 嵌入组件 来源模块
0 首页 sys.symbol.house NewsHome() features/news
1 视频 sys.symbol.video VideoHome() features/video
2 直播 sys.symbol.video_badge_adiowaves LiveHome() features/live
3 我的 sys.symbol.person PersonalHome() features/personal

buildTabBar/buildTabIcon 辅助方法 :将图标构建和样式配置封装为私有方法,确保图标颜色与文本颜色保持一致(均使用 focus_colorplaceholder_color)。

段落5 --- @Builder pageMap 路由映射(第67-80行)
typescript 复制代码
@Builder
pageMap(name: string, param: Object) {
  if (name === 'NewsDetail') {
    NewsDetail({ pageStack: this.pageStack })
  } else if (name === 'NewsCategory') {
    NewsCategory({ pageStack: this.pageStack })
  } else if (name === 'VideoPlayer') {
    VideoPlayer({ pageStack: this.pageStack })
  } else if (name === 'LoginPage') {
    LoginPage({ pageStack: this.pageStack })
  } else if (name === 'MyComments') {
    MyComments({ pageStack: this.pageStack })
  }
}

5 个路由映射(显式传参 pageStack)

路由名 组件 来源 触发位置
'NewsDetail' NewsDetail() news NewsHome.onClick
'NewsCategory' NewsCategory() news NewsHome.CategoryBar
'VideoPlayer' VideoPlayer() video VideoHome.onClick
'LoginPage' LoginPage() personal PersonalHome.AvatarSection
'MyComments' MyComments() personal PersonalHome.FunctionGrid

路由匹配流程

复制代码
用户点击新闻
  → pushPathByName('NewsDetail', newsItem)
  → Navigation 在栈中创建新页面
  → 调用 navDestination 回调:pageMap('NewsDetail', newsItem)
  → if (name === 'NewsDetail') → 渲染 NewsDetail({ pageStack: this.pageStack })
  → NewsDetail 通过 @Param pageStack 接收路由栈引用
  → NewsDetail.aboutToAppear() → AppStorage.get('routeParam_NewsDetail') 获取路由参数

六、HdsTabs 沉浸光感悬浮页签详解

6.1 HdsTabs 组件用法

typescript 复制代码
import { HdsTabs } from '@kit.UIDesignKit';  // 仅导入 HdsTabs
import { SymbolGlyphModifier } from '@kit.ArkUI';  // 系统图标渲染器
// BottomTabBarStyle 是全局类,不需要导入

HdsTabs({ index: this.currentTabIndex }) {
  TabContent() { NewsHome() }.tabBar(this.buildTabBar($r('sys.symbol.house'), '首页'))
  // ... 其他 Tab
}
.barOverlap(true)
.barPosition(BarPosition.End)
.vertical(false)
.barFloatingStyle({ barBottomMargin: 16 })
.onChange((index: number) => { this.currentTabIndex = index; })
.layoutWeight(1)

6.2 buildTabIcon + buildTabBar 辅助方法

为保持图标和文本颜色一致,封装两个私有方法:

typescript 复制代码
private buildTabIcon(symbol: Resource, selected: boolean): SymbolGlyphModifier {
  return new SymbolGlyphModifier(symbol)
    .fontColor([selected ? $r('app.color.focus_color') : $r('app.color.placeholder_color')]);
}

private buildTabBar(symbol: Resource, label: string): BottomTabBarStyle {
  return new BottomTabBarStyle({
    normal: this.buildTabIcon(symbol, false),
    selected: this.buildTabIcon(symbol, true)
  }, label).labelStyle({
    unselectedColor: $r('app.color.placeholder_color'),
    selectedColor: $r('app.color.focus_color')
  });
}

关键点

  • BottomTabBarStyle 构造函数的第一个参数是 TabBarSymbol 对象(包含 normal/selected 状态的 SymbolGlyphModifier),第二个参数是标签文字
  • .labelStyle({ selectedColor, unselectedColor }) 设置文本颜色,默认 selectedColor#FF007DFF(蓝色),需手动配置与图标一致的颜色
  • SymbolGlyphModifier.fontColor() 接收数组,用于设置图标颜色

6.3 系统图标选择

Tab 系统图标 (sys.symbol) 说明
首页 house 房屋图标
视频 video 视频图标
直播 video_badge_adiowaves 视频+信号波图标(直播含义)
我的 person 人物图标

注意 :系统图标名称必须严格匹配 sys.symbol 资源定义,不存在的名称会报编译错误 Unknown resource name

6.4 沉浸光感进阶

如需开启系统级沉浸光感材质效果,可引入 hdsMaterial 并配置 systemMaterialEffect

typescript 复制代码
import { HdsTabs, hdsMaterial } from '@kit.UIDesignKit';

.barFloatingStyle({
  barBottomMargin: 16,
  gradientMask: { maskColor: '#66F1F3F5', maskHeight: 92 },
  systemMaterialEffect: {
    materialType: hdsMaterial.MaterialType.ADAPTIVE,
    materialLevel: hdsMaterial.MaterialLevel.ADAPTIVE
  }
})

早期版本中,LiveHome 内部也使用了 Tabs 组件,导致 MainPage 的 Tabs 与 LiveHome 的 Tabs 产生嵌套冲突,内容不渲染。解决方案是将 LiveHome 的 Tab 栏改为自定义 Row + Text 方案,从而消除了嵌套冲突。


七、@Provider + @Param 路由栈传参机制

7.1 数据流图

复制代码
MainPage
  @Provider('pageStack') pageStack: NavPathStack
    │
    ├── Tab → NewsHome (Home 组件不需要 pageStack)
    │         onClick → AppStorage.setOrCreate('routeParam_NewsDetail', plainObj)
    │                  → pageStack.pushPathByName('NewsDetail', null)
    │
    ├── Tab → VideoHome (Home 组件不需要 pageStack)
    │         onClick → AppStorage.setOrCreate('routeParam_VideoPlayer', plainObj)
    │                  → pageStack.pushPathByName('VideoPlayer', null)
    │
    ├── Tab → LiveHome
    │         (无路由跳转,不需要 pageStack)
    │
    └── Tab → PersonalHome (Home 组件不需要 pageStack)
              onClick → pageStack.pushPathByName('LoginPage', null)
    
    pageMap 路由映射:
    └── NewsDetail({ pageStack: this.pageStack })  ← 显式传参
         @Param pageStack: NavPathStack
         → AppStorage.get('routeParam_NewsDetail') 读取路由参数

7.2 为什么用 @Param 替代 @Consumer

早期版本中,NavDestination 组件使用 @Consumer('pageStack') 从组件树中获取 pageStack。但实际运行中发现:

  1. @Consumer 在非 @ComponentV2 上下文中的兼容性问题:NavDestination 组件由系统回调创建,可能不完全支持 @Consumer
  2. AppStorage 禁止存储 @ObservedV2 代理对象(错误码 140115):路由参数不能直接传递 @ObservedV2 对象

因此采用新方案:

  • pageStack 通过 pageMap 显式传参NewsDetail({ pageStack: this.pageStack }),子组件用 @Param pageStack 接收
  • 路由参数通过 AppStorage 中转:Home 组件 onClick 中提取纯对象存入 AppStorage,NavDestination 的 aboutToAppear 中读取

7.3 V1 → V2 对照

V1 V2 说明
@Provide('pageStack') @Provider('pageStack') 注入方(父组件)
@Consume('pageStack') @Param pageStack 消费方(子组件改为显式传参)
@Provide pageStack (key=name) @Provider('pageStack') (显式key) V2 必须显式指定 key

7.4 路由参数传递流程(AppStorage 中转)

由于 AppStorage 禁止存储 @ObservedV2 代理对象,路由参数采用以下流程:

typescript 复制代码
// Home 组件 onClick:提取纯对象存入 AppStorage
let plainObj: Record<string, Object> = {
  'newsId': item.newsId,
  'newsTitle': item.newsTitle,
  // ...
};
AppStorage.setOrCreate('routeParam_NewsDetail', plainObj);
this.pageStack.pushPathByName('NewsDetail', null);

// NavDestination 组件 aboutToAppear:读取纯对象重建数据
aboutToAppear() {
  let param = AppStorage.get<Record<string, Object>>('routeParam_NewsDetail');
  if (param) {
    this.newsItem = new NewsItem(
      param['newsId'] as string, param['newsTitle'] as string, ...);
  }
}

NavPathStack 是 Navigation 组件配套的路由管理器,提供完整的路由操作 API:

方法 功能
pushPathByName(name, param) 推入新页面 + 参数
pop() 返回上一页
getParamByName(name) 获取路由参数
clear() 清空路由栈

相比自定义状态对象,NavPathStack 是系统级方案,与 Navigation 深度集成。


八、PageConstants 路由常量

8.1 完整源码

typescript 复制代码
/**
 * 页面路由常量
 * 统一管理所有页面路由名称
 */
export class PageConstants {
  /** 启动页延迟跳转时间(毫秒) */
  static readonly SPLASH_DELAY: number = 1500;

  /** 路由名称:新闻详情 */
  static readonly PAGE_NEWS_DETAIL: string = 'NewsDetail';
  /** 路由名称:新闻分类 */
  static readonly PAGE_NEWS_CATEGORY: string = 'NewsCategory';
  /** 路由名称:视频播放 */
  static readonly PAGE_VIDEO_PLAYER: string = 'VideoPlayer';
  /** 路由名称:登录页 */
  static readonly PAGE_LOGIN: string = 'LoginPage';
  /** 路由名称:我的评论 */
  static readonly PAGE_MY_COMMENTS: string = 'MyComments';

  /** Tab索引:首页 */
  static readonly TAB_HOME: number = 0;
  /** Tab索引:视频 */
  static readonly TAB_VIDEO: number = 1;
  /** Tab索引:直播 */
  static readonly TAB_LIVE: number = 2;
  /** Tab索引:我的 */
  static readonly TAB_PERSONAL: number = 3;
}

设计思路

  • static readonly:静态只读,编译期确定值,不可修改
  • 集中管理所有路由名称和 Tab 索引,避免散落在各组件中的魔术字符串
  • 修改路由名只需改一处,所有引用处自动更新

九、MainPageData Tab 按钮数据

9.1 完整源码

typescript 复制代码
import { CommonConstants } from 'common';

/**
 * 底部Tab按钮数据模型
 */
export class TabButtonInfo {
  index: number = 0;
  title: string = '';
  icon: Resource = $r('app.media.startIcon');
  selectedIcon: Resource = $r('app.media.startIcon');

  constructor(index: number, title: string, icon: Resource, selectedIcon: Resource) {
    this.index = index;
    this.title = title;
    this.icon = icon;
    this.selectedIcon = selectedIcon;
  }
}

/**
 * 底部Tab按钮数据列表
 * 用于HdsTabs配置
 */
export const TAB_BUTTONS: TabButtonInfo[] = [
  new TabButtonInfo(0, '首页', $r('app.media.ic_home_normal'), $r('app.media.ic_home_focus')),
  new TabButtonInfo(1, '视频', $r('app.media.video'), $r('app.media.video')),
  new TabButtonInfo(2, '直播', $r('app.media.live'), $r('app.media.live')),
  new TabButtonInfo(3, '我的', $r('app.media.ic_personal_normal'), $r('app.media.ic_personal_focus')),
];

TabButtonInfo 模型

属性 类型 说明
index number Tab 索引
title string Tab 标题
icon Resource 未选中图标
selectedIcon Resource 选中图标

4 个 Tab 按钮数据

索引 标题 未选中图标 选中图标
0 首页 ic_home_normal ic_home_focus
1 视频 video video
2 直播 live live
3 我的 ic_personal_normal ic_personal_focus

注意视频和直播的选中/未选中图标相同(Tabs 会自动处理选中态颜色变化)。


十、整应用数据流图

10.1 启动流程

复制代码
应用启动
  → EntryAbility.onCreate()
  → 加载 pages/Index (@Entry)
  → Index.aboutToAppear()
  → setTimeout(1500ms)
  → router.replaceUrl('pages/MainPage')
  → 加载 MainPage (@Entry)
  → Navigation + Tabs 初始化
  → 默认显示第一个 TabContent → NewsHome

10.2 Tab 切换流程

复制代码
用户点击底部"视频"Tab
  → Tabs.onChange(index: 1)
  → this.currentTabIndex = 1
  → TabContent 内容切换为 VideoHome()

10.3 页面跳转流程

复制代码
用户点击新闻列表项
  → NewsHome.onClick()
  → 提取纯对象存入 AppStorage:AppStorage.setOrCreate('routeParam_NewsDetail', plainObj)
  → pageStack.pushPathByName('NewsDetail', null)
  → Navigation 匹配 navDestination 回调
  → pageMap('NewsDetail', null)
  → 渲染 NewsDetail({ pageStack: this.pageStack })
  → NewsDetail 通过 @Param 接收 pageStack
  → NewsDetail.aboutToAppear()
  → AppStorage.get('routeParam_NewsDetail') 读取纯对象
  → 重建 NewsItem 实例,显示新闻详情

10.4 登录状态流

复制代码
PersonalHome → 点击"登录/注册"
  → pushPathByName('LoginPage', null)
  → LoginPage 显示
  → 输入手机号+验证码
  → AppStorage.setOrCreate('userAccount', phone)
  → pageStack.pop() 返回
  → PersonalHome 重新 aboutToAppear
  → AppStorage.get('userAccount') 恢复登录态
  → UI 更新为已登录状态

十一、V2 装饰器全景汇总表

覆盖整个应用 7 个模块的所有 V2 装饰器:

装饰器 含义 使用位置
@ComponentV2 V2 组件声明 所有组件
@Entry 页面入口 Index、MainPage
@Local 组件内状态 NewsHome、VideoHome、LiveHome、PersonalHome、LoginPage 等
@Param 父→子传参 NewsDetail、VideoPlayer、LoginPage、MyComments、NewsCategory、CommonWeb
@Provider('key') 注入组件树 MainPage.pageStack
@ObservedV2 V2 可观察类 NewsItem、VideoItem、LiveItem、CommentItem、UserInfo
@Trace 属性级追踪 所有 @ObservedV2 类的属性
@Builder UI 构建模板 各组件的 Builder 方法

十二、常见问题 Q&A

Q1: 为什么 Index 和 MainPage 用 router 而不用 NavPathStack?

A: Index 和 MainPage 都是 @Entry 页面,注册在 main_pages.json 中,它们之间是平级关系,不在 Navigation 容器内。router 用于 @Entry 页面间的跳转,NavPathStack 用于 Navigation 容器内的页面跳转。

Q2: ServiceHome 为什么没有嵌入 Tab?

A: 当前应用只有 4 个底部 Tab(首页/视频/直播/我的),ServiceHome 已导入但预留用于后续扩展------可以添加第 5 个 TabContent 或在 MoreSection 中跳转到服务页面。

Q3: HdsTabs 与标准 Tabs 的核心区别是什么?

A: HdsTabs 来自 @kit.UIDesignKit,支持悬浮页签(.barOverlap(true) + .barFloatingStyle())和沉浸光感材质效果。标准 Tabs 是 ArkUI 内置组件,不支持悬浮和沉浸光感。本项目已使用 HdsTabs,关键要点:

  • HdsTabs 需要从 @kit.UIDesignKit 导入
  • BottomTabBarStyle全局类,不需要导入
  • .barOverlap(true) 使页签栏悬浮覆盖内容
  • .barFloatingStyle({ barBottomMargin: 16 }) 设置悬浮间距

Q4: 如何添加新的路由页面?

A: 四步操作:

  1. 创建 NavDestination 组件(在 feature 模块中),使用 @Param pageStack: NavPathStack 接收路由栈
  2. 在 MainPage 的 pageMap @Builder 中添加 else if (name === 'NewPage') { NewPage({ pageStack: this.pageStack }) } 分支
  3. Home 组件 onClick 中提取纯对象存入 AppStorage
  4. 在触发位置调用 pushPathByName('NewPage', null)

Q5: NavigationMode.StackNavigationMode.Standard 区别?

A: Stack 模式下页面像栈一样 push/pop;Standard 模式类似浏览器历史,支持 replace。当前应用使用 Stack 模式,这是最常用的模式。


十三、小结

产品定制层 product/phone 以 115 行源码(不含配置文件)实现了完整的应用入口和导航框架,核心知识点包括:

  1. HdsTabs 悬浮底部导航:HdsTabs + BottomTabBarStyle(全局类)+ SymbolGlyphModifier 系统图标 + .labelStyle() 文本颜色配置,实现带图标的沉浸光感悬浮页签
  2. Navigation + NavPathStack:路由容器 + 栈管理器,统一管理 5 个 NavDestination 子页面
  3. @Provider + @Param 显式传参 :pageMap 中 { pageStack: this.pageStack } 显式传参给各子组件
  4. AppStorage 路由参数中转:避免 @ObservedV2 代理对象存入 AppStorage,提取纯 Record 对象中转
  5. @Entry + router:启动页到主页的跳转,replaceUrl 避免返回
  6. pageMap 路由映射:@Builder 回调根据路由名渲染对应组件
  7. HAP vs HAR:entry 类型 vs har 类型,唯一可运行模块 vs 库模块

product/phone 是整个应用的"总指挥",将 7 个模块的组件组装为一个完整的 HarmonyOS 新闻应用。理解 MainPage 的架构设计,就掌握了整个应用的运行原理。


相关推荐
浮芷.1 小时前
六星光芒阵:HarmonyOS API 24 Canvas 高级绘图实战
科技·华为·开源·harmonyos·鸿蒙
极客范儿2 小时前
华为HCIP网络工程师认证—交换基础
网络·华为
狼哥16862 小时前
《新闻资讯》一、应用分层模块化整体实现指南
ui·harmonyos
木咺吟2 小时前
鸿蒙原生应用实战(一):项目搭建与首页开发 — 游戏收藏夹
华为·harmonyos
风华圆舞2 小时前
鸿蒙 + Flutter 如何把 AI 助手嵌进应用页面里——以食界探味为
人工智能·flutter·harmonyos
星栈独行2 小时前
写 Makepad Demo 不难,难的是把它写成项目
前端·程序人生·ui·rust
金启攻2 小时前
【鸿蒙原生应用实战】第五篇:活动记录页——数据筛选、统计与成就系统
harmonyos
金启攻2 小时前
【鸿蒙原生应用实战】第一篇:项目搭建与首页开发——从零构建户外助手App
华为·harmonyos
Swift社区2 小时前
AI + 鸿蒙游戏:下一代游戏架构正在形成吗?
人工智能·游戏·harmonyos