为啥还分不清 Stage 和 Page?不如一小时把 ArkUI 玩明白,岂不快哉?

举报
bug菌 发表于 2025/11/01 22:25:41 2025/11/01
【摘要】 🏆本文收录于「滚雪球学SpringBoot」专栏(全网一个名),手把手带你零基础入门Spring Boot,从入门到就业,助你早日登顶实现财富自由🚀;同时,欢迎大家关注&&收藏&&订阅!持续更新中,up!up!up!!环境说明:Windows 10 + IntelliJ IDEA 2021.3.2 + Jdk 1.8 前言老实说,第一次打开鸿蒙应用项目时,我也被“Stage 模型”“Pa...

🏆本文收录于「滚雪球学SpringBoot」专栏(全网一个名),手把手带你零基础入门Spring Boot,从入门到就业,助你早日登顶实现财富自由🚀;同时,欢迎大家关注&&收藏&&订阅!持续更新中,up!up!up!!

环境说明:Windows 10 + IntelliJ IDEA 2021.3.2 + Jdk 1.8

前言

老实说,第一次打开鸿蒙应用项目时,我也被“Stage 模型”“Page 模型”整得一愣一愣的:这俩到底谁管窗、谁管页、谁来调度生命周期?别急,今天我们用人话 + 代码把鸿蒙应用框架(ArkUI)扒个明白:从模型差异、UI 组件体系,到状态管理与动画——不堆概念,不玩花活,写出能跑、能上线、能扩展的代码。要点清晰、坑点直说,顺手塞点我亲测的“避坑锦囊”。走起~🚀

1) ArkUI 是啥:更像“声明式搭积木”,而不是“命令式画图纸”

ArkUI 是鸿蒙(OpenHarmony/HarmonyOS)的声明式 UI 框架,语法糖来自 ArkTS(TypeScript 的超集语法)。口号拆解一下:

  • 声明式:你描述“界面应该长啥样”,框架负责“怎么画出来”。
  • 响应式:数据变了,界面自动刷新(无需手搓 findViewById 那套)。
  • 跨端设计:组件语义稳定,设备差异由框架适配(手机、平板、可穿戴…)。

一句话:像 React 的写法 + 原生系统级性能,但保留了鸿蒙生态的一体化能力。


2) Stage 模型 vs Page 模型:到底该怎么选?

这俩模型不是“新旧替代”,而是“视角不同”

Page 模型(轻量、上手快)

  • 面向页面栈:以页面(Page)为单位,配合路由跳转管理视图。
  • 简化生命周期:少碰底层窗口、任务栈,写业务最省心。
  • 适合:中小型 App、单窗体、以页面堆栈为主的交互。

记忆法:Page 就是“页面导向”。像纯前端路由一样丝滑。

Stage 模型(灵活、工程化)

  • 面向能力(Ability)和窗口(WindowStage):能精细控制窗口、任务、多实例。
  • 生命周期全面:Ability 启动/挂起/恢复/销毁全流程可控。
  • 适合:多窗口、跨任务、复杂系统集成、需精细化生命周期管理的中大型 App。

记忆法:Stage 就是“舞台导向”。灯光、幕布、演员(窗口、任务、页面)都你说了算。

选型建议

  • 80% 业务:用 Page 模型,快。
  • 需要多窗口/系统级协作:上 Stage 模型,稳。
  • 团队分层明确:UI 团队 Page、平台组 Stage,不冲突还能共存。

3) UI 组件体系:Row/Column/Stack 只是开胃菜,List/Grid/Tabs 才是主菜

ArkUI 的组件分三类理解更顺:

  1. 布局容器RowColumnStackFlexGrid
  2. 基础控件TextImageButtonTextInputCheckboxSwitchSlider
  3. 结构与导航Navigation/NavDestinationTabsList/ListItemScrollSearch

特点:属性链式 + 修饰器式样式,写起来“像拼句子”。

示意(ArkTS):

@Entry
@Component
struct HomePage {
  @State keyword: string = ''

  build() {
    Column({ space: 12 }) {
      Text('ArkUI 入门清单').fontSize(22).fontWeight(FontWeight.Bold).padding(12)

      TextInput({ placeholder: '搜索点什么……' })
        .width('100%')
        .onChange(v => this.keyword = v)

      List() {
        ListItem() { Text('Stage vs Page,懂了没?') }
        ListItem() { Text('状态管理——@State 到 @Provide/@Consume') }
        ListItem() { Text('动画系统——隐式与显式的取舍') }
      }
      .width('100%')
      .divider({ strokeWidth: 1 })
    }
    .padding(16)
  }
}

4) 状态管理:@State 到 @Provide/@Consume,一步步把“数据这口锅”端稳

ArkUI 的状态系统分层很清晰,理解“数据归属”就不乱:

  • 局部状态@State(组件私有)、@Link(对子组件双向绑定)、@Prop(父传子只读)
  • 跨层共享@Provide / @Consume(祖先-后代依赖注入式共享)
  • 应用级存储AppStorage / @StorageLink(全局可持久/可观察)
  • 可观察对象@Observed / @ObjectLink(对象属性级变更跟踪,适合复杂模型)

小抄一张表:

想解决的问题 用什么 说明
组件内部的 UI 状态 @State 最常用,最简单
父组件把只读数据给子组件 @Prop 子组件不能改
父子双向同步(小心滥用) @Link 小范围联动很香
跨很多层共享(如主题/用户) @Provide/@Consume 结构化依赖注入
全局共享 + 可观察 AppStorage/@StorageLink 持久、跨页面
复杂对象变更 @Observed/@ObjectLink 局部字段变更也能触发

示例:@Provide/@Consume 共享主题色

@Provide('themeColor') themeColor: Color = Color.Blue

// 任意后代
@Consume('themeColor') color!: Color

5) 动画系统:隐式动画到显式动画,从“丝滑过渡”到“情绪表达”

ArkUI 动画大体两路:

  1. 隐式动画(推荐兜底)
    在样式链上加 .animation(),样式变化自动平滑过渡。写最少,观感友好。

    @State progress: number = 0
    
    Rectangle()
      .width(this.progress + '%')
      .height(6)
      .borderRadius(3)
      .animation({ duration: 300, curve: Curve.EaseInOut })
    
  2. 显式动画(需要编排时)
    animateTo()TransitionEffectKeyframeAnimation 等,精确控制时序、曲线、联动。

    animateTo({ duration: 400, curve: Curve.Spring }) => {
      this.open = !this.open
      this.opacity = this.open ? 1 : 0
      this.offsetY = this.open ? 0 : -20
    }
    

补两味“调料”:

  • 过渡(transition)TransitionEffect 做进出场(Fade/Slide/Scale/Shared),路由切页更自然。
  • 手势 + 动画GesturePanGesture 联动 animateTo,做“拖拽回弹”那味儿人性化交互。

6) 一套可跑的微型示例:待办清单 + 过渡动画(ArkTS)

目标:展示 Page 模型 下的页面路由、状态管理(列表 + 局部编辑)、动画(新增项的“气泡”过渡)。

路由入口(Page 模型)

// Entry 文件
@Entry
@Component
struct App {
  build() {
    Navigation() {
      NavDestination({ title: 'Todos' }) {
        HomePage()
      }
      NavDestination({ title: 'Detail' }) {
        DetailPage()
      }
    }
  }
}

主页:列表 + 新增项动画

@Struct
class Todo {
  constructor(public id: number, public text: string, public done: boolean = false) {}
}

@Component
struct HomePage {
  @State input: string = ''
  @State todos: Todo[] = [ new Todo(1, '读完 ArkUI 状态管理'), new Todo(2, '给按钮加个弹簧动画') ]
  private nextId: number = 3

  build() {
    Column({ space: 12 }) {
      // 输入区
      Row({ space: 8 }) {
        TextInput({ placeholder: '来点啥待办?' })
          .width('70%')
          .onChange(v => this.input = v)
        Button('Add')
          .type(ButtonType.Capsule)
          .onClick(() => this.addTodo())
      }

      // 列表
      List({ space: 6 }) {
        ForEach(this.todos, (item: Todo) => {
          ListItem() {
            Row({ space: 10 }) {
              Checkbox({ name: `todo-${item.id}`, group: 'todos', checked: item.done })
                .onChange(v => item.done = v)
              Text(item.text)
                .decoration({ type: item.done ? TextDecorationType.LineThrough : TextDecorationType.None })
                .fontColor(item.done ? '#999999' : '#222222')
              Blank()
              Button('Detail')
                .onClick(() => {
                  // 跳转携带数据(可用 AppStorage/参数等方式)
                  AppStorage.Set('currentTodo', JSON.stringify(item))
                  router.pushUrl({ url: 'Detail' })
                })
            }
            .padding(10)
          }
          // 每个 ListItem 出场做个“轻弹”过渡
          .transition(TransitionEffect.OPACITY
              .combine(TransitionEffect.SCALE)
              .animation({ curve: Curve.Spring, duration: 240 }))
        })
      }
      .edgeEffect(EdgeEffect.Spring)
    }
    .padding(16)
  }

  addTodo() {
    if (!this.input.trim()) return
    // 显式动画包裹状态更新,让新增项“蹦”一下
    animateTo({ duration: 220, curve: Curve.Spring }, () => {
      this.todos = [ new Todo(this.nextId++, this.input), ...this.todos ]
      this.input = ''
    })
  }
}

详情页:@StorageLink 读取全局存的当前项

import router from '@ohos.router'

@Component
struct DetailPage {
  @StorageLink('currentTodo') current: string = ''  // 简化处理:字符串存储
  @State shake: number = 0

  get todo(): Todo | null {
    try { return JSON.parse(this.current) as Todo } catch { return null }
  }

  build() {
    Column({ space: 14 }) {
      if (!this.todo) {
        Text('没有找到待办项……').fontColor(Color.Red)
        Button('返回').onClick(()=> router.back())
        return
      }
      Text(`📝 ${this.todo!.text}`)
        .fontSize(20)
        .fontWeight(FontWeight.Bold)
        .translate({ x: this.shake })
        .animation({ duration: 120 })  // 隐式抖动

      Row({ space: 8 }) {
        Button('轻轻提醒一下')
          .onClick(()=> this.joyShake())
        Button('返回')
          .onClick(()=> router.back())
      }
    }
    .padding(20)
  }

  joyShake() {
    // 一个小抖动的显式动画
    animateTo({ duration: 80 }, () => this.shake = -6)
    setTimeout(()=> animateTo({ duration: 80 }, () => this.shake = 6), 90)
    setTimeout(()=> animateTo({ duration: 60 }, () => this.shake = 0), 180)
  }
}

这段示例囊括了:Page 路由、局部 @State、AppStorage 跨页共享、隐式/显式动画。你可以把 AppStorage 换成 @Provide/@Consume 或路由参数传递,看团队习惯。


7) 工程结构与常见坑:我踩过的雷你就别踩了

推荐工程分层(思路,不是唯一答案)

/features        # 业务特性:todo、user、settings
  /todo
    components/  # 纯 UI 组件(无副作用)
    pages/       # 页面组件(@Entry 或路由承载)
    services/    # 数据访问(fetch、dataShare)
    store/       # 状态(@Provide/@Consume 封装)
/shared          # 公共:hooks、types、theme、ui-kits
/app             # 路由、入口、全局 Provider、权限守卫
  • UI 与状态解耦:可复用、可单测。
  • Provider 下沉:主题、用户、权限走 @Provide,避免到处“传 props”。
  • 服务边界清晰:网络请求与数据缓存别混在组件里。

常见坑 & 解法

  1. “为啥我 Cookie/参数丢了?”

    • 跨页面数据建议用 AppStorage 或参数化路由(在 Stage 下可走 Ability 参数)。
  2. “列表动画一卡一顿?”

    • 列表项尽量不可变更新(创建新数组触发 diff 更明确),避免在大循环里频繁同步 IO。
  3. “多窗口怎么办?”(Stage 模型)

    • 使用 WindowStage 管理窗口生命周期,注意不同窗口共享状态要通过可观察对象或全局存储,并做好并发保护。
  4. “状态乱飞找不到源头?”

    • 规则:自上而下单向数据流。跨层才用 @Provide/@Consume;双向只限局部用 @Link
  5. “动画掉帧?”

    • 把重计算放到 requestAnimationFrame 同步点之外(或拆分),尽量用框架提供的隐式动画,显式动画集中控制、别嵌套太深。

8) 结语 & 你可能的下一步

到这儿,Stage 与 Page 的边界是不是清楚多了?UI 组件的套路是不是也顺手些了?状态与动画这一挂,掌握了**“最少必要原则”**之后,剩下就是不断把需求拆小、抽象沉淀、踩坑复盘。框架是工具,情绪是体验的调味料——让你的界面“动”起来、让交互“说话”,用户才会“心动”。

**你想我继续怎么打磨?**👇

  • 要不要我把上面 Demo 改成 Stage 模型版本,演示窗口与 Ability 生命周期?
  • 还是把示例升级为一个多 Tab 的小应用(首页/搜索/设置),顺带加上主题切换与持久化
  • 又或者,来个组件库式的 UI 指南(Button、Card、List、Dialog、Toast 的设计范式与统一动效节奏)?

只要你告诉我目标设备(手机/平板/穿戴)最想实现的业务场景,我就把骨架按你的口味调好,顺带把动画统一成一套“呼吸感”的节奏。咱们把“能跑的 Demo”,打磨成“敢上线的小成品”。😉✨

🧧福利赠与你🧧

  无论你是计算机专业的学生,还是对编程有兴趣的小伙伴,都建议直接毫无顾忌的学习此专栏「滚雪球学SpringBoot」专栏(全网一个名),bug菌郑重承诺,凡是学习此专栏的同学,均能获取到所需的知识和技能,全网最快速入门SpringBoot,就像滚雪球一样,越滚越大, 无边无际,指数级提升。

  最后,如果这篇文章对你有所帮助,帮忙给作者来个一键三连,关注、点赞、收藏,您的支持就是我坚持写作最大的动力。

  同时欢迎大家关注公众号:「猿圈奇妙屋」 ,以便学习更多同类型的技术文章,免费白嫖最新BAT互联网公司面试题、4000G pdf电子书籍、简历模板、技术文章Markdown文档等海量资料。

✨️ Who am I?

我是bug菌(全网一个名),CSDN | 掘金 | InfoQ | 51CTO | 华为云 | 阿里云 | 腾讯云 等社区博客专家,C站博客之星Top30,华为云多年度十佳博主/价值贡献奖,掘金多年度人气作者Top40,掘金等各大社区平台签约作者,51CTO年度博主Top12,掘金/InfoQ/51CTO等社区优质创作者;全网粉丝合计 30w+;更多精彩福利点击这里;硬核微信公众号「猿圈奇妙屋」,欢迎你的加入!免费白嫖最新BAT互联网公司面试真题、4000G PDF电子书籍、简历模板等海量资料,你想要的我都有,关键是你不来拿。

-End-

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。