【OpenHarmony/HarmonyOs 】本地收藏管理实战:ArkTS Service 层实现网址 CRUD 与容错解析
【OpenHarmony/HarmonyOs 】本地收藏管理实战:ArkTS Service 层实现网址 CRUD 与容错解析
前言
当应用允许用户添加收藏后,问题就不再只是“把数组显示出来”。我们还要处理空标题、非法链接、重复 URL、旧版本脏数据、排序规则和异常反馈。本文通过 EntryManageService 展示如何在 ArkTS 中构建一个职责清晰的本地 CRUD 服务。🗂️
一、为什么要有 Service 层
如果添加、删除、校验和 JSON 解析都写在页面中,会出现三个问题:
- 多个页面重复规则,行为逐渐不一致;
- UI 与数据存储强耦合,未来难以迁移 Cloud DB;
- 业务逻辑只能通过 UI 测试,单元测试成本很高。
Service 层让页面只表达意图:
const service = EntryManageService.getInstance();
this.customSites = await service.addCustomSite(title, url);
至于如何校验、排序和持久化,由服务内部负责。
二、单例服务与依赖
export class EntryManageService {
private static instance: EntryManageService;
private storage: StorageUtil = StorageUtil.getInstance();
private constructor() {}
static getInstance(): EntryManageService {
if (!EntryManageService.instance) {
EntryManageService.instance = new EntryManageService();
}
return EntryManageService.instance;
}
}
这个服务当前依赖 Preferences。进一步工程化时,可以定义 SiteRepository 接口并注入实现,使本地仓库与云仓库可替换。
三、读取数据时永远不要信任磁盘内容
Preferences 中保存的是 JSON 字符串。应用升级、手动调试或异常中断都可能留下非法数据,所以读取过程需要两层防御:安全解析和字段规范化。
private safeParseArray(json: string): Object[] {
if (!json) return [];
try {
const parsed = JSON.parse(json) as Object;
return Array.isArray(parsed) ? parsed as Object[] : [];
} catch {
return [];
}
}
解析为数组并不代表元素合法,还要逐项验证:
private normalizeUrlItem(raw: Object): UrlItem | null {
const item = raw as RawUrlItem;
const id = typeof item.id === 'string' ? item.id : '';
const title = typeof item.title === 'string' ? item.title.trim() : '';
const url = typeof item.url === 'string'
? this.normalizeHttpsUrl(item.url) : null;
if (!id || !title || !url) return null;
return {
id,
title,
url,
categoryId: item.categoryId || 'custom',
sort: typeof item.sort === 'number' ? item.sort : 0,
createdAt: typeof item.createdAt === 'number' ? item.createdAt : 0,
updatedAt: typeof item.updatedAt === 'number' ? item.updatedAt : 0
};
}
无效项被丢弃,缺失的可选字段获得默认值。这能防止一个坏对象让整个收藏页崩溃。
四、新增:校验、去重、生成元数据
async addCustomSite(titleRaw: string, urlRaw: string): Promise<UrlItem[]> {
const title = titleRaw.trim();
const url = this.normalizeHttpsUrl(urlRaw);
if (!title) throw new Error('EMPTY_TITLE');
if (!url) throw new Error('INVALID_URL');
const list = await this.listCustomSites();
if (list.some(item => item.url === url)) {
throw new Error('DUPLICATE_URL');
}
const now = Date.now();
const newItem: UrlItem = {
id: now.toString(), title, url,
categoryId: 'custom', sort: 0,
createdAt: now, updatedAt: now
};
const next = [newItem, ...list];
await this.persistCustomSites(next);
return next;
}
服务返回更新后的数组,页面可以一次性替换 @State,触发声明式 UI 刷新。错误使用稳定代码而不是完整中文文案,页面可根据场景决定 AlertDialog、Toast 或表单行内提示。
时间戳作为 ID 对单机原型足够直观,但极端情况下同一毫秒可能冲突。生产项目建议使用 UUID 或由数据库生成主键。
五、更新时保留不可变字段
更新接口接收 Patch,让调用者只传变化部分:
export interface UrlItemPatch {
title?: string;
url?: string;
categoryId?: string;
sort?: number;
}
更新后保留原 id 和 createdAt,只刷新 updatedAt。如果 URL 发生变化,还要排除当前记录后再检查重复:
const duplicate = list.some(item =>
item.id !== id && item.url === nextUrl
);
if (duplicate) throw new Error('DUPLICATE_URL');
这是 CRUD 中很常见却容易遗漏的细节。
六、删除与排序语义
async removeCustomSite(idRaw: string): Promise<UrlItem[]> {
const id = idRaw.trim();
if (!id) throw new Error('INVALID_ID');
const list = await this.listCustomSites();
const next = list.filter(item => item.id !== id);
await this.persistCustomSites(next);
return next;
}
当前实现对不存在的 ID 采用幂等删除:结果仍是成功状态。这对于用户快速重复点击、重试或未来云同步都很友好。
读取后按 updatedAt 降序,同一时间再按标题排序,能够保证显示结果稳定。稳定排序很重要,否则列表可能在每次刷新时随机跳动。
七、本地搜索的权重策略
Service 为自定义网址提供加权搜索:标题前缀 20 分、URL 前缀 10 分、标题包含 5 分、URL 包含 3 分。规则虽然简单,却比单纯过滤更符合用户预期。
更大规模时可以继续增加:
- 拼音首字母;
- 访问频率加分;
- 最近使用衰减;
- 标签匹配;
- 用户固定排序优先。
八、存储方案的演进边界
JSON + Preferences 适合 MVP,但每次修改都要读写整份数组。当数据量、并发和查询复杂度上升时,应迁移:
Preferences JSON
↓ 数据量增加
ArkData RDB(离线结构化查询)
↓ 多设备同步
本地 RDB + AGC Cloud DB + 冲突合并
由于页面只依赖 Service,替换底层仓库时页面代码可以基本不动,这正是分层设计的价值。✅
九、总结
可靠的收藏功能来自一组明确规则:输入先清洗、URL 强制 HTTPS、写入前去重、读取时容错、更新时间可追踪、错误码保持稳定。将这些规则集中到 Service 层,可以让 ArkUI 页面保持简洁,并为数据库与云同步升级预留空间。

更多推荐

所有评论(0)