HarmonyOS知识库——数据层架构与操作函数的设计模式
知识库 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 一看就知道"按笔记本获取笔记",不需要看实现。
更多推荐


所有评论(0)