鸿蒙 HarmonyOS 6|ArkUI(01):从框架认知到项目骨架

前言

这篇我们把 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 自动生成的页面会自动写进清单,手动复制的页面要自己补条目。

我们把导航容器放在首页最外层,然后用一个 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"的组合路线是一致的。

我们把第二页写成被导航容器接管的目标页:

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 这些骨架,以及涉及到的布局性能与可读性。

相关推荐
崔庆才丨静觅41 分钟前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby60611 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了2 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅2 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅2 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅2 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment2 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅3 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊3 小时前
jwt介绍
前端
爱敲代码的小鱼3 小时前
AJAX(异步交互的技术来实现从服务端中获取数据):
前端·javascript·ajax