大家好,我是[晚风依旧似温柔],新人一枚,欢迎大家关注~

本文目录:

前言

先说点人话:如果你现在做鸿蒙应用开发,还只停留在 PageAbility / ServiceAbility 那一套旧模型,那多半已经感觉到有点别扭了——代码越写越臃肿、生命周期越来越混乱,窗口逻辑、页面逻辑全揉在一起,一改就崩,一动就乱。

Stage 模型 出来的目的,其实就两件事:

  1. 把过去“大杂烩式”的能力模型拆干净;
  2. 更好地服务 ArkUI 声明式 UI + 多窗口场景 + 更清晰架构

这篇咱们就按你给的大纲,一条一条往下掰开讲,不整文档腔,用真实项目视角来聊:

  • Stage vs Ability 模型差异
  • UIAbility、WindowStage 生命周期
  • 创建 Stage 应用示例
  • 页面跳转与参数传递
  • 与旧模型兼容注意事项
  • 最佳实践与常见错误

看完这篇,你至少应该能做到两件事:
① 理解 Stage 的设计思路,② 独立搭一个能上线的 Stage 工程架构。

一、Stage vs Ability 模型差异:从“一锅炖”到“分层料理”

1.1 旧 Ability 模型:能用,但越来越难维护

先复盘一下老朋友 Ability 模型

  • PageAbility:带界面的页面
  • ServiceAbility:后台服务
  • DataAbility:数据访问

在典型旧项目里,一个 PageAbility 往往做了这些事:

  • 管生命周期(onStart / onActive / onInactive / onStop)
  • 管 UI 加载(通过 setUIContent 或者 XML / JS UI)
  • 管页面路由(甚至自己 new 组件切来切去)
  • 偶尔兼着做一点业务逻辑(发请求、存状态)

结果就是:

  • 一个类里同时有 UI、生命周期、业务逻辑、路由逻辑
  • 页面稍微复杂一点,PageAbility 的行数飙到上千
  • 想拆、想复用、想改架构——都很痛苦

一句话形容旧模型:

“能跑,但非常容易写成意大利面条。”


1.2 Stage 模型:UIAbility + WindowStage + Page 的三段式

Stage 模型干了一件很重要的事:把以前堆在 Ability 里的东西拆开了

核心角色变成:

  1. UIAbility

    • 一个“应用场景”的入口
    • 负责进程/场景生命周期
    • 负责拿到 WindowStage,但不直接写 UI 布局
  2. WindowStage

    • 某个窗口的“舞台”
    • 决定实际加载哪个 ArkUI 页面:loadContent('pages/Index')
    • 管这个窗口级别的生命周期:创建 / 刷新 / 销毁
  3. ArkUI Page(页面组件)

    • 真正的 UI、交互、数据绑定都在这里
    • 就是你写的 @Entry struct IndexDetail 等组件

可以画成这样一条链:

UIAbility  ——  WindowStage  ——  ArkUI 页面(Index / Detail / ...)

再对比一下两代模型的心智:

维度 Ability 模型 Stage 模型
入口角色 PageAbility UIAbility
UI 管理 Ability 自己管理 UI WindowStage 加载 ArkUI 页面
页面结构 XML/JS UI + Ability 挂一起 ArkUI 声明式组件,和 Ability 解耦
生命周期粒度 能力级 场景级(UIAbility)+ 窗口级(WindowStage)
路由方式 Ability 间跳转为主 同 UIAbility 内通过 router 管理页面栈
适合的规模 小中型项目还能顶住 更适合中大型,多窗口,多场景

简单粗暴一句话

Ability 模型像老式一居室,卧室厨房客厅一锅炖;
Stage 模型像复式公寓,客厅=WindowStage,家=UIAbility,房间=Page,层次清晰很多。


二、UIAbility、WindowStage 生命周期:时机没搞懂,Bug 会非常诡异

写 Stage 应用,搞清楚生命周期是第一步

2.1 UIAbility 生命周期:应用“场景”的生老病死

典型回调:

import UIAbility from '@ohos.app.ability.UIAbility';

export default class MainAbility extends UIAbility {
  onCreate(want, launchParam) {
    console.info('[MainAbility] onCreate');
    // 初始化全局对象:日志、网络、AppStorage、依赖注入容器等
  }

  onForeground() {
    console.info('[MainAbility] onForeground');
    // 回到前台:恢复前台专属资源(比如继续某些轮询)
  }

  onBackground() {
    console.info('[MainAbility] onBackground');
    // 进入后台:暂停动画、定时任务,持久化重要状态
  }

  onDestroy() {
    console.info('[MainAbility] onDestroy');
    // 最后一次清理:关闭连接、取消注册等
  }
}

可以这么记:

  • onCreate只执行一次(实例级),适合做一次性的初始化
  • onForeground / onBackground场景在前台/后台切换
  • onDestroy:用户彻底退出这个场景后才会走到

✋ 很重要:
UIAbility 不负责具体 UI 布局和页面跳转,那是 WindowStage 和 ArkUI 页面要做的事。


2.2 WindowStage 生命周期:窗口的“开灯、关灯、换场”

UIAbility 中最关键的是这个回调:

onWindowStageCreate(windowStage) { ... }

它的职责非常明确:窗口创建好了,你来告诉我这个窗口要展示哪个页面。

典型三件事:

onWindowStageCreate(windowStage) {
  console.info('[MainAbility] onWindowStageCreate');

  windowStage.loadContent('pages/Index', (err, data) => {
    if (err) {
      console.error(`loadContent failed: ${JSON.stringify(err)}`);
      return;
    }
    console.info('Index page loaded');
  });
}

onWindowStageRefresh(windowStage) {
  console.info('[MainAbility] onWindowStageRefresh');
  // 当窗口配置变更(如横竖屏、深浅色等)时可能会调用
}

onWindowStageDestroy() {
  console.info('[MainAbility] onWindowStageDestroy');
  // 释放跟这个窗口强关联的资源:监听、定时、对象等
}

节奏是这样的:

  1. 系统拉起 UIAbility
  2. 创建一个 WindowStage
  3. 调用 onWindowStageCreate,你在这里 loadContent('pages/Index')
  4. ArkUI 开始渲染 Index 页面
  5. 窗口被销毁时,触发 onWindowStageDestroy

你可以把 WindowStage 理解为:
装 ArkUI 页面的那块玻璃窗”。


三、创建 Stage 应用示例:最小但完整的一条跑通链路

我们来做一个非常典型、但是真实可用的结构:
首页 Index → 详情页 Detail,带参数跳转。

3.1 目录结构示意

entry
 ├── src/main/ets
 │    ├── MainAbility
 │    │     └── MainAbility.ets
 │    └── pages
 │          ├── Index.ets
 │          └── Detail.ets
 └── resources
      └── ...

3.2 MainAbility:只负责场景 + 窗口 + 页面入口

// MainAbility/MainAbility.ets
import UIAbility from '@ohos.app.ability.UIAbility';

export default class MainAbility extends UIAbility {
  onCreate(want, launchParam) {
    console.info('[MainAbility] onCreate');
  }

  onWindowStageCreate(windowStage) {
    console.info('[MainAbility] onWindowStageCreate');

    windowStage.loadContent('pages/Index', (err) => {
      if (err) {
        console.error('Failed to load Index page: ' + JSON.stringify(err));
      } else {
        console.info('Index page loaded');
      }
    });
  }

  onForeground() {
    console.info('[MainAbility] onForeground');
  }

  onBackground() {
    console.info('[MainAbility] onBackground');
  }

  onDestroy() {
    console.info('[MainAbility] onDestroy');
  }
}

注意,这里完全不写 UI,也不做业务
它只负责:“这个窗口从哪一页开始演戏?” → Index


3.3 Index 页面:ArkUI 写 UI + 路由跳转

// pages/Index.ets
import router from '@ohos.router';

@Entry
@Component
struct Index {
  @State count: number = 0;

  build() {
    Column() {
      Text('Stage 模型 Demo 首页')
        .fontSize(22)
        .margin({ bottom: 20 })

      Text(`当前计数:${this.count}`)
        .fontSize(20)
        .margin({ bottom: 20 })

      Button('加 1')
        .margin({ bottom: 10 })
        .onClick(() => {
          this.count++;
        })

      Button('跳转到详情页')
        .onClick(() => {
          router.pushUrl({
            url: 'pages/Detail',
            params: {
              from: 'Index',
              currentCount: this.count
            }
          });
        })
    }
    .width('100%')
    .height('100%')
    .justifyContent(FlexAlign.Center)
    .alignItems(HorizontalAlign.Center)
  }
}

这里把 Stage 下 “页面就是组件” 的味道展示得很清楚:

  • @Entry + @Component 声明一个入口页面
  • 内部用 Column / Text / Button 写 UI
  • router.pushUrl 做页面跳转

3.4 Detail 页面:接参数 + 返回

// pages/Detail.ets
import router from '@ohos.router';

@Entry
@Component
struct Detail {
  @State fromPage: string = '';
  @State initCount: number = 0;

  aboutToAppear() {
    const params = router.getParams() as Record<string, Object>;
    this.fromPage = (params?.from as string) ?? 'Unknown';
    this.initCount = (params?.currentCount as number) ?? 0;
  }

  build() {
    Column() {
      Text('详情页')
        .fontSize(22)
        .margin({ bottom: 20 })

      Text(`来自页面:${this.fromPage}`)
        .fontSize(18)
        .margin({ bottom: 10 })

      Text(`进入详情时计数:${this.initCount}`)
        .fontSize(18)
        .margin({ bottom: 20 })

      Button('返回上一页')
        .onClick(() => {
          router.back();
        })
    }
    .width('100%')
    .height('100%')
    .justifyContent(FlexAlign.Center)
    .alignItems(HorizontalAlign.Center)
  }
}

到这一步:

  • Stage 模型的 UIAbility/WindowStage 流程
  • ArkUI 页面结构
  • 基础路由 + 参数传递

已经完整串起来了。


四、页面跳转与参数传递:router 的正确使用姿势

在 Stage 模型中,页面之间的跳转核心就是 @ohos.router

4.1 pushUrl:普通跳转(压栈)

router.pushUrl({
  url: 'pages/Detail'
});

带参数:

router.pushUrl({
  url: 'pages/Detail',
  params: {
    userId: 1001,
    from: 'Home'
  }
});

4.2 getParams:目标页面取参数

aboutToAppear() {
  const params = router.getParams() as Record<string, Object>;
  this.userId = params?.userId as number ?? 0;
}

常识但重要:

  • 不要在构造函数里拿参数,推荐 aboutToAppear / onPageShow 里获取
  • 大对象别直接塞 params,传 ID 就够了,详情页自己去查(可维护性 ↑)

4.3 back / getResult:带结果返回上一页

很多项目有这样的需求:

A 页面打开 B 页面,B 完成一个选择后把结果返回给 A。

例如:选择联系人

// B 页面:选择联系人后返回上一页
router.back({
  result: {
    selectedId: 9527,
    selectedName: '张三'
  }
});
// A 页面:在合适时机接收结果(如 onPageShow)
onPageShow() {
  router.getResult().then(res => {
    const r = res?.result as Record<string, Object> | undefined;
    if (r?.selectedId) {
      console.info(`选中 ID = ${r.selectedId}, name = ${r.selectedName}`);
    }
  });
}

这种写法比“搞全局单例、事件总线”那种野路子干净多了——逻辑跟路由栈绑定,数据流清晰。


五、与旧模型兼容注意事项:迁移时期最容易翻车的点

现实一点,说 Stage 的时候不可避免要面对“旧项目怎么办?”

5.1 生命周期对不上:不要机械照搬

旧模型里你可能写过:

// PageAbility
onStart() { ... }
onActive() { ... }
onInactive() { ... }
onStop() { ... }

Stage 下:

  • onCreate ≈ 初始化(但更偏“场景级单例设立”)
  • onForeground ≈ 相当于 “onActive” 的感觉
  • onBackground ≈ 接近 “onInactive / onStop 之前”
  • onDestroy:彻底退出场景

迁移时不要机械对号入座,而是要想:

  • 这段逻辑是“应用场景级”的?
  • 还是“窗口级”的?
  • 或者其实应该放到 ArkUI 页面 / ViewModel 里?

5.2 旧的 startAbility / terminateAbility 思路要改

旧模型很多地方用 Ability 跳 Ability 来当页面:

// 旧写法(示意)
this.context.startAbility({
  bundleName: 'xxx',
  abilityName: 'yyy',
  parameters: { ... }
});

Stage + ArkUI 之后,除非是跨应用 / 跨能力场景,绝大多数普通页面切换都应该:

  • 同一个 UIAbility 内完成
  • router.pushUrl / back 管理

能在 Stage 内解决的,就别再设计成“多 Ability 乱飞”的结构了。


5.3 UI 技术栈统一:不要 XML / JS UI / ArkUI 混在一锅

迁移过程中最容易犯的错之一:

新页面用 ArkUI,旧页面还用 XML / JS UI,而且还互相跳来跳去。

能做的最好选择是:

  • 新增页面统一用 ArkUI
  • 老页面能改就尽量逐步迁 ArkUI
  • 尽量避免一个业务流在多套 UI 技术间反复切换

你可以分期迁移,但目标应该明确指向“最终都在 ArkUI 阵营里”


5.4 多窗口、多设备能力:别再自己 hack

以前有些人为了模拟“多窗口”搞过很多骚操作,比如:

  • 一堆透明 Activity / Ability
  • 手动管理 Fake Window
  • 各种 offset 绕来绕去

Stage 在窗口这块设计得完备多了:

  • UIAbility 可以管理多个 WindowStage
  • 支持副窗口、浮窗、多设备协同等

迁移的时候,不妨把原来那些 hack 好好梳理一下,看看能不能直接靠 Stage 的多窗口能力重构掉,那种“技术债”能趁机还掉不少。


六、最佳实践与常见错误:真正在项目里“踩过后才会记住”的那种

6.1 最佳实践:UIAbility 聚焦“场景管理”,别乱伸手

UIAbility 建议只做这些事:

  • 初始化应用级资源(日志、网络、DI 容器、全局状态)
  • 管理 WindowStage / 多窗口
  • 处理全局入口(Deep Link、通知跳转、分布式启动等)

不要做这些事:

  • 在里面写具体页面的业务逻辑(下单、列表刷新、表单校验等)
  • 手动操作某个页面里的状态

你可以把 UIAbility 当成“App Shell”,而不是“大 Controller”。


6.2 最佳实践:划清“Ability / Page / ViewModel / Service”的边界

一个比较健康的分层可能是这样:

entry/src/main/ets
 ├── ability
 │     └── MainAbility.ets
 ├── pages
 │     ├── Home.ets
 │     ├── Detail.ets
 │     └── Settings.ets
 ├── viewmodel
 │     ├── HomeViewModel.ets
 │     ├── DetailViewModel.ets
 │     └── UserViewModel.ets
 └── services
       ├── httpClient.ets
       └── api.ts
  • Ability 层:场景入口 + WindowStage 管理
  • Page 层:UI + 用户交互(点击、滑动、路由)
  • ViewModel 层:状态管理 + 业务逻辑
  • Service 层:网络请求 / 本地存储 / 工具函数

长远看,这比“所有逻辑都糊在页面里”可维护太多。


6.3 最佳实践:路由参数「只传标识,不传大对象」

再次强调一遍这个原则,因为太重要:

  • ❌ 不建议把整个人对象、整条订单对象通过 params 丢到路由里
  • ✅ 更推荐只传 id / type / from 之类简单标识

这会极大降低页面间耦合度,也方便以后改字段、改结构。


6.4 常见错误:WindowStage 销毁时不解绑监听

典型场景:

  • onWindowStageCreate 注册了一堆监听,例:

    • 事件总线订阅
    • WebSocket / 长连接事件
    • 自定义全局消息

结果在 onWindowStageDestroy 什么也没干,窗口没了,监听还在,最终:

  • 多开几次页面,监听翻倍
  • 一次事件触发 N 遍回调
  • Debug 时莫名其妙的“重复请求”“重复弹窗”

建议:成对写法

onWindowStageCreate(windowStage) {
  this.subscribeId = globalEventBus.subscribe('xxx', this.handleXxx);
}

onWindowStageDestroy() {
  if (this.subscribeId) {
    globalEventBus.unsubscribe(this.subscribeId);
  }
}

6.5 常见错误:仍用旧思路“一个页面一个 Ability”

如果你还想在 Stage 下搞“十几个 UIAbility,每个对应一个页面”,那基本上就是没 get 到 Stage 的设计意义

更推荐的方式:

  • 一个应用 一个主 UIAbility(主流程)
  • 特殊场景(比如分享入口、某些独立模块)再单独建 UIAbility
  • 绝大部分页面用 router 在单一 UIAbility 内完成导航

6.6 常见错误:生命周期里做太重的事,导致启动慢

有人喜欢在 onCreate 里:

  • 初始化一堆 SDK
  • 同时发几组网络请求
  • 再读大量本地数据

结果就是:启动时间肉眼可见地慢

更好的做法:

  • onCreate 做必要的初始化即可
  • 重量级逻辑拆到业务首屏的 ViewModel 里按需加载
  • 一些非关键的耗时操作可以延后执行

Stage 给你的是更精细的生命周期,不是“你可以在 onCreate 里塞更多东西”😂。

如果觉得有帮助,别忘了点个赞+关注支持一下~
喜欢记得关注,别让好内容被埋没~

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐