ArkTS Stage 模型总览:AbilityStage、UIAbility 与 ExtensionAbility 怎么分工

前两篇我们已经把 Stage 工程目录、首页加载、资源管理和页面跳转讲清楚了。从这一篇开始,重点放到 Stage 模型本身:哪些代码应该放在 AbilityStage,哪些代码应该放在 UIAbility,哪些场景应该使用 ExtensionAbility,后台任务又应该怎样设计。

本文的目标很明确:让读者看完后能把概念落到工程里,而不是只记住几个类名。

在这里插入图片描述

1. 本章先解决什么问题

理解 Stage 模型不是背类名,而是先把模块初始化、界面入口、扩展能力、页面渲染的职责边界分清楚。

如果你正在写 HarmonyOS 应用,可以先带着三个问题读本文:

  1. 这段代码应该放在入口、页面、服务层还是扩展能力里?
  2. 出问题时应该先查配置、生命周期、页面路由还是后台任务?
  3. 这个能力是否真的需要后台运行,还是可以交给前台页面或系统调度?

补充流程图:

理解:本章先解决什么问题

确定代码位置

编写最小示例

运行验证

沉淀到工程结构

补充代码:

export interface 本章先解决什么问题StageNode {
  name: string;
  owner: 'AbilityStage' | 'UIAbility' | 'ExtensionAbility' | 'Page' | 'Service';
  responsibility: string;
}

export const 本章先解决什么问题Node: 本章先解决什么问题StageNode = {
  name: '本章先解决什么问题',
  owner: 'Service',
  responsibility: '把本小节的概念落到可复用工程代码中'
};

代码解释:

  1. 流程图先把本小节从概念、代码位置、实现、验证串起来,读者不会只看到一段抽象描述。
  2. 示例代码给出一个可以迁移到 Stage 工程中的最小写法。
  3. 这段代码把小节概念转成一个职责节点。写 Stage 工程时,先明确 owner,再决定代码放在哪个目录,能减少职责混乱。

2. 为什么要先学 Stage 模型分工

很多入门问题都来自职责放错位置:把初始化写进页面、把页面逻辑写进 Ability、把后台逻辑当成普通页面逻辑处理。Stage 模型的价值,就是让工程入口、页面入口和扩展能力各自承担清晰职责。

补充流程图:

理解:为什么要先学 Stage 模型分工

确定代码位置

编写最小示例

运行验证

沉淀到工程结构

补充代码:

export interface 为什么要先学_Stage_模型分工StageNode {
  name: string;
  owner: 'AbilityStage' | 'UIAbility' | 'ExtensionAbility' | 'Page' | 'Service';
  responsibility: string;
}

export const 为什么要先学_Stage_模型分工Node: 为什么要先学_Stage_模型分工StageNode = {
  name: '为什么要先学 Stage 模型分工',
  owner: 'Service',
  responsibility: '把本小节的概念落到可复用工程代码中'
};

代码解释:

  1. 流程图先把本小节从概念、代码位置、实现、验证串起来,读者不会只看到一段抽象描述。
  2. 示例代码给出一个可以迁移到 Stage 工程中的最小写法。
  3. 这段代码把小节概念转成一个职责节点。写 Stage 工程时,先明确 owner,再决定代码放在哪个目录,能减少职责混乱。

3. AbilityStage 负责模块级初始化

AbilityStage 跟具体页面无关,它更适合做模块级准备工作,例如读取模块配置、初始化日志、准备全局服务、注册必要的监听。它不应该承载页面跳转、按钮点击这类 UI 逻辑。

补充流程图:

理解:AbilityStage 负责模块级初始化

确定代码位置

编写最小示例

运行验证

沉淀到工程结构

补充代码:

export class AbilityStage_负责模块级Initializer {
  private static ready: boolean = false;

  static init(): void {
    if (AbilityStage_负责模块级Initializer.ready) {
      return;
    }
    AppRuntimeConfig.set('AbilityStage_负责模块级', 'enabled');
    AbilityStage_负责模块级Initializer.ready = true;
  }
}

class AppRuntimeConfig {
  private static values: Record<string, string> = {};

  static set(key: string, value: string): void {
    AppRuntimeConfig.values[key] = value;
  }
}

代码解释:

  1. 流程图先把本小节从概念、代码位置、实现、验证串起来,读者不会只看到一段抽象描述。
  2. 示例代码给出一个可以迁移到 Stage 工程中的最小写法。
  3. 这段代码演示模块初始化要具备幂等能力。AbilityStage 可能成为多个初始化逻辑的入口,重复初始化会导致监听重复注册或配置被覆盖。

4. UIAbility 负责界面能力入口

UIAbility 是用户能看到界面的 Ability。它负责生命周期、窗口创建、页面加载、前后台切换等工作。一个常见的 EntryAbility 会在 onWindowStageCreate 中调用 loadContent 加载首页。

补充流程图:

理解:UIAbility 负责界面能力入口

确定代码位置

编写最小示例

运行验证

沉淀到工程结构

补充代码:

export interface UIAbility_负责界面能力入口StageNode {
  name: string;
  owner: 'AbilityStage' | 'UIAbility' | 'ExtensionAbility' | 'Page' | 'Service';
  responsibility: string;
}

export const UIAbility_负责界面能力入口Node: UIAbility_负责界面能力入口StageNode = {
  name: 'UIAbility 负责界面能力入口',
  owner: 'Service',
  responsibility: '把本小节的概念落到可复用工程代码中'
};

代码解释:

  1. 流程图先把本小节从概念、代码位置、实现、验证串起来,读者不会只看到一段抽象描述。
  2. 示例代码给出一个可以迁移到 Stage 工程中的最小写法。
  3. 这段代码把小节概念转成一个职责节点。写 Stage 工程时,先明确 owner,再决定代码放在哪个目录,能减少职责混乱。

5. ExtensionAbility 负责非页面扩展能力

ExtensionAbility 不是用来画普通页面的,它面向系统扩展入口,例如服务、卡片、分享等场景。涉及后台或系统入口时,要先判断是否应该使用扩展能力,而不是强行让 UIAbility 承担。

补充流程图:

理解:ExtensionAbility 负责非页面扩展能力

确定代码位置

编写最小示例

运行验证

沉淀到工程结构

补充代码:

interface ExtensionRequest {
  type: 'service' | 'form' | 'share';
  payload: Record<string, string>;
}

export class ExtensionAbility_负ExtensionRouter {
  static route(request: ExtensionRequest): string {
    if (request.type === 'service') {
      return 'handle service request';
    }
    if (request.type === 'form') {
      return 'update form content';
    }
    return 'handle share content';
  }
}

代码解释:

  1. 流程图先把本小节从概念、代码位置、实现、验证串起来,读者不会只看到一段抽象描述。
  2. 示例代码给出一个可以迁移到 Stage 工程中的最小写法。
  3. 这段代码把不同扩展入口先统一成请求对象,再按 type 分发。这样可以避免把服务、卡片、分享逻辑全部写在一个生命周期回调里。

6. ArkUI 页面负责界面表达

ArkUI 页面应该聚焦布局、状态、用户交互和页面内业务编排。页面可以调用服务层,但不建议直接承担应用启动、模块初始化或后台任务调度。

补充流程图:

理解:ArkUI 页面负责界面表达

确定代码位置

编写最小示例

运行验证

沉淀到工程结构

补充代码:

export interface ArkUI_页面负责界面表达StageNode {
  name: string;
  owner: 'AbilityStage' | 'UIAbility' | 'ExtensionAbility' | 'Page' | 'Service';
  responsibility: string;
}

export const ArkUI_页面负责界面表达Node: ArkUI_页面负责界面表达StageNode = {
  name: 'ArkUI 页面负责界面表达',
  owner: 'Service',
  responsibility: '把本小节的概念落到可复用工程代码中'
};

代码解释:

  1. 流程图先把本小节从概念、代码位置、实现、验证串起来,读者不会只看到一段抽象描述。
  2. 示例代码给出一个可以迁移到 Stage 工程中的最小写法。
  3. 这段代码把小节概念转成一个职责节点。写 Stage 工程时,先明确 owner,再决定代码放在哪个目录,能减少职责混乱。

7. 工程结构建议

建议把 Stage 相关代码按职责分层,不要把所有逻辑写在一个文件里。

entry/src/main/ets
├── entryability
├── entryabilitystage
├── extensionability
├── pages
├── components
├── services
├── models
└── utils

目录解释:

  1. entryability 放 UIAbility 入口。
  2. entryabilitystage 放模块级初始化。
  3. extensionability 放服务、卡片、分享等扩展能力。
  4. pages 放 ArkUI 页面。
  5. services 放可复用业务服务。

8. 从 Demo 到项目时怎么拆

官方示例通常为了让读者快速跑通,会把代码写得比较集中。真实项目不能一直停留在 Demo 写法,否则页面一多就会出现三个问题:入口文件越来越大、页面事件越来越重、后台和前台逻辑互相影响。

更推荐的拆法是:

  1. AbilityStage 只处理模块级初始化,例如日志、配置、轻量服务注册。
  2. UIAbility 只处理界面入口,例如生命周期、窗口创建、首页加载、Want 参数接收。
  3. ExtensionAbility 只处理系统扩展入口,例如服务、卡片、分享等非普通页面场景。
  4. pages 只写页面状态和交互,不直接堆复杂业务。
  5. services 承担可复用业务,例如任务调度、缓存、配置读取、数据请求。

一个判断标准很实用:如果这段代码离开当前页面后仍然有价值,就不要写死在页面里;如果这段代码只和启动入口有关,就不要放进组件里;如果这段代码需要系统以扩展方式调用,就不要强行放进 UIAbility。

补充流程图:

理解:从 Demo 到项目时怎么拆

确定代码位置

编写最小示例

运行验证

沉淀到工程结构

补充代码:

export interface 从_Demo_到项目时怎么拆StageNode {
  name: string;
  owner: 'AbilityStage' | 'UIAbility' | 'ExtensionAbility' | 'Page' | 'Service';
  responsibility: string;
}

export const 从_Demo_到项目时怎么拆Node: 从_Demo_到项目时怎么拆StageNode = {
  name: '从 Demo 到项目时怎么拆',
  owner: 'Service',
  responsibility: '把本小节的概念落到可复用工程代码中'
};

代码解释:

  1. 流程图先把本小节从概念、代码位置、实现、验证串起来,读者不会只看到一段抽象描述。
  2. 示例代码给出一个可以迁移到 Stage 工程中的最小写法。
  3. 这段代码把小节概念转成一个职责节点。写 Stage 工程时,先明确 owner,再决定代码放在哪个目录,能减少职责混乱。

9. 实战代码

module.json5 中声明 UIAbility

{
  "module": {
    "name": "entry",
    "type": "entry",
    "mainElement": "EntryAbility",
    "abilities": [
      {
        "name": "EntryAbility",
        "srcEntry": "./ets/entryability/EntryAbility.ets",
        "exported": true
      }
    ]
  }
}

代码解释:

  1. 这段代码对应本节的最小可运行思路。
  2. 重点不是复制粘贴,而是理解它放在 Stage 工程中的位置。
  3. 这段配置告诉系统:entry 模块的主入口是 EntryAbility,源码位置在 entryability 目录下。

EntryAbility 加载首页

import { UIAbility } from '@kit.AbilityKit';
import { window } from '@kit.ArkUI';

export default class EntryAbility extends UIAbility {
  onWindowStageCreate(windowStage: window.WindowStage): void {
    windowStage.loadContent('pages/Index');
  }
}

代码解释:

  1. 这段代码对应本节的最小可运行思路。
  2. 重点不是复制粘贴,而是理解它放在 Stage 工程中的位置。
  3. UIAbility 不直接写页面布局,而是通过 WindowStage 加载 ArkUI 页面。

页面只负责 UI 结构

@Entry
@Component
struct Index {
  build() {
    Column({ space: 16 }) {
      Text('Stage 模型分工')
        .fontSize(28)
        .fontWeight(FontWeight.Bold)
      Text('Ability 负责入口,页面负责展示。')
        .fontSize(16)
    }
    .width('100%')
    .height('100%')
    .justifyContent(FlexAlign.Center)
  }
}

代码解释:

  1. 这段代码对应本节的最小可运行思路。
  2. 重点不是复制粘贴,而是理解它放在 Stage 工程中的位置。
  3. 页面代码保持纯粹,后续维护成本会低很多。

10. 实战案例:做一个学习任务入口

下面用一个小案例把本章主题落到工程里。假设应用首页有一个“开始学习”按钮,点击后进入某个学习任务。页面只负责触发动作,任务参数和任务状态交给服务层维护。

export interface StudyTask {
  id: string;
  title: string;
  source: string;
  createdAt: number;
}

export class StudyTaskService {
  private static currentTask?: StudyTask;

  static create(title: string, source: string): StudyTask {
    const task: StudyTask = {
      id: `${Date.now()}`,
      title,
      source,
      createdAt: Date.now()
    };
    StudyTaskService.currentTask = task;
    return task;
  }

  static getCurrent(): StudyTask | undefined {
    return StudyTaskService.currentTask;
  }
}

代码解释:

  1. 页面不直接拼业务对象,而是调用服务层创建任务。
  2. StudyTask 明确了任务字段,后续传参、缓存、日志都会更清楚。
  3. 服务层可以继续扩展成持久化版本,不影响页面结构。

页面使用方式如下:

@Entry
@Component
struct Index {
  @State latestTitle: string = '暂无任务';

  build() {
    Column({ space: 16 }) {
      Text('Stage 学习任务')
        .fontSize(28)
        .fontWeight(FontWeight.Bold)

      Text(this.latestTitle)
        .fontSize(16)
        .fontColor('#5A6B7B')

      Button('开始学习')
        .height(48)
        .onClick(() => {
          const task = StudyTaskService.create('Stage 模型实战', 'home');
          this.latestTitle = task.title;
        })
    }
    .width('100%')
    .height('100%')
    .justifyContent(FlexAlign.Center)
  }
}

这段页面代码只做三件事:显示标题、响应点击、更新 UI。真正的任务对象由服务层创建,这就是从 Demo 走向工程化的第一步。

11. 常见问题

11.1 代码写了但没有生效

先检查配置文件是否声明了对应入口,再检查路径大小写是否一致。Stage 工程里很多问题不是代码逻辑错,而是配置没有把类和系统入口连接起来。

11.2 页面和 Ability 职责混在一起

页面负责展示和交互,Ability 负责入口和生命周期。业务逻辑如果多个页面都要用,应该沉到 services 层。

11.3 后台能力被系统限制

后台任务不能按桌面端思路设计。要先判断任务是否必须立即执行、用户是否能感知、是否可以延迟,再选择对应方案。

11.4 生命周期日志看不懂

建议给每个入口统一 tag,例如 EntryAbilityAbilityStageTaskService。调试时按 tag 过滤日志,先看入口是否执行,再看页面是否加载,最后看业务服务是否被调用。

11.5 页面能打开但状态不对

优先检查状态是否应该放在页面中。如果状态需要跨页面共享,就放进服务层或持久化存储;如果状态只影响当前页面,再使用 @State 管理。

补充流程图:

理解:常见问题

确定代码位置

编写最小示例

运行验证

沉淀到工程结构

补充代码:

export interface 常见问题StageNode {
  name: string;
  owner: 'AbilityStage' | 'UIAbility' | 'ExtensionAbility' | 'Page' | 'Service';
  responsibility: string;
}

export const 常见问题Node: 常见问题StageNode = {
  name: '常见问题',
  owner: 'Service',
  responsibility: '把本小节的概念落到可复用工程代码中'
};

代码解释:

  1. 流程图先把本小节从概念、代码位置、实现、验证串起来,读者不会只看到一段抽象描述。
  2. 示例代码给出一个可以迁移到 Stage 工程中的最小写法。
  3. 这段代码把小节概念转成一个职责节点。写 Stage 工程时,先明确 owner,再决定代码放在哪个目录,能减少职责混乱。

12. 运行验证清单

  1. 修改 module.json5 后重新构建安装。
  2. 给关键生命周期加 hilog
  3. 页面路径统一使用 pages/页面名,不要写 .ets 后缀。
  4. 新增页面后同步更新 main_pages.json
  5. 后台任务要记录任务状态,避免中断后无法恢复。
  6. 把可复用逻辑放进 services,不要散落在页面事件里。

补充流程图:

理解:运行验证清单

确定代码位置

编写最小示例

运行验证

沉淀到工程结构

补充代码:

export interface 运行验证清单StageNode {
  name: string;
  owner: 'AbilityStage' | 'UIAbility' | 'ExtensionAbility' | 'Page' | 'Service';
  responsibility: string;
}

export const 运行验证清单Node: 运行验证清单StageNode = {
  name: '运行验证清单',
  owner: 'Service',
  responsibility: '把本小节的概念落到可复用工程代码中'
};

代码解释:

  1. 流程图先把本小节从概念、代码位置、实现、验证串起来,读者不会只看到一段抽象描述。
  2. 示例代码给出一个可以迁移到 Stage 工程中的最小写法。
  3. 这段代码把小节概念转成一个职责节点。写 Stage 工程时,先明确 owner,再决定代码放在哪个目录,能减少职责混乱。

13. 读者练习

建议你按下面步骤自己做一遍:

  1. 在现有 Stage 工程里新建一个 services 目录。
  2. 把页面里的任务创建逻辑移动到 StudyTaskService
  3. 在 Ability 生命周期里加入 hilog
  4. 新增一个页面,验证页面跳转和服务层状态是否正常。
  5. 把可复用文字移动到 string.json
  6. 把可复用颜色移动到 color.json
  7. 重新运行应用,确认入口、页面、服务层职责清晰。

如果这 7 步都能跑通,说明你已经不是只会改默认模板,而是开始按工程方式组织 Stage 应用了。

补充流程图:

理解:读者练习

确定代码位置

编写最小示例

运行验证

沉淀到工程结构

补充代码:

export interface 读者练习StageNode {
  name: string;
  owner: 'AbilityStage' | 'UIAbility' | 'ExtensionAbility' | 'Page' | 'Service';
  responsibility: string;
}

export const 读者练习Node: 读者练习StageNode = {
  name: '读者练习',
  owner: 'Service',
  responsibility: '把本小节的概念落到可复用工程代码中'
};

代码解释:

  1. 流程图先把本小节从概念、代码位置、实现、验证串起来,读者不会只看到一段抽象描述。
  2. 示例代码给出一个可以迁移到 Stage 工程中的最小写法。
  3. 这段代码把小节概念转成一个职责节点。写 Stage 工程时,先明确 owner,再决定代码放在哪个目录,能减少职责混乱。

14. 本章总结

理解 Stage 模型不是背类名,而是先把模块初始化、界面入口、扩展能力、页面渲染的职责边界分清楚。 真正写项目时,不要先问“这个 API 怎么调”,而要先问“这个能力应该属于哪一层”。职责边界清楚,Stage 工程才会越写越稳。

参考资料

  1. 华为开发者文档:https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/stage-model-development-overview
  2. 华为开发者文档:https://developer.huawei.com/consumer/cn/doc/harmonyos-faqs/faqs-background-tasks-1
  3. 华为开发者文档:https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/start-with-ets-stage
    补充流程图:

理解:本章总结

确定代码位置

编写最小示例

运行验证

沉淀到工程结构

补充代码:

export interface 本章总结StageNode {
  name: string;
  owner: 'AbilityStage' | 'UIAbility' | 'ExtensionAbility' | 'Page' | 'Service';
  responsibility: string;
}

export const 本章总结Node: 本章总结StageNode = {
  name: '本章总结',
  owner: 'Service',
  responsibility: '把本小节的概念落到可复用工程代码中'
};

代码解释:

  1. 流程图先把本小节从概念、代码位置、实现、验证串起来,读者不会只看到一段抽象描述。
  2. 示例代码给出一个可以迁移到 Stage 工程中的最小写法。
  3. 这段代码把小节概念转成一个职责节点。写 Stage 工程时,先明确 owner,再决定代码放在哪个目录,能减少职责混乱。
Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐