HarmonyOS 端云一体化开发(一)
一、章节概览
1. 学习目标
完成本章学习后,你应当能够:
- 解释端云一体化开发的基本思想和价值。
- 在 AGC 平台创建项目并规划认证服务、云数据库等资源。
- 使用 DevEco Studio 云开发模板创建与云端资源关联的工程。
- 理解云端对象类型、字段类型、主键和数据约束。
- 在云数据库控制台设计农业设备数据表。
- 根据云端对象类型生成或维护端侧数据模型类。
- 使用 ArkUI 声明式组件构建设备数据卡片。
- 使用状态变量实现 UI 与云端数据的动态绑定。
- 编写端侧 Cloud DB 查询,完成条件过滤和排序。
- 监听云端数据变化,实现查询结果的同步和刷新。
- 处理认证失败、查询失败、权限不足和数据格式异常。
- 通过实践验证端云设计方案的可行性和有效性。
- 形成从需求、建模、实现、联调到验证的系统化开发思维。
- 在云数据安全、权限配置、隐私保护和工程协作方面承担责任。
2. 知识结构
端云一体化开发(一)
├── 端云一体化基础
│ ├── 端侧与云侧
│ ├── 数据流
│ ├── 认证与权限
│ └── 开发流程
├── AGC 项目初始化
│ ├── 创建项目
│ ├── 认证服务
│ ├── Cloud DB
│ └── 应用关联
├── 端云工程
│ ├── DevEco Studio 云开发模板
│ ├── 工程配置
│ ├── SDK 初始化
│ └── 环境区分
├── 数据建模
│ ├── 对象类型
│ ├── 字段与类型
│ ├── 主键和约束
│ └── 端侧模型生成
├── ArkUI 卡片
│ ├── 组件化
│ ├── 声明式布局
│ ├── @State 绑定
│ └── 加载与错误状态
├── Cloud DB
│ ├── 查询
│ ├── 过滤
│ ├── 排序
│ ├── 分页
│ └── 订阅更新
└── 工程实践
├── 联调
├── 测试
├── 安全
└── 复盘
二、端云一体化开发基础
2.1 什么是端云一体化
端云一体化是把端侧应用与云端资源作为一个整体进行设计和开发:
端侧(手机、平板、元服务卡片)
│
│ API / SDK / 数据订阅
▼
云侧(认证、数据库、存储、函数和管理控制台)
端侧负责:
• 展示界面;
• 接收用户输入;
• 发起查询和操作;
• 管理页面状态;
• 提供卡片和交互入口。
云侧负责:
• 用户认证;
• 数据存储;
• 数据访问控制;
• 多端共享;
• 数据备份和管理;
• 向端侧提供可访问的服务能力。
2.2 为什么要采用端云一体化
传统单机应用可以把数据保存在本地,但在多设备、多用户和实时协作场景中会遇到:
• 数据无法跨设备共享;
• 更换设备后数据丢失;
• 多人协作难以同步;
• 需要自行搭建后端接口;
• 权限和数据安全难以统一管理。
端云一体化可以帮助开发者:
• 快速创建云端资源;
• 使用统一数据模型;
• 减少重复编写基础后端代码;
• 实现多端数据共享;
• 让 UI 与云端数据保持同步;
• 集中管理认证和访问权限。
2.3 端云数据流
农业设备列表的典型数据流:
用户打开元服务卡片
↓
端侧初始化 Cloud DB SDK
↓
检查认证状态
↓
查询云端设备对象
↓
过滤、排序和转换数据
↓
更新 @State
↓
卡片显示最新设备信息
↓
云端数据发生变化
↓
订阅事件触发
↓
重新查询或局部更新 UI
2.4 端云一体化不等于“所有逻辑都放云端”
应合理划分职责:

端侧的按钮隐藏不能代替云端权限控制。任何重要数据和高风险操作都必须在云侧再次验证。
三、AGC 项目环境初始化
3.1 AGC 项目平台的作用
AGC(AppGallery Connect)平台通常用于管理应用和云端服务资源。课程要求在平台完成:
• 创建项目;
• 关联应用;
• 配置认证服务;
• 创建或配置云数据库;
• 管理应用标识和服务开关;
• 查看云端数据;
• 配置访问规则;
• 获取开发所需的项目配置。
平台界面会随版本调整,但核心思路保持不变:先建立云端项目和资源,再让端侧工程与其关联。
3.2 创建 AGC 项目
通用步骤:
- 登录 AGC 平台。
- 创建一个新的项目。
- 填写项目名称。
- 添加与 HarmonyOS 应用对应的应用条目。
- 确认包名、应用标识和项目归属。
- 开启课程需要的认证服务和 Cloud DB。
- 记录项目环境信息。
- 下载或生成端侧工程需要的配置。
- 在 DevEco Studio 工程中完成关联。
- 使用最小查询验证连接是否成功。
项目命名建议
AgriCloudDemo
GreenhouseCloudLab
EquipmentCloudService
建议:
• 名称具有业务含义;
• 不使用临时测试词作为正式项目名称;
• 开发、测试和生产项目分开;
• 团队统一命名规则;
• 不把密钥或敏感信息写入项目名称和代码。
3.3 认证服务
认证服务用于确认“当前用户是谁”,并为数据访问规则提供身份依据。
常见认证流程:
用户打开应用
↓
检查是否已认证
├── 已认证 → 访问 Cloud DB
└── 未认证 → 登录/匿名认证/提示用户
认证信息可以影响:
• 当前用户能查看哪些设备;
• 当前用户能否修改设备;
• 用户创建的数据归属;
• 设备和组织之间的关系;
• 访问日志和审计记录。
认证设计原则
• 认证状态要明确;
• 登录过期要能恢复;
• 未认证时不要直接展示敏感数据;
• 不在日志中输出令牌;
• 不把用户身份只交给端侧传参决定;
• 云端规则应根据可靠身份信息判断权限。
3.4 云数据库服务
Cloud DB 用于保存结构化业务数据。农业场景可以存储:
• 大棚信息;
• 设备信息;
• 传感器数据;
• 告警记录;
• 设备操作日志;
• 用户与设备的关联关系。
初始化时要规划:
项目
├── 认证
├── 数据库
│ ├── greenhouse
│ ├── equipment
│ └── alarm
└── 应用关联和权限规则
3.5 环境区分
建议区分:
开发环境:学生个人测试数据
测试环境:团队联调和验收
生产环境:真实业务和真实用户
不要让课堂测试代码直接访问生产数据库。环境配置应明确:
• 项目 ID;
• 应用标识;
• 数据库资源;
• 认证方式;
• 日志级别;
• 数据权限;
• 发布渠道。
四、端云一体化工程创建
4.1 使用云开发模板
在 DevEco Studio 中,可以使用与云开发相关的工程模板创建项目。模板的价值在于:
• 生成基本项目结构;
• 配置端云关联所需文件;
• 提供 Cloud DB 初始化示例;
• 创建基础页面和模型;
• 减少手动配置错误。
通用步骤:
- 打开 DevEco Studio。
- 选择创建新工程。
- 选择云开发或端云一体化相关模板。
- 填写项目名称、包名和保存路径。
- 选择 SDK 和设备类型。
- 选择或关联 AGC 项目。
- 确认云端资源配置。
- 等待工程同步和依赖解析。
- 检查生成的配置文件和入口页面。
- 执行一次最小编译和运行。
4.2 工程中的重要部分
项目根目录
├── AppScope/
├── entry/
│ └── src/main/
│ ├── ets/
│ │ ├── pages/
│ │ ├── components/
│ │ ├── model/
│ │ └── service/
│ ├── resources/
│ └── module.json5
├── 云端关联配置
├── 构建配置
└── 依赖配置
建议按职责组织端侧代码:
ets/
├── pages/ # 页面
├── components/ # 可复用 UI
├── model/ # 端侧对象类型
├── service/ # Cloud DB 和认证服务
├── mapper/ # 云端数据到 UI 模型的转换
└── utils/ # 格式化和校验工具
4.3 初始化 Cloud DB SDK
概念流程:
async function initializeCloud(): Promise<void> {
// 1. 初始化应用上下文
// 2. 初始化认证服务
// 3. 初始化 Cloud DB
// 4. 检查登录或认证状态
}
实际初始化 API 和配置方式需遵循当前 SDK 版本。初始化失败时应明确区分:
• 配置文件缺失;
• 项目关联错误;
• SDK 未安装;
• 网络不可用;
• 认证失败;
• 权限不足。
4.4 初始化验收
• [ ] 云开发模板工程可以打开。
• [ ] AGC 项目与应用包名匹配。
• [ ] 认证服务配置正确。
• [ ] Cloud DB 服务已开启。
• [ ] 端侧依赖可解析。
• [ ] SDK 初始化没有异常日志。
• [ ] 可以执行一个最小查询或连接检查。
• [ ] 开发环境与生产环境没有混淆。
五、云端对象类型设计
5.1 什么是对象类型
对象类型是对云端数据结构的描述。它决定:
• 对象有哪些字段;
• 每个字段是什么类型;
• 哪些字段必填;
• 哪些字段可以为空;
• 哪些字段作为主键;
• 数据能否被查询、排序和订阅。
一个农业设备对象可以表示为:
interface Equipment {
id: string;
name: string;
category: string;
status: string;
greenhouseId: string;
battery: number;
updatedAt: string;
}
云端控制台中的对象类型应与端侧模型保持一致。
5.2 字段设计原则
字段名称清晰
推荐:
greenhouseId
updatedAt
batteryPercent
不推荐:
a
temp1
data2
类型稳定
如果 battery 表示百分比,应统一使用数字,不要有时返回数字、有时返回字符串:
正确:battery = 82
不一致:battery = "82"
时间统一
统一选择:
• 时间戳;
• ISO 8601 字符串;
• 云数据库支持的日期类型。
不要在不同表中混用多种无法转换的时间格式。
状态受约束
online
offline
maintenance
状态值应由统一类型或枚举约束,避免出现:
onlie
Online
在线中
5.3 大棚对象类型
interface Greenhouse {
id: string;
name: string;
location: string;
area: number;
status: 'normal' | 'warning' | 'offline';
updatedAt: string;
}
5.4 设备与大棚的关系
设备通常属于某个大棚:
Greenhouse 1 ──── * Equipment
设备表中可以保存:
greenhouseId
查询某个大棚的设备:
WHERE greenhouseId = 'GH-001'
如果还需要设备历史数据,可以拆分:
equipment
equipment_reading
equipment_alarm
不要把所有历史数据不断追加到设备对象的一个大字段中,这会降低查询和维护效率。
5.5 主键与索引
主键
主键应:
• 唯一;
• 稳定;
• 不随名称变化;
• 便于路由和查询;
• 能在多端共享。
索引
适合建立索引的字段可能包括:
• greenhouseId;
• status;
• updatedAt;
• category。
索引不是越多越好。应根据真实查询场景规划,避免增加无用写入成本。
5.6 数据约束
示例约束:
id:必填、唯一
name:必填、长度限制
battery:0~100 的整数
status:限定为三种状态
greenhouseId:必须关联有效大棚
updatedAt:必须是合法时间
客户端可以做第一层校验,云端还必须做最终校验。
六、端侧对象模型生成
6.1 为什么需要端侧模型
如果端侧直接操作原始对象,会出现:
• 字段名称拼写错误;
• 云端字段更新后端侧未同步;
• 状态值不统一;
• 查询结果无法获得编辑器提示;
• UI 与数据结构耦合过深。
端侧模型可以提供:
• 类型检查;
• 自动补全;
• 统一字段;
• 数据转换入口;
• 与云端对象类型的对应关系。
6.2 自动生成与手工维护
理想流程:
云端对象类型
↓
模型生成工具
↓
端侧数据模型类
↓
页面和服务层使用
如果当前工具支持自动生成,应在云端模型变更后重新生成并检查差异。若必须手工维护,应建立字段变更记录。
6.3 端侧模型示例
export type EquipmentStatus =
| 'online'
| 'offline'
| 'maintenance';
export class EquipmentModel {
id: string = '';
name: string = '';
category: string = '';
status: EquipmentStatus = 'offline';
greenhouseId: string = '';
battery: number = 0;
updatedAt: string = '';
}
实际生成模型的构造函数、装饰器和字段定义需要以当前 Cloud DB SDK 模板为准。
6.4 云端模型与 UI 模型分离
云端模型:
interface EquipmentRecord {
id: string;
name: string;
status: EquipmentStatus;
updatedAt: string;
}
UI 模型:
interface EquipmentCardModel {
title: string;
statusText: string;
statusColor: string;
updatedAtText: string;
}
转换函数:
function toCardModel(
record: EquipmentRecord
): EquipmentCardModel {
return {
title: record.name,
statusText: getStatusText(record.status),
statusColor: getStatusColor(record.status),
updatedAtText: formatTime(record.updatedAt)
};
}
分离的好处:
• 数据层和界面层解耦;
• UI 不需要知道数据库字段细节;
• 更容易适配不同卡片尺寸;
• 后端字段变化影响范围更小。
6.5 模型同步检查
云端字段变更后检查:
云端对象类型
↓
端侧模型
↓
查询字段
↓
转换函数
↓
UI 绑定
↓
测试数据和卡片配置
不要只修改云端表而忘记端侧代码。
七、ArkUI 声明式卡片开发
7.1 页面与卡片结构
端侧卡片可以拆成:
GreenhouseCardPage
├── CardHeader
├── EnvironmentMetrics
├── DeviceSummary
├── AlarmSummary
└── CardFooter
每个组件只负责一类 UI 任务。
7.2 卡片示例
@Component
struct GreenhouseDataCard {
@Prop name: string;
@Prop temperature: number;
@Prop humidity: number;
@Prop alarmCount: number;
build() {
Column({ space: 8 }) {
Text(this.name)
.fontSize(18)
.fontWeight(FontWeight.Bold)
Row({ space: 16 }) {
Column() {
Text(`${this.temperature.toFixed(1)}℃`)
.fontSize(22)
Text('温度')
.fontSize(12)
}
Column() {
Text(`${this.humidity.toFixed(0)}%`)
.fontSize(22)
Text('湿度')
.fontSize(12)
}
}
.width('100%')
.justifyContent(FlexAlign.SpaceAround)
Text(this.alarmCount > 0
? `告警:${this.alarmCount} 条`
: '当前无告警')
.fontSize(12)
}
.padding(16)
.backgroundColor('#FFFFFF')
.borderRadius(16)
}
}
7.3 数据绑定
@Component
struct GreenhousePage {
@State data: GreenhouseData | null = null;
build() {
Column() {
if (this.data === null) {
Text('暂无大棚数据')
} else {
GreenhouseDataCard({
name: this.data.name,
temperature: this.data.temperature,
humidity: this.data.humidity,
alarmCount: this.data.alarmCount
})
}
}
}
}
当执行:
this.data = latestData;
绑定 data 的界面会重新构建。
7.4 加载和错误状态
type LoadState<T> =
| { kind: 'loading' }
| { kind: 'success'; data: T }
| { kind: 'empty' }
| { kind: 'error'; message: string };
使用状态驱动:
@State state: LoadState<GreenhouseData> = {
kind: 'loading'
};
这样可以清晰区分:
• 正在加载;
• 获取成功;
• 成功但无数据;
• 请求失败。
不要用一个空对象同时表示“未加载”“加载失败”和“真实数据全为零”。
八、云数据库查询操作
8.1 查询流程
确定对象类型
↓
创建查询对象
↓
添加过滤条件
↓
设置排序
↓
设置分页
↓
执行查询
↓
转换结果
↓
更新 @State
8.2 查询全部设备
概念示例:
async function queryAllEquipments(): Promise<Equipment[]> {
const query = createEquipmentQuery();
const records = await query.execute();
return records.map(toEquipmentModel);
}
实际查询对象、执行函数和返回类型需按照当前 Cloud DB SDK 的 API 编写。
8.3 条件过滤
查询某个大棚的在线设备:
async function queryOnlineEquipments(
greenhouseId: string
): Promise<Equipment[]> {
const query = createEquipmentQuery()
.where('greenhouseId', '==', greenhouseId)
.where('status', '==', 'online');
const records = await query.execute();
return records.map(toEquipmentModel);
}
注意:
• 过滤字段名称要与云端模型一致;
• 参数应进行类型校验;
• 多条件组合要确认逻辑是 AND 还是 OR;
• 过滤条件应尽量在云端执行,减少端侧数据量。
8.4 排序
按更新时间倒序:
const query = createEquipmentQuery()
.orderBy('updatedAt', 'desc');
按电量从低到高:
const query = createEquipmentQuery()
.orderBy('battery', 'asc');
排序前要确认字段类型统一,否则字符串排序和数字排序可能产生不同结果。
8.5 分页
interface PageResult<T> {
records: T[];
page: number;
pageSize: number;
total?: number;
hasMore: boolean;
}
分页的价值:
• 降低首次请求数据量;
• 减少页面渲染压力;
• 改善大棚设备数量较多时的体验;
• 控制网络和内存消耗。
8.6 查询错误处理
async function safeQuery(): Promise<Equipment[]> {
try {
return await queryAllEquipments();
} catch (error: unknown) {
if (error instanceof Error) {
console.error(`查询失败:${error.message}`);
}
throw new Error('设备数据暂时无法获取');
}
}
应区分:
• 认证失败;
• 权限不足;
• 网络异常;
• 查询条件错误;
• 云服务异常;
• 数据格式错误。
不要把所有错误都显示成“暂无数据”。
九、数据订阅与更新
9.1 为什么需要订阅
如果多个用户或设备都能修改云端数据,仅在页面首次进入时查询一次是不够的:
云端状态已改变
↓
页面仍显示旧状态
↓
用户做出错误判断
数据订阅可以在云端对象变化时通知端侧:
云端数据变化
↓
订阅回调触发
↓
接收变化事件
↓
重新查询或更新本地状态
↓
UI 自动刷新
9.2 订阅方式
具体 API 取决于当前 Cloud DB SDK,常见思路包括:
• 订阅查询结果;
• 监听对象变化;
• 接收新增、修改和删除事件;
• 通过回调更新页面;
• 使用事件后重新查询。
概念示例:
private unsubscribe?: () => void;
onPageShow(): void {
this.unsubscribe = subscribeEquipmentChanges(
(change: EquipmentChange) => {
this.handleChange(change);
}
);
}
onPageHide(): void {
this.unsubscribe?.();
this.unsubscribe = undefined;
}
9.3 重新查询与局部更新
重新查询
收到变化事件
↓
重新查询当前条件下的数据
↓
整体更新 @State
优点:逻辑简单,一致性较好。
缺点:可能增加请求量。
局部更新
收到修改事件
↓
找到对应设备
↓
只更新该项
优点:响应快、数据量小。
缺点:需要正确处理新增、修改、删除、排序和筛选。
初学阶段可先实现重新查询,再根据性能需求进行局部优化。
9.4 订阅生命周期
页面显示 → 建立订阅
页面隐藏 → 取消订阅
页面重新显示 → 恢复订阅
页面销毁 → 确保全部释放
如果忘记取消订阅,可能出现:
• 重复回调;
• 页面离开后仍更新状态;
• 内存泄漏;
• 同一个事件触发多次查询;
• 卡片和页面数据互相覆盖。
9.5 事件顺序和重复事件
订阅事件可能存在:
• 重复投递;
• 网络恢复后的批量事件;
• 事件乱序;
• 事件与手动刷新同时到达。
建议:
• 使用事件 ID 或版本号去重;
• 关键修改后重新查询;
• 记录数据更新时间;
• 不盲目相信事件到达顺序;
• 对删除对象进行存在性检查。
十、综合案例:云端大棚设备卡片
10.1 需求说明
实现一个端云一体化大棚卡片:
• 云端保存大棚设备数据;
• 端侧查询指定大棚;
• 卡片显示温度、湿度、在线设备数和告警数;
• 页面打开时加载数据;
• 数据变化时自动更新;
• 卡片点击进入完整设备页面;
• 请求失败时显示缓存或重试;
• 页面隐藏时取消订阅;
• 完成至少一次端云联调。
10.2 云端对象类型
interface GreenhouseReading {
id: string;
greenhouseId: string;
temperature: number;
humidity: number;
illumination: number;
alarmCount: number;
recordedAt: string;
}
interface Equipment {
id: string;
greenhouseId: string;
name: string;
status: 'online' | 'offline' | 'maintenance';
updatedAt: string;
}
10.3 统计模型
interface GreenhouseSummary {
greenhouseId: string;
greenhouseName: string;
temperature: number;
humidity: number;
illumination: number;
onlineCount: number;
alarmCount: number;
updatedAt: string;
}
10.4 数据服务层
class GreenhouseService {
async getSummary(
greenhouseId: string
): Promise<GreenhouseSummary> {
const reading = await this.queryLatestReading(
greenhouseId
);
const equipments = await this.queryEquipments(
greenhouseId
);
return {
greenhouseId,
greenhouseName: '大棚一号',
temperature: reading.temperature,
humidity: reading.humidity,
illumination: reading.illumination,
onlineCount: equipments.filter(
item => item.status === 'online'
).length,
alarmCount: reading.alarmCount,
updatedAt: reading.recordedAt
};
}
private async queryLatestReading(
greenhouseId: string
): Promise<GreenhouseReading> {
// 按 recordedAt 倒序查询最新一条
throw new Error('示例方法');
}
private async queryEquipments(
greenhouseId: string
): Promise<Equipment[]> {
// 按 greenhouseId 查询设备
throw new Error('示例方法');
}
}
服务层的价值:
• 页面不直接拼接查询细节;
• 卡片和完整页面可以复用;
• 查询、转换和错误处理集中管理;
• 便于单元测试和后续替换数据源。
10.5 页面状态管理
type SummaryState =
| { kind: 'loading'; previous?: GreenhouseSummary }
| { kind: 'success'; data: GreenhouseSummary }
| { kind: 'error'; message: string; previous?: GreenhouseSummary };
@Component
struct GreenhouseCloudPage {
@State state: SummaryState = {
kind: 'loading'
};
private readonly service = new GreenhouseService();
private greenhouseId: string = 'GH-001';
private unsubscribe?: () => void;
aboutToAppear(): void {
void this.loadSummary();
}
onPageShow(): void {
this.subscribeChanges();
}
onPageHide(): void {
this.unsubscribe?.();
this.unsubscribe = undefined;
}
private async loadSummary(): Promise<void> {
const previous =
this.state.kind === 'success'
? this.state.data
: undefined;
this.state = { kind: 'loading', previous };
try {
const data = await this.service.getSummary(
this.greenhouseId
);
this.state = { kind: 'success', data };
} catch (error: unknown) {
this.state = {
kind: 'error',
message: '大棚数据获取失败,请稍后重试',
previous
};
}
}
private subscribeChanges(): void {
this.unsubscribe = subscribeGreenhouseChanges(
this.greenhouseId,
() => {
void this.loadSummary();
}
);
}
build() {
if (this.state.kind === 'loading') {
Text('正在获取大棚数据……')
} else if (this.state.kind === 'error') {
Column({ space: 8 }) {
if (this.state.previous !== undefined) {
Text(`数据可能已过期:${this.state.previous.updatedAt}`)
}
Text(this.state.message)
Button('重试')
.onClick(() => {
void this.loadSummary();
})
}
} else {
GreenhouseSummaryCard({
data: this.state.data
})
}
}
}
说明:示例中的订阅函数、Cloud DB 查询和路由函数是概念接口,实际代码必须根据当前 HarmonyOS/Cloud DB SDK 版本调整。
10.6 端云联调步骤
- 在 AGC 创建项目
- 开启认证和 Cloud DB
- 创建设备/大棚对象类型
- 配置字段、权限和测试数据
- 创建端云工程
- 初始化 Cloud DB SDK
- 端侧生成或维护对象模型
- 执行最小查询
- 将结果绑定到 @State
- 在卡片中显示
- 修改云端数据
- 验证订阅或重新查询
- 验证错误、权限和断网场景
更多推荐



所有评论(0)