知识库 App 的 NoteService 是整个项目最复杂的文件——12 篇 mock 数据、3 个接口、9 个操作函数,同时管理静态查询和动态修改。和菜谱 App 的 RecipeData 相比,NoteService 的修改操作更多(新增/删除/切换收藏),查询维度更丰富(按笔记本/按标签/搜索/统计)。

完整效果
在这里插入图片描述

一、数据层的整体结构

NoteService.ets
├─ 接口定义(3 个)
│   ├─ Note          笔记核心数据
│   ├─ Notebook      笔记本信息
│   └─ TagCount      标签统计
├─ Mock 数据(2 组)
│   ├─ _notes        12 篇笔记(let 可变)
│   └─ NOTEBOOKS     4 个笔记本(export const 只读)
├─ 查询函数(6 个)
│   ├─ getNotes         全部笔记
│   ├─ getNotesByNotebook  按笔记本筛选
│   ├─ getFavorites     收藏笔记
│   ├─ searchNotes      搜索笔记
│   ├─ getNoteById      按 id 查询
│   └─ getAllTags       标签统计
└─ 修改函数(3 个)
    ├─ addNote          新增笔记
    ├─ deleteNote       删除笔记
    └─ toggleFav        切换收藏

两组 mock 数据的可变性不同:

数据 声明方式 可变性 原因
_notes let 可变(增删改) 新增/删除/收藏操作需要修改
NOTEBOOKS export const 引用不可变 只读展示,noteCount 通过属性修改

踩坑记录: 最初 NOTEBOOKS 也用 let 声明,后来发现没有操作函数会替换 NOTEBOOKS 的引用——只有 addNote 会修改 NOTEBOOKS[i].noteCount。改成 export const 更安全,防止意外替换整个数组。

二、Note 接口的 8 个字段

export interface Note {
  id: number; title: string; content: string; notebookId: number
  tags: string[]; favorited: boolean; createdAt: string; updatedAt: string
}

在这里插入图片描述

字段分组与设计理由

分组 字段 类型 设计理由
标识 id number 唯一标识,路由传参用
内容 title, content string 笔记的核心数据
分类 notebookId number 关联笔记本(存 id 不存名称)
分类 tags string[] 多标签支持
状态 favorited boolean 收藏标记
时间 createdAt, updatedAt string 创建和更新时间

tags 用 string[] 而不是 string。 一篇笔记可以有多个标签(如"HarmonyOS"“编程”),用数组存储。如果用逗号分隔的字符串,查询时需要 split 再匹配,代码更复杂。

notebookId 用 number 关联 Notebook。 存 id 不存名称——修改笔记本名只需改 NOTEBOOKS 一处,不需要遍历所有笔记更新。这是关系型数据设计的基本原则。

favorited 直接存在 Note 对象上。 和菜谱 App 的独立 _favs 数组不同——知识库把收藏标记直接放在笔记数据里。两种方式各有优劣:

方案 优点 缺点
存在 Note 上(知识库) 查询简单,不需要交叉匹配 修改收藏需要遍历 _notes
独立数组(菜谱) 收藏操作不污染笔记数据 查询需要双重遍历匹配

在 12 篇笔记的规模下,两种方式差异不大。但存 Note 上更直观——看到一篇笔记就知道它是否被收藏。

三、Notebook 接口的 noteCount 字段

export interface Notebook {
  id: number; name: string; icon: string; color: string; noteCount: number
}

noteCount 是冗余字段——可以从 _notes 里实时计算。 为什么不实时计算?因为每次显示笔记本列表都遍历 _notes 统计数量,效率不高。noteCount 是"空间换时间"的策略——多存一个数字,避免每次查询都遍历。

noteCount 需要手动维护——addNote 时 +1,deleteNote 时 -1。 如果忘了维护,noteCount 和实际笔记数不一致。

踩坑记录: 最初 deleteNote 忘了更新 noteCount,删除笔记后笔记本卡片显示的笔记数量没变。加了 NOTEBOOKS[i].noteCount-- 后解决。这提醒我们:修改 _notes 的操作函数必须同步更新 noteCount。

四、6 个查询函数的设计

函数一:getNotes — 全量查询

export function getNotes(): Note[] { return _notes }

直接返回 _notes 的引用——不创建新数组。 因为调用方只是读取数据(渲染列表),不需要修改。返回引用比创建新数组更高效。

风险: 如果调用方修改了返回值,会影响 _notes。但在当前代码里,所有调用方都只读取不修改,所以安全。

函数二:getNotesByNotebook — 条件过滤

export function getNotesByNotebook(nbId: number): Note[] {
  const r: Note[] = []
  for (let i: number = 0; i < _notes.length; i++) {
    if (_notes[i].notebookId === nbId) r.push(_notes[i])
  }
  return r
}

在这里插入图片描述

单层遍历 + 条件过滤。 notebookId 是数字,直接比较即可。返回新数组——r.push() 创建了独立数组。

函数三:getFavorites — 布尔过滤

export function getFavorites(): Note[] {
  const r: Note[] = []
  for (let i: number = 0; i < _notes.length; i++) {
    if (_notes[i].favorited) r.push(_notes[i])
  }
  return r
}

结构和 getNotesByNotebook 完全一样——只是过滤条件不同。 一个是 notebookId === nbId,一个是 favorited === true。这种"同构函数"是数据层的常见模式。

函数四:searchNotes — 关键词搜索

export function searchNotes(kw: string): Note[] {
  const q: string = kw.toLowerCase()
  const r: Note[] = []
  for (let i: number = 0; i < _notes.length; i++) {
    if (_notes[i].title.toLowerCase().indexOf(q) >= 0 ||
        _notes[i].content.toLowerCase().indexOf(q) >= 0) {
      r.push(_notes[i])
    }
  }
  return r
}

在这里插入图片描述

搜索标题和内容两个字段。 toLowerCase() 让搜索忽略大小写。indexOf(q) >= 0 检查关键词是否出现在标题或内容里。

为什么用 indexOf 而不是 includes indexOf 在 ArkTS 里的兼容性更好。includes 是 ES6 方法,某些 ArkTS 版本可能不支持。

函数五:getNoteById — 单条查询

export function getNoteById(id: number): Note | undefined {
  for (let i: number = 0; i < _notes.length; i++) {
    if (_notes[i].id === id) return _notes[i]
  }
  return undefined
}

返回 Note | undefined——找不到时返回 undefined。 调用方需要处理 undefined 情况(如 NoteEditor 的 if (this.note) 判断)。

函数六:getAllTags — 标签统计

export function getAllTags(): TagCount[] {
  const result: TagCount[] = []
  for (let i: number = 0; i < _notes.length; i++) {
    for (let j: number = 0; j < _notes[i].tags.length; j++) {
      const t: string = _notes[i].tags[j]
      let found: boolean = false
      for (let k: number = 0; k < result.length; k++) {
        if (result[k].name === t) { result[k].count++; found = true; break }
      }
      if (!found) { result.push({ name: t, count: 1 }) }
    }
  }
  return result
}

在这里插入图片描述

三重循环的频率统计模式。 外层遍历笔记,中层遍历标签,内层检查是否已统计。这是"计数器"的标准实现——已存在则 +1,不存在则新增。

6 个查询函数的复杂度对比

函数 遍历层级 时间复杂度 返回类型
getNotes 无遍历 O(1) Note[](引用)
getNotesByNotebook 单层 O(n) Note[](新数组)
getFavorites 单层 O(n) Note[](新数组)
searchNotes 单层 O(n×m) Note[](新数组)
getNoteById 单层 O(n) Note | undefined
getAllTags 三层 O(n×m×k) TagCount[](新数组)

n=笔记数,m=每篇笔记的标签/内容长度,k=不重复标签数。

五、3 个修改函数的设计

addNote — 新增笔记

export function addNote(title: string, content: string, nbId: number, tags: string[]): void {
  _nextId++
  const d: string = '2026-07-15'
  const n: Note[] = [{ id: _nextId, title: title, content: content, notebookId: nbId,
    tags: tags, favorited: false, createdAt: d, updatedAt: d }]
  for (let i: number = 0; i < _notes.length; i++) { n.push(_notes[i]) }
  _notes = n
  for (let i: number = 0; i < NOTEBOOKS.length; i++) {
    if (NOTEBOOKS[i].id === nbId) NOTEBOOKS[i].noteCount++
  }
}

四个关键操作:

步骤 操作 作用
1 _nextId++ 生成唯一 id
2 创建新数组 n,新笔记放最前面 保证新笔记在列表顶部
3 _notes = n 替换引用触发 @State 刷新
4 NOTEBOOKS[i].noteCount++ 同步更新笔记本笔记计数

新笔记放数组最前面——用户新建后立即在列表顶部看到。 如果 push 到末尾,用户需要滚动到最后才能看到新笔记。

_nextId 从 100 开始——和 mock 数据的 id 1-12 不冲突。 如果从 1 开始,新增笔记的 id 可能和 mock 数据重复。

deleteNote — 删除笔记

export function deleteNote(id: number): void {
  const r: Note[] = []
  for (let i: number = 0; i < _notes.length; i++) {
    if (_notes[i].id !== id) r.push(_notes[i])
  }
  _notes = r
}

按 id 过滤——创建新数组,跳过要删除的项。 和 addNote 的"创建新数组 + 替换引用"模式一致。

踩坑记录: 最初 deleteNote 忘了更新 noteCount,删除笔记后笔记本卡片的笔记数量没变。加了 noteCount-- 逻辑后解决。

toggleFav — 切换收藏

export function toggleFav(id: number): boolean {
  for (let i: number = 0; i < _notes.length; i++) {
    if (_notes[i].id === id) {
      _notes[i].favorited = !_notes[i].favorited
      return _notes[i].favorited
    }
  }
  return false
}

直接修改对象属性——和 addNote/deleteNote 的"创建新数组"模式不同。 因为 favorited 是布尔值,修改属性后 _notes 的引用不变,但对象内容变了。调用方通过返回值更新 @State。

返回 boolean 让调用方直接赋值。 this.fav = toggleFav(id) 一行代码完成操作和状态同步。

三个修改函数的模式对比

函数 修改方式 是否创建新数组 是否更新 noteCount 返回值
addNote 创建新数组 + 替换引用 是(+1) void
deleteNote 创建新数组 + 替换引用 是(-1) void
toggleFav 直接修改对象属性 boolean

addNote 和 deleteNote 用"创建新数组"——因为增删操作改变了数组长度。 toggleFav 用"修改属性"——因为只是改变了对象的一个字段,数组长度不变。

六、数据流的完整链路

新建笔记的数据流

NoteEditor.save()
  → addNote(title, content, nbId, tags)
    → _nextId++
    → 创建新数组 n,新笔记放最前
    → _notes = n(替换引用)
    → NOTEBOOKS[i].noteCount++
  → router.back()
  → 首页 onPageShow → refresh() → getNotes()
  → this.notes = getNotes()(获取最新 _notes)
  → ForEach 渲染(新笔记在列表顶部)

删除笔记的数据流

NoteEditor.deleteNote()
  → deleteNote(id)
    → 创建新数组 r,跳过要删除的项
    → _notes = r(替换引用)
    → NOTEBOOKS[i].noteCount--
  → router.back()
  → 首页 onPageShow → refresh() → getNotes()
  → ForEach 渲染(被删除的笔记消失)

收藏切换的数据流

NoteEditor.toggleFav()
  → this.fav = toggleFav(id)
    → _notes[i].favorited = !favorited
    → return 新的 favorited 值
  → UI 更新:❤️ ↔ 🤍

用户返回首页
  → onPageShow → refresh() → getNotes()
  → 笔记卡片里的 ❤️ 图标根据 favorited 显示

七、和菜谱 App 数据层的对比

维度 知识库 NoteService 菜谱 RecipeData
接口数量 3 个 3 个
Mock 数据 _notes(可变)+ NOTEBOOKS(只读) CATS + RECIPES(只读)
共享状态 无独立共享状态 _favs + _shop 两组
查询函数 6 个 4 个
修改函数 3 个 4 个
收藏实现 直接存在 Note 上 独立 _favs 数组
搜索功能 有(searchNotes)

知识库的查询函数更多——因为有搜索和标签统计两个额外维度。 菜谱 App 只有按分类筛选一种查询。

知识库没有独立的共享状态——收藏直接存在 Note 上。 菜谱 App 需要独立的 _favs_shop 数组,因为收藏和采购清单是独立于菜谱数据的额外状态。

八、操作函数的命名规范

命名模式 函数 含义
get + 名词 getNotes, getFavorites, getNoteById 查询数据
get + 名词 + By + 条件 getNotesByNotebook 按条件查询
get + 形容词 + 名词 getAllTags 获取统计信息
动词 + 名词 addNote, deleteNote 增删操作
动词 + 名词 toggleFav 状态切换

命名规范让代码自解释——看到函数名就知道它做什么。 getNotesByNotebook 一看就知道"按笔记本获取笔记",不需要看实现。

Logo

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

更多推荐