都 2025 年了,你还没真正搞懂鸿蒙 Stage 模型?
先说点人话:如果你现在做鸿蒙应用开发,还只停留在PageAbility / ServiceAbility 那一套旧模型,那多半已经感觉到有点别扭了——代码越写越臃肿、生命周期越来越混乱,窗口逻辑、页面逻辑全揉在一起,一改就崩,一动就乱。而Stage 模型把过去“大杂烩式”的能力模型拆干净;更好地服务ArkUI 声明式 UI + 多窗口场景 + 更清晰架构。Stage vs Ability 模型差异
大家好,我是[晚风依旧似温柔],新人一枚,欢迎大家关注~
本文目录:
前言
先说点人话:如果你现在做鸿蒙应用开发,还只停留在 PageAbility / ServiceAbility 那一套旧模型,那多半已经感觉到有点别扭了——代码越写越臃肿、生命周期越来越混乱,窗口逻辑、页面逻辑全揉在一起,一改就崩,一动就乱。
而 Stage 模型 出来的目的,其实就两件事:
- 把过去“大杂烩式”的能力模型拆干净;
- 更好地服务 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 里的东西拆开了。
核心角色变成:
-
UIAbility
- 一个“应用场景”的入口
- 负责进程/场景生命周期
- 负责拿到
WindowStage,但不直接写 UI 布局
-
WindowStage
- 某个窗口的“舞台”
- 决定实际加载哪个 ArkUI 页面:
loadContent('pages/Index') - 管这个窗口级别的生命周期:创建 / 刷新 / 销毁
-
ArkUI Page(页面组件)
- 真正的 UI、交互、数据绑定都在这里
- 就是你写的
@Entry struct Index、Detail等组件
可以画成这样一条链:
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');
// 释放跟这个窗口强关联的资源:监听、定时、对象等
}
节奏是这样的:
- 系统拉起 UIAbility
- 创建一个 WindowStage
- 调用
onWindowStageCreate,你在这里loadContent('pages/Index') - ArkUI 开始渲染 Index 页面
- 窗口被销毁时,触发
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 里塞更多东西”😂。
如果觉得有帮助,别忘了点个赞+关注支持一下~
喜欢记得关注,别让好内容被埋没~
更多推荐



所有评论(0)