HarmonyOS知识库——开发实战总结与设计决策复盘 📅 2026/7/15 15:14:28 知识库 App 的 6 个页面和 9 个操作函数已经全部实现。这篇文章不重复讲解代码细节而是从开发者做了什么选择、为什么这样选、有没有更好的方案三个角度做整体复盘。完整效果一、开发顺序与页面依赖1. NoteService.ets 数据层接口 mock 数据 操作函数 2. Index.ets 首页仪表盘 底部导航 3. NoteEditor.ets 编辑器新建/编辑双模式 标签管理 4. NoteList.ets 列表页多源数据 搜索过滤 5. TagFilterPage.ets 标签筛选双状态切换 6. ProfilePage.ets 个人中心标签云 统计 7. FavoritesPage.ets 收藏页收藏列表为什么先做数据层NoteService 是所有页面的基础——没有接口定义和 mock 数据页面无法渲染任何内容。先做数据层有两个好处第一接口定义决定了页面的 UI 结构。Note 的 8 个字段直接决定了编辑器要输入什么、列表页要显示什么、详情页要展示什么。先写接口再做页面能避免页面写好了发现数据结构不够用的返工。第二操作函数的返回值设计影响页面的状态管理。toggleFav 返回 booleanaddNote 返回 void——这两个设计决策在数据层就确定了页面层直接沿用。为什么 NoteEditor 第二做NoteEditor 是唯一的数据写入页面——新建和编辑都通过它完成。先做 NoteEditor 能验证 addNote/deleteNote/toggleFav 三个修改函数是否正确。如果先做列表页列表永远只显示 mock 数据无法测试新建/编辑后列表是否更新。为什么 FavoritesPage 最后做FavoritesPage 只读取getFavorites()——不需要任何新操作函数。它的实现依赖 toggleFav 在 NoteEditor 里正常工作。如果先做 FavoritesPage收藏列表永远是空的没有地方切换收藏状态。二、数据层的六个设计决策决策一Note 接口 favorited 字段直接存笔记上// 知识库favorited 存在 Note 上interfaceNote{favorited:boolean}// 替代方案独立收藏数组let_favs:number[][]选择理由12 篇笔记的规模下直接存在 Note 上更直观——看到一篇笔记就知道它是否被收藏。查询收藏只需要getFavorites()单层遍历不需要交叉匹配。适用场景数据量小 100 条、收藏是笔记固有属性的场景。如果收藏需要跨多个数据类型共享比如同时收藏笔记和笔记本独立数组更合适。决策二Notebook 保留 noteCount 冗余字段interfaceNotebook{noteCount:number}选择理由noteCount 可以从 _notes 里实时计算但每次显示笔记本列表都遍历 12 篇笔记统计数量效率不高。保留 noteCount 是空间换时间——多存一个数字避免每次查询都遍历。代价addNote 和 deleteNote 必须同步更新 noteCount。忘了更新就会数据不一致。踩坑记录最初 deleteNote 忘了NOTEBOOKS[i].noteCount--删除笔记后笔记本卡片的笔记数量没变。加了同步逻辑后解决。决策三getAllTags 用三重循环统计for(leti0;i_notes.length;i){// 遍历笔记for(letj0;j_notes[i].tags.length;j){// 遍历标签for(letk0;kresult.length;k){// 检查是否已统计选择理由三重循环是最直观的频率统计实现——不需要引入 Map 或 Set 等额外数据结构。在 12 篇笔记、约 15 个不重复标签的规模下总执行次数约 540 次性能完全不是问题。替代方案用Mapstring, number做计数器时间复杂度降到 O(n×m)。但 ArkTS 对 Map 的支持不如 TypeScript 完善用 for 循环更可靠。决策四addNote 新笔记放数组最前面constn:Note[][{id:_nextId,/* 新笔记 */}]for(leti0;i_notes.length;i){n.push(_notes[i])}_notesn选择理由新建笔记后用户期望立即在列表顶部看到。如果 push 到末尾用户需要滚动到最后才能看到新笔记。和 deleteNote 的对比deleteNote 是过滤重建跳过要删除的项addNote 是头插重建新项放最前面。两种操作都创建新数组并替换引用。决策五searchNotes 用 indexOf 不用 includesif(_notes[i].title.toLowerCase().indexOf(q)0||...)选择理由indexOf 在 ArkTS 里的兼容性更好。includes 是 ES6 方法某些 ArkTS 版本可能不支持。indexOf 是 ES1 方法所有版本都支持。代价indexOf(q) 0比includes(q)稍微啰嗦。但兼容性比代码简洁更重要。决策六_nextId 从 100 开始let_nextId:number100选择理由mock 数据的 id 是 1-12从 100 开始保证新增笔记的 id 不和 mock 数据冲突。如果从 13 开始id 连续性更好但 100 是一个更明显的分界线——id 100 是 mock 数据id 100 是用户新建的数据。三、页面层的四个关键技术技术一NoteEditor 的编辑副本模式Statetitle:string// 编辑副本Statenote:Note|undefined// 原始数据// 编辑时修改副本.onChange((v:string){this.titlev})// 保存时写回原始数据this.note.titlethis.title.trim()为什么需要编辑副本用户可能修改了但没保存——点返回取消编辑。如果直接修改 note.title取消编辑后原始数据也被改了。用独立的 title/content 存编辑副本只有点保存才写回 note。只在编辑模式需要复制 tags 数组。title/content 是基本类型赋值时复制值。tags 是数组赋值时复制引用——直接this.tags this.note.tags会导致编辑器里添加/删除标签影响原始数据。技术二NoteList 的多源数据切换this.notesid0?getNotes():getNotesByNotebook(id)一行代码处理两种数据源。notebookId-1 显示全部笔记其他值显示该笔记本的笔记。搜索时调用 searchNotes清空搜索时调用 getNotes 恢复。三种数据源最终走到同一个 ForEach。不管数据从哪来渲染逻辑完全一样。这是数据源和渲染分离的架构优势。技术三TagFilterPage 的双状态切换if(this.selectedTag.length0){// 标签网格}else{// 筛选笔记列表}if-else 控制两种完全不同的 UI 结构。选中标签前显示 Flex 标签网格选中后显示 Column 笔记列表。状态切换不需要页面跳转——同页面内完成。和显示/隐藏的区别显示/隐藏用visibility或opacity两种 UI 同时存在但只显示一种。条件渲染是只渲染当前需要的 UI性能更好。技术四build 里直接调查询函数Text(累计 getNotes().length 篇笔记)ProfilePage 在 build 里直接调用getNotes().length——不缓存到 State。因为 ProfilePage 没有其他 State 变化每次 build 重新查询的性能开销可以忽略。和首页的对比首页用State notes缓存笔记列表因为首页有搜索等其他 State 变化会触发 build。ProfilePage 没有这种需求直接查询更简洁。四、状态管理的三种模式模式一直接查询赋值// ProfilePagethis.tagsgetAllTags()// NoteListthis.notesgetNotesByNotebook(id)适用于查询一次、渲染多次的场景。查询结果赋值给 StateUI 根据 State 渲染。模式二操作函数 重新查询// NoteEditor 保存后addNote(this.title,this.content,this.selectedNb,this.tags)router.back()// 返回后 onPageShow 重新查询适用于先修改数据、再刷新页面的场景。操作函数修改 _notes返回后 onPageShow 重新查询最新数据。模式三操作函数返回值赋值// NoteEditor 收藏切换this.favtoggleFav(this.note.id)适用于操作后只需要一个状态值的场景。toggleFav 返回 boolean直接赋值给 State。三种模式的选择标准场景模式例子初始加载数据直接查询赋值aboutToAppear 里调 getNotes()修改数据后刷新列表操作函数重新查询addNote 后 router.back 触发 onPageShow切换单个状态返回值赋值toggleFav 返回 boolean五、辅助函数的重复与抽取nbName/nbIcon/nbColor 三个辅助函数在 NoteList、FavoritesPage、TagFilterPage 三个页面重复出现。当前状态各自定义三个页面各写了一份代码有重复但每个页面自包含。可选方案抽取到 NoteServiceexportfunctiongetNotebookName(id:number):string{...}exportfunctiongetNotebookIcon(id:number):string{...}exportfunctiongetNotebookColor(id:number):string{...}三个页面直接导入调用。为什么当前没抽取因素分析重复量3 个函数 × 3 个页面 9 处重复函数复杂度每个函数只有 1 行逻辑页面数量只有 3 个页面用到维护成本改一个函数需要同步改 3 个文件在 3 个页面的规模下重复代码的维护成本可以接受。如果增加到 5 页面都用到再抽取也不迟。六、已知问题与优化方向问题位置影响优化方案删除笔记无确认弹窗NoteEditor误触删除无法恢复加 AlertDialog 确认标题为空保存无提示NoteEditor用户不知道保存失败加 Toast 提示搜索无防抖NoteList快速输入时频繁查询加 300ms 防抖noteCount 可能不一致NoteService删除后数量没更新已修复但需测试数据不持久化NoteService重启后数据清空引入 Preferences 存储标签无删除功能NoteEditor只能添加不能删除标签已实现删除点击标签 ×优先级排序数据持久化 删除确认弹窗 搜索防抖 标题为空提示。数据持久化影响用户体验最直接——重启后所有笔记丢失。七、项目复杂度评估维度数据页面数量6 个接口数量3 个Note, Notebook, TagCount操作函数9 个6 查询 3 修改State 变量最多 8 个NoteEditorBuilder2 个Stat, Nav路由路径11 条辅助函数3 个nbName/nbIcon/nbColor3 页面重复复杂度集中在数据层和路由层。数据层有 9 个操作函数管理静态查询和动态修改路由层有 11 条路径、4 种参数类型。页面层相对简单——大多数页面是导航栏 列表的标准结构。八、知识库 App 的核心设计模式模式一多入口单出口所有页面最终都汇聚到 NoteEditor——它是唯一的写入页面。其他页面都是读取或筛选。这种设计让数据写入集中管理降低了数据不一致的风险。模式二参数驱动页面状态路由参数决定目标页面的行为——noteId 决定新建/编辑notebookId 决定筛选范围tag 决定标签筛选。同一个页面靠参数呈现不同状态减少了页面数量。模式三查询函数即数据源页面不自己管理数据——所有数据来自 NoteService 的查询函数。getNotes()、getFavorites()、searchNotes() 是三个独立的数据源页面按需调用。数据层和页面层的职责清晰分离。模式四State 刷新的统一模式所有 State 刷新都遵循创建新数组 替换引用的模式。addNote、deleteNote、removeTag 都是创建新数组后赋值给 State。toggleFav 是唯一用修改属性 返回值赋值的例外。