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

相关推荐
vortex51 分钟前
深度字典攻击(实操笔记·红笔思考)
前端·chrome·笔记
我是伪码农3 分钟前
Vue 1.30
前端·javascript·vue.js
利刃大大11 分钟前
【Vue】默认插槽 && 具名插槽 && 作用域插槽
前端·javascript·vue.js
艳阳天_.15 分钟前
web 分录科目实现辅助账
开发语言·前端·javascript
小白640232 分钟前
2025年终总结-迷途漫漫,终有一归
前端·程序人生
烟花落o37 分钟前
贪吃蛇及相关知识点讲解
c语言·前端·游戏开发·贪吃蛇·编程学习
晚霞的不甘40 分钟前
Flutter for OpenHarmony专注与习惯的完美融合: 打造你的高效生活助手
前端·数据库·经验分享·flutter·前端框架·生活
kogorou0105-bit1 小时前
前端设计模式:发布订阅与依赖倒置的解耦之道
前端·设计模式·面试·状态模式
止观止1 小时前
像三元表达式一样写类型?深入理解 TS 条件类型与 `infer` 推断
前端·typescript
雪芽蓝域zzs1 小时前
uniapp 省市区三级联动
前端·javascript·uni-app