1. 学习目标

完成本章后,你应当能够:

  1. 准确解释元服务的定义、特点和典型使用场景。
  2. 理解元服务卡片与完整应用页面的分工。
  3. 说明 HarmonyOS NEXT 中 Feature Ability 及扩展能力的基本关系。
  4. 在 module.json5 中识别和配置元服务相关的 extensionAbilities。
  5. 在 resources/base/profile/ 下创建和维护卡片配置文件。
  6. 理解卡片尺寸、布局、刷新周期和元数据之间的关系。
  7. 使用 Column、Row、Stack 和基础组件构建大棚监测卡片。
  8. 使用 formProvider 或回调机制更新卡片显示内容。
  9. 设计卡片首次加载、定时刷新、被动刷新和失败恢复流程。
  10. 处理卡片点击事件,跳转到元服务中的完整页面。
  11. 模拟获取温度、湿度、光照和设备状态等动态数据。
  12. 完成元服务的编译、签名、打包、安装和发布准备。
  13. 记录开发过程中的问题、证据、解决方法和验证结果。
  14. 在技术向善、数据安全、设备安全和职业责任方面作出正确判断。

2.元服务定义与设计思想

2.1 什么是元服务

元服务是一种轻量、便捷、强调即时触达的应用服务形态。它通常围绕一个明确任务提供能力,用户可以通过卡片、搜索、系统入口或其他服务入口快速使用。
典型特点:
• 免安装或轻量使用;
• 服务功能相对聚焦;
• 访问路径短;
• 可以通过卡片直接展示信息;
• 可以从卡片跳转到更完整的元服务页面;
• 适合高频查看、快捷操作和状态监控。
例如智慧农业场景中的元服务可以提供:
查看大棚环境摘要
查看离线设备
查看今日告警
查看水泵运行状态
查看传感器最近更新时间
元服务并不意味着业务简单,而是把复杂系统中最常用、最适合即时触达的功能提取出来。

2.2 元服务与完整应用页面的分工

在这里插入图片描述
卡片中不适合放置复杂表单、长篇说明和高风险控制流程。卡片应负责:

快速知道发生了什么
快速决定是否需要进一步操作
快速进入完整页面

2.3 卡片信息设计

大棚卡片的核心信息可以是:

大棚一号
温度:26.4℃
湿度:68%
光照:正常
告警:1 条
更新于:08:30

设计时要回答:

  1. 用户打开卡片最关心什么?
  2. 哪些数据必须实时或准实时?
  3. 哪些数据可以点击后查看?
  4. 数据过期时如何标记?
  5. 网络失败时是否保留上次成功数据?
  6. 卡片尺寸变化后哪些信息需要隐藏?

2.4 技术向善

“技术向善”要求开发者关注技术对人、环境和社会的影响。农业元服务尤其要注意:
• 错误数据可能导致错误灌溉或通风决策;
• 告警缺失可能影响作物和设备安全;
• 设备状态延迟不能被当作实时事实;
• 位置、产量和生产数据可能属于敏感业务数据;
• 远程控制不能只追求方便而忽略安全确认;
• 系统应明确说明数据来源、更新时间和异常情况。

3.HarmonyOS NEXT Feature Ability 整体架构

本节用于帮助学生建立整体架构认识。不同 HarmonyOS NEXT SDK 版本可能调整配置字段、能力名称和生命周期接口,实际开发应以当前 DevEco Studio 工程模板及官方 SDK 定义为准。

3.1 从应用到卡片的层次

可以把一个元服务工程理解为:

应用(Application)
   │
   ├── 模块(Module)
   │      ├── 页面能力(Ability)
   │      ├── 扩展能力(ExtensionAbility)
   │      │       └── 元服务卡片能力
   │      └── 资源和配置
   │
   ├── 业务服务层
   ├── 数据访问层
   └── UI 页面与卡片
  1. 应用层
    表示一个完整产品或应用身份,包含名称、版本、签名和发布信息。
  2. 模块层
    模块是应用中的可构建单元。一个项目可以包含入口模块、功能模块或其他扩展模块。
  3. Ability
    Ability 表示应用可以向系统提供的基本能力入口。页面、路由和用户任务通常在 Ability 中组织。
  4. ExtensionAbility
    ExtensionAbility 表示向系统扩展的能力入口。元服务卡片、后台服务或其他系统扩展能力可以通过相应的扩展能力接入。
  5. 卡片层
    卡片负责以受限空间呈现信息,并按照系统允许的生命周期和刷新机制提供服务。

3.2 用户访问卡片的路径

用户在桌面添加卡片
        ↓
系统根据卡片配置创建卡片实例
        ↓
卡片扩展能力准备数据
        ↓
卡片 UI 显示摘要
        ↓
用户点击卡片
        ↓
跳转到元服务或完整页面

卡片本身不一定长期运行一个完整应用页面,而是通过系统提供的能力获取和展示数据。

3.3 数据流架构

推荐的数据流:

传感器/远程接口
        ↓
数据服务层
        ↓
校验、转换、缓存
        ↓
卡片数据提供者
        ↓
卡片 UI
        ↓
用户查看或点击
        ↓
完整元服务页面

不要让卡片直接承担所有网络、数据库和复杂业务逻辑。卡片应使用统一的数据服务层,保证卡片、列表页和详情页的数据规则一致。

3.4 生命周期意识

卡片和完整页面的生命周期不同:

完整页面:进入 → 显示 → 交互 → 返回/销毁
卡片:创建 → 更新 → 隐藏/销毁 → 系统再次请求更新

因此:
• 不能假设卡片一直在内存中;
• 不能依赖卡片实例保存全部数据;
• 刷新时应重新获取必要数据;
• 重要数据应存储在可靠的数据源中;
• 卡片刷新失败时要有降级显示。

4.工程配置:module.json5

4.1 配置文件的作用

module.json5 通常用于描述模块的:
• 模块名称;
• 模块类型;
• 页面入口;
• 能力入口;
• 扩展能力;
• 权限;
• 设备支持;
• 元服务或卡片关联信息。
配置文件不是普通业务代码,但它直接决定系统能否识别、安装和启动对应能力。

4.2 extensionAbilities 的概念

extensionAbilities 用于声明模块提供的扩展能力。元服务卡片通常需要在这里声明对应的扩展能力及其入口信息。
概念结构:

{
  "module": {
    "name": "entry",
    "type": "entry",
    "description": "$string:module_desc",
    "mainElement": "EntryAbility",
    "deviceTypes": ["phone", "tablet"],
    "extensionAbilities": [
      {
        "name": "GreenhouseFormExtension",
        "type": "form",
        "srcEntry": "./ets/entryformability/GreenhouseFormExtension.ets"
      }
    ]
  }
}

说明:以上是帮助理解字段关系的示意结构。实际 type、srcEntry、卡片字段和配置层级必须使用当前 SDK 模板生成的格式,不能直接把示例当成所有版本的最终配置。

4.3 配置字段理解方法

阅读 module.json5 时,可以按以下问题分析:

  1. 这个配置属于应用还是模块?
  2. 它声明了哪个入口?
  3. 系统通过什么名称识别该能力?
  4. 入口文件在哪里?
  5. 该能力支持哪些设备类型?
  6. 是否需要权限?
  7. 卡片的元数据在哪里关联?
  8. 修改后是否需要重新安装或清理旧版本?

4.4 配置修改原则

• 修改前备份或提交代码;
• 使用 IDE 的 JSON/JSON5 校验;
• 不要删除未知字段后再观察是否能编译;
• 修改一个字段后立即构建验证;
• 记录字段含义和变更原因;
• 团队统一命名和路径;
• 不要在配置中写入密钥和敏感信息。

4.5 常见配置错误

JSON/JSON5 格式错误

{
  "name": "entry"
  "type": "entry"
}

缺少逗号会导致整个配置解析失败。

  1. 入口路径错误
    配置写的是:
./ets/entryformability/GreenhouseFormExtension.ets

但实际文件路径不一致,系统就无法加载扩展能力。
2. 名称不一致
代码中的能力名称、配置中的名称、卡片配置中的名称如果不一致,可能导致卡片无法识别。
3. 设备类型不匹配
开发时只声明了某类设备,但实际在另一类设备上测试,可能无法显示或布局异常。

5.卡片配置文件

5.1 配置文件的位置

课程材料要求在:
resources/base/profile/
下创建 JSON 配置文件,用于描述卡片元数据、尺寸、布局和更新周期。
示例目录:
entry/
└── src/main/
├── ets/
└── resources/
└── base/
└── profile/
└── greenhouse_card.json
具体目录层级可能随工程模板变化,创建时以当前项目生成的资源目录为准。

5.2 卡片配置的作用

卡片配置文件通常用于告诉系统:
• 卡片名称;
• 卡片标识;
• 支持的尺寸;
• 卡片入口;
• 卡片模板或布局;
• 更新周期;
• 预览信息;
• 关联的元服务能力。
概念示例:

{
  "name": "greenhouse_card",
  "description": "温室环境监测卡片",
  "supportDimensions": ["2x2", "4x2"],
  "updateDuration": 30,
  "formEntity": "GreenhouseFormExtension"
}

说明:字段名和结构可能因 SDK 版本不同而变化。请以当前卡片模板和官方 API 定义为准。

5.3 卡片尺寸

卡片尺寸会影响:
• 能显示多少内容;
• 文本是否需要截断;
• 指标是否横向排列;
• 图片是否需要隐藏;
• 操作按钮是否能够保留。
建议设计:

小尺寸:温度、湿度、告警数
大尺寸:温度、湿度、光照、设备状态、更新时间

不要简单地把大尺寸布局缩小到小尺寸。应针对尺寸设计信息优先级。

5.4 更新周期

更新周期不是越短越好。需要综合考虑:
• 数据变化频率;
• 用户对实时性的要求;
• 网络流量;
• 设备电量;
• 系统后台限制;
• 服务端负载;
• 卡片的业务风险。
例如:
在这里插入图片描述
配置刷新周期后仍可能受到系统调度限制,不能假设卡片一定按精确秒数刷新。

5.5 配置文件验收

• [ ] 文件位于正确的资源目录。
• [ ] JSON/JSON5 格式合法。
• [ ] 卡片标识唯一。
• [ ] 卡片入口与扩展能力名称一致。
• [ ] 支持尺寸符合设计。
• [ ] 更新周期符合业务需要。
• [ ] 修改后重新构建并安装验证。
• [ ] 删除旧卡片后重新添加,排除缓存影响。

6.卡片布局开发

6.1 布局结构

大棚卡片可以采用:

Column
├── Row:大棚名称 + 更新时间
├── Row:温度 + 湿度 + 光照
├── Row:设备在线数 + 告警数
└── Button:查看详情

小尺寸卡片可以简化为:

Column
├── 大棚一号
├── 26.4℃ / 68%
└── 1 条告警

6.2 使用 Column

Column({ space: 8 }) {
  Text('大棚一号')
  Text('温度:26.4℃')
  Text('湿度:68%')
}
.padding(16)

Column 适合组织卡片的上下层级。

6.3 使用 Row

Row({ space: 12 }) {
  Column() {
    Text('26.4℃')
    Text('温度')
  }

  Column() {
    Text('68%')
    Text('湿度')
  }

  Column() {
    Text('正常')
    Text('光照')
  }
}
.width('100%')
.justifyContent(FlexAlign.SpaceAround)

Row 适合并排展示多个指标。指标数量过多时,应考虑小尺寸卡片的空间限制。

6.4 使用 Stack

Stack() {
  Image($r('app.media.greenhouse'))
    .width('100%')
    .height(120)

  Column() {
    Text('大棚一号')
      .fontSize(18)
      .fontWeight(FontWeight.Bold)

    Text('环境正常')
      .fontSize(12)
  }
  .alignItems(HorizontalAlign.Start)
  .padding(12)
}

Stack 适合在背景图、状态图标或卡片角标上叠加文字。

6.5 基础组件组合

@Component
struct GreenhouseCardContent {
  @Prop name: string;
  @Prop temperature: number;
  @Prop humidity: number;
  @Prop alarmCount: number;

  build() {
    Column({ space: 10 }) {
      Row() {
        Text(this.name)
          .fontSize(18)
          .fontWeight(FontWeight.Bold)

        Text(this.alarmCount > 0 ? '有告警' : '正常')
          .fontSize(12)
          .fontColor(this.alarmCount > 0 ? '#B42318' : '#087443')
      }
      .width('100%')
      .justifyContent(FlexAlign.SpaceBetween)

      Row({ space: 16 }) {
        Text(`温度 ${this.temperature.toFixed(1)}℃`)
        Text(`湿度 ${this.humidity.toFixed(0)}%`)
      }

      Text(`告警:${this.alarmCount} 条`)
        .fontSize(12)
        .opacity(0.65)
    }
    .width('100%')
    .padding(16)
    .backgroundColor('#FFFFFF')
    .borderRadius(16)
  }
}

6.6 卡片布局优化原则

• 先确定核心信息,再安排容器;
• 小尺寸优先保留异常信息;
• 文本过长时截断或换行;
• 数字和单位不要挤在一起;
• 不要把按钮放在难以点击的角落;
• 颜色与文字同时表达状态;
• 在不同屏幕和主题下验证;
• 不让背景图片影响文字对比度。

7.动态数据更新

7.1 数据更新的基本链路

触发刷新
  ↓
获取温室数据
  ↓
验证数据结构和业务范围
  ↓
转换为卡片显示模型
  ↓
调用卡片更新接口
  ↓
系统重新呈现卡片

卡片显示模型可以和后端原始模型不同:

interface GreenhouseRawData {
  temperature: number;
  humidity: number;
  illumination: number;
  alarms: Alarm[];
}

interface GreenhouseCardModel {
  temperatureText: string;
  humidityText: string;
  illuminationText: string;
  alarmText: string;
  updatedAt: string;
}

这样可以把复杂数据处理放在数据层,卡片只负责展示。

7.2 使用 formProvider 的概念

课程材料要求利用 formProvider 接口或回调机制更新卡片。其核心思想是:

卡片需要更新
  ↓
数据提供者被调用
  ↓
提供最新卡片数据
  ↓
系统更新卡片显示

概念代码:

async function provideGreenhouseForm(): Promise<GreenhouseCardModel> {
  const data = await fetchGreenhouseData();

  return {
    temperatureText: `${data.temperature.toFixed(1)}℃`,
    humidityText: `${data.humidity.toFixed(0)}%`,
    illuminationText: formatIllumination(data.illumination),
    alarmText: data.alarms.length > 0
      ? `${data.alarms.length} 条告警`
      : '无告警',
    updatedAt: formatTime(new Date())
  };
}

具体 formProvider 方法名、参数、返回对象和生命周期接口要以当前 SDK 的卡片开发模板为准。

7.3 首次加载

首次加载流程:

卡片创建
  ↓
显示默认占位或加载状态
  ↓
获取温室数据
  ├── 成功 → 显示数据和时间
  └── 失败 → 显示错误或上次缓存

首次加载不能因为网络慢而让卡片一直空白。可以显示:

正在获取温室数据……

7.4 定时刷新

定时刷新适合变化相对规律的环境数据:

系统触发刷新
  ↓
判断距离上次刷新是否足够长
  ↓
获取最新数据
  ↓
更新卡片

注意:
• 系统可能限制后台刷新;
• 更新周期过短会增加资源消耗;
• 不应在每次刷新时重复创建无法释放的连接;
• 页面和卡片刷新策略可以不同;
• 卡片显示更新时间,帮助用户判断数据新鲜度。

7.5 被动刷新和事件回调

被动刷新可以由以下事件触发:
• 设备状态变化;
• 新告警产生;
• 用户点击刷新;
• 完整页面修改设备后;
• WebSocket 收到服务端消息;
• 系统重新显示卡片。
事件触发应避免重复更新:

private lastEventId: string = '';

function handleEvent(event: EquipmentEvent): void {
  if (event.id === this.lastEventId) {
    return;
  }

  this.lastEventId = event.id;
  void updateCard();
}

7.6 错误与缓存

卡片刷新失败时可以:

有上次成功数据
  → 保留数据显示 + 标记“数据可能已过期”

没有缓存
  → 显示“暂时无法获取数据” + 重试入口

不要把失败状态伪装成正常数据:

错误时显示 0℃、0% 和“正常”

这会误导用户。错误、未知和真实的零值必须区分。

7.7 数据校验

function isValidTemperature(value: unknown): value is number {
  return (
    typeof value === 'number' &&
    Number.isFinite(value) &&
    value >= -50 &&
    value <= 80
  );
}

function isValidHumidity(value: unknown): value is number {
  return (
    typeof value === 'number' &&
    Number.isFinite(value) &&
    value >= 0 &&
    value <= 100
  );
}

校验内容包括:
• 字段是否存在;
• 类型是否正确;
• 温度是否超出合理范围;
• 湿度是否在 0~100;
• 告警数量是否为非负整数;
• 时间是否可解析;
• 设备和大棚 ID 是否有效。

8.卡片交互与事件

8.1 卡片点击

卡片点击通常触发:

点击大棚卡片
  ↓
打开元服务中的完整页面
  ↓
传递 greenhouseId
  ↓
加载详细数据

概念示例:

function openGreenhouseDetail(id: string): void {
  router.pushUrl({
    url: 'pages/GreenhouseDetail',
    params: {
      greenhouseId: id
    }
  });
}

卡片点击区域应尽量覆盖主要内容,但不要让用户误触发危险操作。

8.2 点击后的参数

只传递必要参数:

{
  greenhouseId: 'GH-001'
}

详情页重新查询最新数据,而不是完全相信卡片中传来的旧摘要。

8.3 卡片按钮

如果卡片提供“刷新”按钮:

Button('刷新')
  .onClick(() => {
    if (!this.loading) {
      void this.refresh();
    }
  })

如果提供“查看告警”按钮:

Button('查看告警')
  .onClick(() => {
    openAlarmPage(this.greenhouseId);
  })

对于“启动水泵”“关闭阀门”等操作,不建议直接放在卡片上完成。更安全的方式是跳转到完整页面,执行权限校验和二次确认。

8.4 点击事件的反馈

点击后应立即让用户知道操作已发生:
• 显示加载状态;
• 按钮文字变为“刷新中”;
• 成功后更新更新时间;
• 失败后显示错误;
• 防止连续提交;
• 必要时允许重试。

Logo

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

更多推荐