前言
这篇我们把 ArkUI 在鸿蒙 6 的底层思路彻底捋顺,然后用一个最小可运行项目把它跑起来。
我们先讲为什么用声明式 UI、为什么用 Navigation 做导航,再把状态管理、页面组织、参数传递、跳转与返回逐个落地。
一、ArkUI 到底怎么想
ArkUI 在鸿蒙 6 走的是声明式范式。换句话说,我们不再"到处发命令改界面",而是把界面写成组件树,让"状态变化"自动驱动"视图重绘"。
导航这层,官方明确推荐组件化的 Navigation,配上 NavPathStack 管理"页面栈",子页由 NavDestination 托管,跳转与返回都走 push/pop 这一套标准动作。这样分工特别干净:导航交给导航容器,页面归页面自己,状态交给状态系统。

我们还得知道"Router 到 Navigation 的迁移"这件事。
老项目可能用过 Router 模块;现在官方给出了一份迁移指南,直说了怎么把路由 API 过渡到 Navigation 体系。新项目我们优先直接上 Navigation,老项目按文档逐步迁移,避免两套范式混用的灰度区。

二、页面与导航我们怎么组织
清单里只声明一个入口页,它只有一个 @Entry,应用启动先显示它。其他页面不再写进清单,而是写成 NavDestination 子页,由首页里的 Navigation 容器托管。
Navigation 拿到一个 NavPathStack 之后,我们就能用 pushPath、pop、replacePath 把"跳转、返回、替换"都标准化,动画和显示模式自然承接。

拿到 NavPathStack 的正确姿势:子页要操作路由,必须拿到 Navigation 所属的那条栈。
最稳的方式是用 NavDestination 的 onReady 拿 NavDestinationContext,再从里头读到 context.pathStack;这比直接 @Provide/@Consume 传栈耦合更低,后期重构会更省心。
三、状态驱动我们怎么落地
ArkUI 的状态系统提供了一组装饰器来表达数据归属与同步方式:组件内部自己管用 @State ,父传子单向同步用 @Prop ,需要双向协同时用 @Link ,跨层共享用 @Provide/@Consume;还有更进阶的 V2 状态能力与混用指导,涉及 API 版本差异与迁移注意事项。
能就近就近,能局部不全局,先用 @State 把作用域收紧,再考虑跨层共享。


四、最小项目:从清单到首页,再到二级页
我们直接起一个最小工程,把"Navigation 是根容器、子页写在 NavDestination、push/pop 走栈、UIAbility 加载入口"完整跑一遍。
工程用 Stage 模型,清单里只声明首页;窗口加载交给 UIAbility 的 onWindowStageCreate。
1)只声明首页的清单
我们把入口页写进 main_pages.json,其它页面交给 Navigation 托管,不再出现在清单里。文件在 resources/base/profile,下述写法最干净:
resources/base/profile/main_pages.json
css
{
"src": ["pages/Index"]
}
官方文档明确提到 main_pages 的位置与作用,甚至说明了文件名可自定义但路径结构要对齐;别忘了 DevEco Studio 自动生成的页面会自动写进清单,手动复制的页面要自己补条目。
2)首页:用 Navigation 托管内容,并提供页面栈
我们把导航容器放在首页最外层,然后用一个 pageMap 把"名字 → 子页 UI"的映射交给它:
entry/src/main/ets/pages/Index.ets
less
import { Second } from './Second'
@Entry
@Component
export struct Index {
@Provide('stack') stack: NavPathStack = new NavPathStack()
@State counter: number = 0
@Builder pageMap(name: string) {
if (name === 'Second') {
Second()
}
}
build() {
Navigation(this.stack) {
Column({ space: 16, alignItems: HorizontalAlign.Center }) {
Text('Hello HarmonyOS').fontSize(22).margin({ top: 24 })
Text(`当前计数:${this.counter}`).fontSize(16)
Button('点我 +1', { type: ButtonType.Capsule, stateEffect: true })
.onClick(() => this.counter += 1)
.margin({ top: 8 })
Button('进入第二页', { type: ButtonType.Capsule, stateEffect: true })
.onClick(() => this.stack.pushPath({ name: 'Second' }))
.margin({ top: 16 })
}
.width('100%').height('100%').justifyContent(FlexAlign.Center)
}
.navDestination(this.pageMap)
.hideTitleBar(true)
}
}
我们再强调三个关键点:
第一,首页的最外层一定要是 Navigation(this.stack),否则 pushPath 不会触发可见跳转;
第二,.navDestination(this.pageMap) 负责把"路径名"和"子页 UI"绑定上,不写就是空白页;
第三,子页要拿到同一条栈,返回才能回到对的页面。
这套写法与官方"推荐使用 Navigation + NavPathStack + navDestination"的组合路线是一致的。
3)二级页:以 NavDestination 为根,并复用页面栈
我们把第二页写成被导航容器接管的目标页:
entry/src/main/ets/pages/Second.ets
less
@Component
export struct Second {
@Consume('stack') stack: NavPathStack
@State tipsShown: boolean = false
build() {
NavDestination() {
Column({ space: 16, alignItems: HorizontalAlign.Center }) {
Text('第二个页面').fontSize(20).margin({ top: 24 })
Button('显示小提示').onClick(() => this.tipsShown = true)
if (this.tipsShown) {
Text('这里演示了在子页里用状态驱动界面更新。').fontSize(14)
}
Button('返回首页')
.onClick(() => this.stack.pop())
.margin({ top: 12 })
}
.width('100%').height('100%').justifyContent(FlexAlign.Center)
}
.title('Second')
}
}
这里我们只盯两件事:第一,子页的根就是 NavDestination,因为它就是 Navigation 的内容区;第二,子页不写 @Entry,也不进清单,这是 NavDestination 的定位。


如果你想用"更推荐"的拿栈方式,我们可以把子页换成这样:在 NavDestination 上挂 onReady,拿到 NavDestinationContext,然后把 context.pathStack 存起来。
4)入口加载与 UIAbility 的分工
我们把"建窗口、设起始页"交给 UIAbility,在 onWindowStageCreate() 里调用 windowStage.loadContent('pages/Index')。
这层只管窗口生命周期,不去"切根";页面的跳转交给 Navigation 的栈管理。官方对 UIAbility 的生命周期与窗口事件订阅也有完整说明,你要做窗口级事件(获得/失去焦点、尺寸变化)就放这里,不要混到页面栈里。
还有一个很实在的"白屏陷阱":如果 onWindowStageCreate 里没设置启动页,应用冷启动就可能出现白屏。
五、常见进阶用法:Tabs、多栈与跨包
我们做点现实里的"增强套路"。
第一种是"底部 Tab + 独立栈":每个 Tab 配一条 NavPathStack,这样你在某个 Tab 里深跳了三层,切到别的 Tab 再切回来,历史不丢。Tabs 与 Navigation 一起上就能得到"持久化的多栈导航"。
第二种是"多导航容器":高级用法里有 MultiNavigation 组件与多栈操作,适合做主从布局、双列模式等复杂场景。API 提供了 pushPath 的多种签名与策略,你可以按场景灵活选择。
第三种是"跨包导航"和"转场控制":包含跨包页面的路由,以及转场动效的开关与自定义,你要拆 HSP 或分模块。
六、常见问题汇总
1)点了没反应。多数是首页根不是 Navigation(this.stack),或者漏了 .navDestination(pageMap)。把根容器换回 Navigation,并补上"名字 → 子页 UI"的映射,pushPath 就会正常。
2)跳过去是空白。常见是二级页不是以 NavDestination 为根,或者 pushPath({ name: 'Second' }) 与 pageMap 里的名字不一致。把子页包进 NavDestination,并保证名称一致,就能看到内容。
3)返回错页或不返回。常见是子页自己 new 了一条 NavPathStack,而不是复用 Navigation 持有的那条;更稳的方式是用 onReady 拿 context.pathStack。
4)清单与入口打架。被清单托管的页面文件必须恰好一个 @Entry。只在首页写 @Entry 并写进 main_pages.json,其他页面全部由 Navigation 托管,冲突自然消失。
5)页面没进清单。DevEco 自动创建的页面会自动登记;你手动复制过去的页面,需要自己补 main_pages.json,否则 UIAbility 冷启动加载不了。
6)状态用法混乱。V1/V2 装饰器混用要看 API 版本与官方混用指南;父子同步、跨层共享、对象观察(Observed/ObjectLink)各有边界,别一上来就全局共享。
总结
这一次我们理清了 ArkUI 在鸿蒙 6 的主线:先确立声明式 UI 与状态驱动,再用 Navigation + NavPathStack 做统一的页面调度,让子页以 NavDestination 承载,把入口页写进 main_pages.json,由 UIAbility 在 onWindowStageCreate 里加载首页。
下一篇我们讲 Row、Column、Stack、Grid、Scroller 这些骨架,以及涉及到的布局性能与可读性。