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

相关推荐
子兮曰2 天前
jev-ultrafast 深度解析:7 秒订机票的浏览器 Agent 是如何炼成的
前端·后端·agent
子兮曰2 天前
Jev 爆发一周:7 秒 Agent 背后的 System One 生态与三场争议
前端·后端·ai编程
前端小万2 天前
写公众号赚了 3000 块后,我做了一款叫 "一键成稿" 的软件
前端·微信小程序
爱勇宝2 天前
ZCode 开源 24 小时:一份没有历史的账本,回答不了"有没有偷代码"
前端·后端·chatglm (智谱)
三十而立洋2 天前
Cookie 详解:从产生到安全,一次讲透
前端·javascript
卡布鲁2 天前
把一个 Vite + Vue3 应用塞进 qiankun (React + Umi3) 主站:十个坑的复盘
前端·javascript·react.js
汉堡大王95272 天前
Jev:不是聊天机器人, 而是一个智能 if 语句
前端·人工智能·后端
梦想很大很大2 天前
从运行事实到回归证据:Workrun 的 Telemetry 与 Evaluation 实践
前端·人工智能·后端
计算机魔术师2 天前
Meta Muse agent 接入 Shopify 的 Shop Pay 实现代理式购物
前端
沙蒿同学2 天前
我用 Go 搭了一条 AI Agent 流水线:从 1 张商品图到一整套淘宝详情页
前端·javascript·后端