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)
模块职责:
- 提供应用启动入口(Index.ets 启动页)
- 集成 HdsTabs 悬浮底部导航,嵌入 4 个 feature 模块的 Home 组件
- 提供 Navigation 路由容器,统一映射 5 个 NavDestination 子页面
- 通过 @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" --- 响应系统首页启动意图 |
extensionAbilities :EntryBackupAbility 是系统自动生成的备份能力,type 为 "backup",用于应用数据备份恢复。
2.3 main_pages.json(7行)
json
{
"src": [
"pages/Index",
"pages/MainPage"
]
}
注册 2 个路由页面:
pages/Index--- 启动页(Splash),应用入口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
}
跳转链路:
setTimeout--- 延迟 1500msthis.getUIContext()--- V2 获取 UI 上下文(V1 用getContext(this)).getRouter()--- 获取路由器实例.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') 的核心作用:
- 创建一个
NavPathStack实例 - 以 key
'pageStack'注入整个组件树 - pageMap 中通过
{ pageStack: this.pageStack }显式传参给各 NavDestination 组件 - 子组件通过
@Param pageStack接收,获得pushPathByName/pop等路由能力
重要变更 :NavDestination 组件(NewsDetail、VideoPlayer 等)不再使用
@Consumer('pageStack')从组件树获取,而是由 pageMap 显式传参{ pageStack: this.pageStack },子组件使用@Param pageStack接收。这种方式更加明确,避免了 @Consumer 在非 @ComponentV2 上下文中的兼容性问题。
段落3 --- Navigation 容器(第28-54行)
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_color和placeholder_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。但实际运行中发现:
- @Consumer 在非 @ComponentV2 上下文中的兼容性问题:NavDestination 组件由系统回调创建,可能不完全支持 @Consumer
- 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, ...);
}
}
7.5 NavPathStack 路由操作 API
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: 四步操作:
- 创建 NavDestination 组件(在 feature 模块中),使用
@Param pageStack: NavPathStack接收路由栈 - 在 MainPage 的 pageMap @Builder 中添加
else if (name === 'NewPage') { NewPage({ pageStack: this.pageStack }) }分支 - Home 组件 onClick 中提取纯对象存入 AppStorage
- 在触发位置调用
pushPathByName('NewPage', null)
Q5: NavigationMode.Stack 和 NavigationMode.Standard 区别?
A: Stack 模式下页面像栈一样 push/pop;Standard 模式类似浏览器历史,支持 replace。当前应用使用 Stack 模式,这是最常用的模式。
十三、小结
产品定制层 product/phone 以 115 行源码(不含配置文件)实现了完整的应用入口和导航框架,核心知识点包括:
- HdsTabs 悬浮底部导航:HdsTabs + BottomTabBarStyle(全局类)+ SymbolGlyphModifier 系统图标 + .labelStyle() 文本颜色配置,实现带图标的沉浸光感悬浮页签
- Navigation + NavPathStack:路由容器 + 栈管理器,统一管理 5 个 NavDestination 子页面
- @Provider + @Param 显式传参 :pageMap 中
{ pageStack: this.pageStack }显式传参给各子组件 - AppStorage 路由参数中转:避免 @ObservedV2 代理对象存入 AppStorage,提取纯 Record 对象中转
- @Entry + router:启动页到主页的跳转,replaceUrl 避免返回
- pageMap 路由映射:@Builder 回调根据路由名渲染对应组件
- HAP vs HAR:entry 类型 vs har 类型,唯一可运行模块 vs 库模块
product/phone 是整个应用的"总指挥",将 7 个模块的组件组装为一个完整的 HarmonyOS 新闻应用。理解 MainPage 的架构设计,就掌握了整个应用的运行原理。