ExtensionAbility 入门:服务、卡片、分享与后台能力边界
前两篇我们已经把 Stage 工程目录、首页加载、资源管理和页面跳转讲清楚了。从这一篇开始,重点放到 Stage 模型本身:哪些代码应该放在 AbilityStage,哪些代码应该放在 UIAbility,哪些场景应该使用 ExtensionAbility,后台任务又应该如何设计。
本文的目标很明确:让读者看完后能把概念落到工程里,而不是只记住几个类名。

1. 本章先解决什么问题
ExtensionAbility 面向非页面扩展场景。理解它的边界,才能避免把后台服务、卡片和分享入口都塞进 UIAbility。
如果你正在写 HarmonyOS 应用,可以先带着三个问题读本文:
- 这段代码应该放在入口、页面、服务层还是扩展能力中?
- 出问题时应该先检查配置、生命周期、页面路由还是后台任务?
- 这个能力是否真的需要后台运行,还是可以交给前台页面或系统调度?
补充流程图:
补充代码:
interface ExtensionRequest {
type: 'service' | 'form' | 'share';
payload: Record<string, string>;
}
export class 本章先解决什么问题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';
}
}
代码解释:
- 流程图将本小节从概念、代码位置、实现到验证串联起来,读者不会只看到一段抽象描述。
- 示例代码给出了一个可以迁移到 Stage 工程中的最小实现。
- 这段代码把不同扩展入口先统一成请求对象,再按 type 分发。这样可以避免把服务、卡片、分享逻辑全部写在一个生命周期回调里。
2. ExtensionAbility 解决什么问题
应用不只有前台页面,还可能需要服务化能力、卡片能力、分享入口等能力。这些场景通常不是普通页面,而是系统的扩展入口。
补充流程图:
补充代码:
interface ExtensionRequest {
type: 'service' | 'form' | 'share';
payload: Record<string, string>;
}
export class ExtensionAbilityRouter {
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';
}
}
代码解释:
- 流程图将本小节从概念、代码位置、实现、验证串联起来,使读者不会只看到一段抽象描述。
- 示例代码给出了一个可以迁移到 Stage 工程中的最小实现。
- 这段代码把不同扩展入口先统一成请求对象,再按 type 分发。这样可以避免把服务、卡片、分享逻辑全部写在一个生命周期回调里。
3. ServiceExtensionAbility 的定位
服务扩展适合处理与服务相关的交互,但不能将其视为无限的后台线程。其后台能力受系统策略和场景约束。
补充流程图:
补充代码:
interface ExtensionRequest {
type: 'service' | 'form' | 'share';
payload: Record<string, string>;
}
export class ServiceExtensionAbExtensionRouter {
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';
}
}
代码解释:
- 流程图将本小节从概念、代码位置、实现到验证串联起来,使读者不会只看到一段抽象描述。
- 示例代码提供了一个可迁移到 Stage 工程中的最小化实现。
- 这段代码将不同的扩展入口统一为请求对象,再按 type 进行分发。这样可以避免把服务、卡片、分享逻辑全部写在一个生命周期回调里。
4. FormExtensionAbility 的定位
卡片能力面向桌面或系统卡片展示,重点是轻量展示和刷新,不适合承载复杂页面逻辑。
补充流程图:
补充代码:
interface ExtensionRequest {
type: 'service' | 'form' | 'share';
payload: Record<string, string>;
}
export class FormExtensionAbilityRouter {
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';
}
}
代码说明:
- 流程图将本小节从概念、代码位置、实现到验证串联起来,避免读者只看到一段抽象描述。
- 示例代码提供了一个可迁移到 Stage 模型工程中的最小化写法。
- 这段代码将不同的扩展入口统一为请求对象,再根据 type 进行分发。这样可以避免将服务、卡片、分享的逻辑全部写在一个生命周期回调中。
5. ShareExtensionAbility 的定位
分享扩展适用于接收外部分享内容,并将内容转交给应用内部处理流程。
补充流程图:
补充代码:
interface ExtensionRequest {
type: 'service' | 'form' | 'share';
payload: Record<string, string>;
}
export class ShareExtensionAbilExtensionRouter {
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';
}
}
代码解释:
- 流程图将本小节从概念、代码位置、实现、验证串联起来,使读者不会只看到一段抽象描述。
- 示例代码给出了一个可以迁移到 Stage 工程中的最小实现。
- 这段代码将不同扩展入口先统一为请求对象,再按 type 分发。这样可以避免将服务、卡片、分享逻辑全部写在一个生命周期回调里。
6. 先判断场景,再选择能力
如果用户正在浏览页面,使用 UIAbility;如果是系统扩展入口,则考虑使用对应的 ExtensionAbility。
补充流程图:
补充代码:
interface ExtensionRequest {
type: 'service' | 'form' | 'share';
payload: Record<string, string>;
}
export class 先判断场景_再选择能力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';
}
}
代码解释:
- 流程图将本小节从概念、代码位置、实现到验证串联起来,使读者不会只看到一段抽象描述。
- 示例代码提供了一个可以迁移到 Stage 模型工程中的最小化写法。
- 这段代码将不同的扩展入口统一为请求对象,再根据 type 进行分发。这样可以避免将服务、卡片、分享等逻辑全部写在一个生命周期回调中。
7. 工程结构建议
建议把 Stage 相关代码按职责分层,不要把所有逻辑写在一个文件里。
entry/src/main/ets
├── entryability
├── entryabilitystage
├── extensionability
├── pages
├── components
├── services
├── models
└── utils
目录解释:
entryability放 UIAbility 入口。entryabilitystage放模块级初始化。extensionability放服务、卡片、分享等扩展能力。pages放 ArkUI 页面。services放可复用业务服务。
8. 从 Demo 到项目时怎么拆
官方示例通常为了让读者快速跑通,会把代码写得比较集中。真实项目不能一直停留在 Demo 写法,否则页面一多就会出现三个问题:入口文件越来越大、页面事件处理越来越重、后台和前台逻辑互相影响。
更推荐的拆法是:
AbilityStage只处理模块级初始化,例如日志、配置、轻量服务注册。UIAbility只处理界面入口,例如生命周期、窗口创建、首页加载、Want 参数接收。ExtensionAbility只处理系统扩展入口,例如服务、卡片、分享等非普通页面场景。pages只写页面状态和交互,不直接堆复杂业务。services承担可复用业务,例如任务调度、缓存、配置读取、数据请求。
一个实用的判断标准是:如果这段代码离开当前页面后仍然有价值,就不要写死在页面里;如果这段代码只和启动入口有关,就不要放进组件里;如果这段代码需要系统以扩展方式调用,就不要强行放进 UIAbility。
补充流程图:
补充代码:
interface ExtensionRequest {
type: 'service' | 'form' | 'share';
payload: Record<string, string>;
}
export class 从_Demo_到项目时怎么拆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';
}
}
代码解释:
- 流程图将本小节从概念、代码位置、实现到验证串联起来,避免读者只看到一段抽象描述。
- 示例代码给出了一个可以迁移到 Stage 工程中的最小实现。
- 这段代码把不同扩展入口先统一成请求对象,再按 type 分发。这样可以避免把服务、卡片、分享逻辑全部写在一个生命周期回调里。
9. 实战代码
ServiceExtensionAbility 示例
import { ServiceExtensionAbility, Want } from '@kit.AbilityKit';
import { hilog } from '@kit.PerformanceAnalysisKit';
export default class DemoServiceExtension extends ServiceExtensionAbility {
onCreate(want: Want): void {
hilog.info(0x0000, 'DemoService', 'service extension create');
}
onRequest(want: Want, startId: number): void {
const taskId = want.parameters?.taskId as string ?? '';
hilog.info(0x0000, 'DemoService', 'taskId=%{public}s startId=%{public}d', taskId, startId);
}
onDestroy(): void {
hilog.info(0x0000, 'DemoService', 'service extension destroy');
}
}
代码解释:
- 这段代码对应本节的最小可运行示例。
- 重点不是复制粘贴,而是理解它放在 Stage 工程中的位置。
- 扩展能力需要关注生命周期和请求参数,不要假设它会一直运行。
module.json5 声明扩展能力
{
"extensionAbilities": [
{
"name": "DemoServiceExtension",
"srcEntry": "./ets/extensionability/DemoServiceExtension.ets",
"type": "service",
"exported": false
}
]
}
代码解释:
- 这段代码对应本节的最小可运行示例。
- 重点不是复制粘贴,而是理解它放在 Stage 工程中的位置。
- 扩展能力需要在模块配置中声明,type 要和实际场景对应。
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;
}
}
代码解释:
- 页面不直接构造业务对象,而是调用服务层创建任务。
StudyTask明确了任务字段,后续传参、缓存和日志都会更清晰。- 服务层可扩展为持久化版本,不影响页面结构。
页面使用方式如下:
@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,例如 EntryAbility、AbilityStage、TaskService。调试时按 tag 过滤日志,先看入口是否执行,再看页面是否加载,最后看业务服务是否被调用。
11.5 页面能打开但状态不对
优先检查状态是否应该放在页面中。如果状态需要跨页面共享,就放进服务层或持久化存储;如果状态只影响当前页面,再使用 @State 管理。
补充流程图:
补充代码:
interface ExtensionRequest {
type: 'service' | 'form' | 'share';
payload: Record<string, string>;
}
export class 常见问题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';
}
}
代码解释:
- 流程图将本小节从概念、代码位置、实现到验证串联起来,避免读者只看到一段抽象描述。
- 示例代码提供了一个可以迁移到 Stage 工程中的最小实现。
- 这段代码将不同的扩展入口统一为请求对象,再按 type 进行分发。这样可以避免将服务、卡片、分享逻辑全部写在一个生命周期回调中。
12. 运行验证清单
- 修改
module.json5后重新构建安装。 - 给关键生命周期加
hilog。 - 页面路径统一使用
pages/页面名,不要写.ets后缀。 - 新增页面后同步更新
main_pages.json。 - 后台任务应记录任务状态,避免中断后无法恢复。
- 将可复用逻辑放入
services,避免散落在页面事件中。
补充流程图:
补充代码:
interface ExtensionRequest {
type: 'service' | 'form' | 'share';
payload: Record<string, string>;
}
export class 运行验证清单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';
}
}
代码解释:
- 流程图将本小节从概念、代码位置、实现到验证串联起来,使读者不会只看到一段抽象描述。
- 示例代码提供了一个可迁移到 Stage 工程中的最小实现。
- 流程图将本小节从概念、代码位置、实现到验证串联起来,使读者不会只看到一段抽象描述。
13. 读者练习
建议你按下面步骤自己做一遍:
- 在现有 Stage 工程里新建一个
services目录。 - 把页面里的任务创建逻辑移动到
StudyTaskService。 - 在 Ability 生命周期里加入日志记录。
- 新增一个页面,验证页面跳转和服务层状态是否正常。
- 把可复用文字移动到
string.json。 - 把可复用颜色移动到
color.json。 - 重新运行应用,确认入口、页面、服务层职责清晰。
如果这 7 步都能顺利完成,说明你已经不是只会修改默认模板,而是开始按照工程化的方式组织 Stage 应用了。
补充流程图:
补充代码:
interface ExtensionRequest {
type: 'service' | 'form' | 'share';
payload: Record<string, string>;
}
export class 读者练习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';
}
}
代码解释:
- 流程图将本小节从概念、代码定位、实现到验证串联起来,使读者不会只看到一段抽象的描述。
- 示例代码给出了一个可以迁移到 Stage 工程中的最小实现示例。
- 这段代码将不同的扩展入口统一为请求对象,再根据类型进行分发。这样可以避免将服务、卡片、分享等逻辑全部写在一个生命周期回调函数中。
14. 本章总结
ExtensionAbility 面向非页面扩展场景。理解它的边界,才能避免把后台服务、卡片和分享入口都塞进 UIAbility。真正写项目时,不要先问“这个 API 怎么调”,而要先问“这个能力应该属于哪一层”。职责边界清晰,Stage 工程才会越写越稳。
参考资料
- 华为开发者文档:https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/stage-model-development-overview
- 华为开发者文档:https://developer.huawei.com/consumer/cn/doc/harmonyos-faqs/faqs-background-tasks-1
- 华为开发者文档:https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/start-with-ets-stage
补充流程图:
补充代码:
interface ExtensionRequest {
type: 'service' | 'form' | 'share';
payload: Record<string, string>;
}
export class 本章总结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';
}
}
代码解释:
- 流程图将本小节从概念、代码位置、实现到验证串联起来,使读者不会只看到一段抽象描述。
- 示例代码给出了一个可以迁移到 Stage 工程中的最小实现。
- 这段代码将不同的扩展入口统一为请求对象,再按 type 进行分发。这样可以避免将服务、卡片、分享逻辑全部写在一个生命周期回调中。
1280

被折叠的 条评论
为什么被折叠?



