【OpenHarmony/HarmonyOs 】本地收藏管理实战:ArkTS Service 层实现网址 CRUD 与容错解析

前言

当应用允许用户添加收藏后,问题就不再只是“把数组显示出来”。我们还要处理空标题、非法链接、重复 URL、旧版本脏数据、排序规则和异常反馈。本文通过 EntryManageService 展示如何在 ArkTS 中构建一个职责清晰的本地 CRUD 服务。🗂️

一、为什么要有 Service 层

如果添加、删除、校验和 JSON 解析都写在页面中,会出现三个问题:

  1. 多个页面重复规则,行为逐渐不一致;
  2. UI 与数据存储强耦合,未来难以迁移 Cloud DB;
  3. 业务逻辑只能通过 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;
}

更新后保留原 idcreatedAt,只刷新 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 页面保持简洁,并为数据库与云同步升级预留空间。

img

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐