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 数据的可变性不同数据声明方式可变性原因_noteslet可变增删改新增/删除/收藏操作需要修改NOTEBOOKSexport const引用不可变只读展示noteCount 通过属性修改踩坑记录最初 NOTEBOOKS 也用let声明后来发现没有操作函数会替换 NOTEBOOKS 的引用——只有addNote会修改NOTEBOOKS[i].noteCount。改成export const更安全防止意外替换整个数组。二、Note 接口的 8 个字段exportinterfaceNote{id:number;title:string;content:string;notebookId:numbertags:string[];favorited:boolean;createdAt:string;updatedAt:string}字段分组与设计理由分组字段类型设计理由标识idnumber唯一标识路由传参用内容title, contentstring笔记的核心数据分类notebookIdnumber关联笔记本存 id 不存名称分类tagsstring[]多标签支持状态favoritedboolean收藏标记时间createdAt, updatedAtstring创建和更新时间tags 用 string[] 而不是 string。一篇笔记可以有多个标签如HarmonyOS“编程”用数组存储。如果用逗号分隔的字符串查询时需要 split 再匹配代码更复杂。notebookId 用 number 关联 Notebook。存 id 不存名称——修改笔记本名只需改 NOTEBOOKS 一处不需要遍历所有笔记更新。这是关系型数据设计的基本原则。favorited 直接存在 Note 对象上。和菜谱 App 的独立_favs数组不同——知识库把收藏标记直接放在笔记数据里。两种方式各有优劣方案优点缺点存在 Note 上知识库查询简单不需要交叉匹配修改收藏需要遍历 _notes独立数组菜谱收藏操作不污染笔记数据查询需要双重遍历匹配在 12 篇笔记的规模下两种方式差异不大。但存 Note 上更直观——看到一篇笔记就知道它是否被收藏。三、Notebook 接口的 noteCount 字段exportinterfaceNotebook{id:number;name:string;icon:string;color:string;noteCount:number}noteCount 是冗余字段——可以从 _notes 里实时计算。为什么不实时计算因为每次显示笔记本列表都遍历 _notes 统计数量效率不高。noteCount 是空间换时间的策略——多存一个数字避免每次查询都遍历。noteCount 需要手动维护——addNote 时 1deleteNote 时 -1。如果忘了维护noteCount 和实际笔记数不一致。踩坑记录最初deleteNote忘了更新noteCount删除笔记后笔记本卡片显示的笔记数量没变。加了NOTEBOOKS[i].noteCount--后解决。这提醒我们修改 _notes 的操作函数必须同步更新 noteCount。四、6 个查询函数的设计函数一getNotes — 全量查询exportfunctiongetNotes():Note[]{return_notes}直接返回 _notes 的引用——不创建新数组。因为调用方只是读取数据渲染列表不需要修改。返回引用比创建新数组更高效。风险如果调用方修改了返回值会影响 _notes。但在当前代码里所有调用方都只读取不修改所以安全。函数二getNotesByNotebook — 条件过滤exportfunctiongetNotesByNotebook(nbId:number):Note[]{constr:Note[][]for(leti:number0;i_notes.length;i){if(_notes[i].notebookIdnbId)r.push(_notes[i])}returnr}单层遍历 条件过滤。notebookId 是数字直接比较即可。返回新数组——r.push()创建了独立数组。函数三getFavorites — 布尔过滤exportfunctiongetFavorites():Note[]{constr:Note[][]for(leti:number0;i_notes.length;i){if(_notes[i].favorited)r.push(_notes[i])}returnr}结构和 getNotesByNotebook 完全一样——只是过滤条件不同。一个是notebookId nbId一个是favorited true。这种同构函数是数据层的常见模式。函数四searchNotes — 关键词搜索exportfunctionsearchNotes(kw:string):Note[]{constq:stringkw.toLowerCase()constr:Note[][]for(leti:number0;i_notes.length;i){if(_notes[i].title.toLowerCase().indexOf(q)0||_notes[i].content.toLowerCase().indexOf(q)0){r.push(_notes[i])}}returnr}搜索标题和内容两个字段。toLowerCase()让搜索忽略大小写。indexOf(q) 0检查关键词是否出现在标题或内容里。为什么用indexOf而不是includesindexOf在 ArkTS 里的兼容性更好。includes是 ES6 方法某些 ArkTS 版本可能不支持。函数五getNoteById — 单条查询exportfunctiongetNoteById(id:number):Note|undefined{for(leti:number0;i_notes.length;i){if(_notes[i].idid)return_notes[i]}returnundefined}返回Note | undefined——找不到时返回 undefined。调用方需要处理 undefined 情况如 NoteEditor 的if (this.note)判断。函数六getAllTags — 标签统计exportfunctiongetAllTags():TagCount[]{constresult:TagCount[][]for(leti:number0;i_notes.length;i){for(letj:number0;j_notes[i].tags.length;j){constt:string_notes[i].tags[j]letfound:booleanfalsefor(letk:number0;kresult.length;k){if(result[k].namet){result[k].count;foundtrue;break}}if(!found){result.push({name:t,count:1})}}}returnresult}三重循环的频率统计模式。外层遍历笔记中层遍历标签内层检查是否已统计。这是计数器的标准实现——已存在则 1不存在则新增。6 个查询函数的复杂度对比函数遍历层级时间复杂度返回类型getNotes无遍历O(1)Note[]引用getNotesByNotebook单层O(n)Note[]新数组getFavorites单层O(n)Note[]新数组searchNotes单层O(n×m)Note[]新数组getNoteById单层O(n)Note | undefinedgetAllTags三层O(n×m×k)TagCount[]新数组n笔记数m每篇笔记的标签/内容长度k不重复标签数。五、3 个修改函数的设计addNote — 新增笔记exportfunctionaddNote(title:string,content:string,nbId:number,tags:string[]):void{_nextIdconstd:string2026-07-15constn:Note[][{id:_nextId,title:title,content:content,notebookId:nbId,tags:tags,favorited:false,createdAt:d,updatedAt:d}]for(leti:number0;i_notes.length;i){n.push(_notes[i])}_notesnfor(leti:number0;iNOTEBOOKS.length;i){if(NOTEBOOKS[i].idnbId)NOTEBOOKS[i].noteCount}}四个关键操作步骤操作作用1_nextId生成唯一 id2创建新数组n新笔记放最前面保证新笔记在列表顶部3_notes n替换引用触发 State 刷新4NOTEBOOKS[i].noteCount同步更新笔记本笔记计数新笔记放数组最前面——用户新建后立即在列表顶部看到。如果 push 到末尾用户需要滚动到最后才能看到新笔记。_nextId 从 100 开始——和 mock 数据的 id 1-12 不冲突。如果从 1 开始新增笔记的 id 可能和 mock 数据重复。deleteNote — 删除笔记exportfunctiondeleteNote(id:number):void{constr:Note[][]for(leti:number0;i_notes.length;i){if(_notes[i].id!id)r.push(_notes[i])}_notesr}按 id 过滤——创建新数组跳过要删除的项。和 addNote 的创建新数组 替换引用模式一致。踩坑记录最初 deleteNote 忘了更新 noteCount删除笔记后笔记本卡片的笔记数量没变。加了 noteCount-- 逻辑后解决。toggleFav — 切换收藏exportfunctiontoggleFav(id:number):boolean{for(leti:number0;i_notes.length;i){if(_notes[i].idid){_notes[i].favorited!_notes[i].favoritedreturn_notes[i].favorited}}returnfalse}直接修改对象属性——和 addNote/deleteNote 的创建新数组模式不同。因为 favorited 是布尔值修改属性后 _notes 的引用不变但对象内容变了。调用方通过返回值更新 State。返回 boolean 让调用方直接赋值。this.fav toggleFav(id)一行代码完成操作和状态同步。三个修改函数的模式对比函数修改方式是否创建新数组是否更新 noteCount返回值addNote创建新数组 替换引用是是1voiddeleteNote创建新数组 替换引用是是-1voidtoggleFav直接修改对象属性否否booleanaddNote 和 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一看就知道按笔记本获取笔记不需要看实现。