Harmony os 技术实战|拼豆制图37:最近生成记录如何做到去重、置顶与限长 用户每生成或导出一张自制图页面都会把它放进“最近生成”。这个列表看似只是数组头插但实际要同时满足四个规则只记录用户生成内容、当前项排在最前、同一 ID 不重复、最多保留 10 条。若分散成多次push、filter和slice收藏页、作品页和导出流程很容易得到不同顺序。拼豆制图把规则集中在savePatternRecord()中。本篇逐行验证现有循环的边界说明为什么它最多得到 10 条而不是 11 条并进一步处理异步持久化、重复对象版本和内置图收藏等容易被忽略的数据语义。一、先把业务不变量写成一句话最近记录的期望可以表达为next [current] old 中所有 id ! current.id 的项 next 最多取前 10 条 只有 user-generated 开头的图案进入最近记录 当前图案无论是否自制都可以加入收藏这里“最近记录”和“收藏”是两个集合不能因为一次操作同时更新就混成同一规则。内置图可以收藏但不应出现在“我的生成”用户生成图可以既在最近列表又在收藏。二、现有实现如何在一次循环中完成三件事核心代码privatesavePatternRecord(pattern:Pattern):void{if(pattern.id.indexOf(user-generated)0){constnextRecords:Pattern[][pattern];for(leti0;ithis.generatedRecords.length;i){if(this.generatedRecords[i].id!pattern.idnextRecords.length10){nextRecords.push(this.generatedRecords[i]);}}this.generatedRecordsnextRecords;}if(!this.isFavorite(pattern.id)){this.favoriteIdsthis.favoriteIds.concat([pattern.id]);}}循环从[pattern]开始因此当前项天然置顶遇到相同 ID 就跳过因此不会重复只有nextRecords.length 10才追加因此最终长度不超过 10。最后一次从 9 追加到 10后续项全部跳过。三、为什么条件顺序不会产生第 11 条以旧列表已有 10 条且当前项不在其中为例初始 next 长度1 追加旧记录 1..9长度依次到 10 旧记录第 10 条next.length 10 为 false不追加 最终10若当前项原本位于旧列表第 4 位初始放入新版 current 旧 1、2、3 依次追加 旧第 4 项同 ID被跳过 继续追加后续项直到总长 10因此它不仅去掉重复还用传入的新对象替换旧对象。若同一 ID 的标题、统计或格子内容发生更新置顶项会携带最新数据。四、ID 前缀是来源协议不应到处复制当前来源判断为pattern.id.indexOf(user-generated)0同一前缀还用于收藏列表中决定是否加入当前生成图。字符串协议简单但若未来 ID 改成 UUID这些判断会同时失效。更稳妥的模型是显式来源字段typePatternOriginbuilt-in|user;interfacePattern{id:string;origin:PatternOrigin;// ...}保存逻辑变为pattern.origin user。业务 ID 只负责身份来源字段负责分类二者不再互相推断。若暂时不能改模型也应抽出isUserPattern(pattern)避免多个页面各写一遍前缀判断。五、收藏去重与最近记录去重要共享同一身份收藏只保存 IDprivateisFavorite(id:string):boolean{returnthis.favoriteIds.indexOf(id)0;}if(!this.isFavorite(pattern.id)){this.favoriteIdsthis.favoriteIds.concat([pattern.id]);}最近记录保存完整Pattern但两者都用pattern.id判断身份。这样作品内容更新不会制造第二个收藏项收藏页仍可通过 ID 找到最新版记录。风险在于 ID 由Date.now()生成。如果极端情况下同一毫秒生成两个不同作品它们会被视为同一项第二个覆盖第一个。页面当前生成流程串行概率很低但数据层最好使用更强的唯一 ID 策略例如时间戳加进程内递增序号。privatestaticsequence:number0;functionnextUserId():string{returnuser-generated-${Date.now()}-${IdFactory.sequence};}六、把数组规则抽成纯函数更容易回归页面方法同时修改两份状态不方便单独验证。可以提取functionupsertRecent(current:Pattern,oldRecords:Pattern[],limit:number):Pattern[]{if(limit0)return[];constnext:Pattern[][current];for(leti0;ioldRecords.lengthnext.lengthlimit;i){if(oldRecords[i].id!current.id){next.push(oldRecords[i]);}}returnnext;}页面只负责来源判断和状态提交。纯函数参数显式包含limit可以验证 0、1、10 等边界也不会把“10”散落到界面代码。这里不建议先filter再slice再展开虽然写法短但会创建多个中间数组。当前最多 10 条差异不大显式单循环更容易看出何时停止。七、列表限长不等于存储限长当前generatedRecords是页面State应用重启后会恢复为空。未来接入持久化时要决定存什么摘要id、标题、尺寸、色数、创建时间、文件路径 完整图4900 个 chartCells 1225 个 previewCells 色卡把 10 张完整图都塞进一个 Preferences JSON 会快速膨胀也会让每次置顶都重写大量数据。更合理的是 Preferences 保存最近 ID 或摘要列表完整图纸按 ID 写入独立 JSON 文件删除第 11 条摘要时再根据收藏和引用关系决定是否删除文件。限长规则属于“最近视图”不一定意味着用户作品只能存在 10 张。若产品要长期作品库最近列表与作品仓库必须分离。八、异步持久化要防止旧写覆盖新写数组在内存中同步更新时顺序确定。接入异步存储后连续保存 A、B 可能出现B 先写完A 后写完最终磁盘回到旧状态。可在服务层串行写入privatewriteChain:PromisevoidPromise.resolve();enqueueSave(snapshot:PatternSummary[]):Promisevoid{this.writeChainthis.writeChain.then(async(){awaitthis.repository.saveRecent(snapshot);});returnthis.writeChain;}或者每次写入带updatedAt/版本号仓库拒绝旧版本覆盖新版本。页面不要一边先更新数组一边由多个点击处理器各自读取旧快照再保存。导出和生成都调用savePatternRecord()集中入口已经为后续串行化创造了条件。九、收藏列表合并还要防跨来源重复favoritePatterns()先遍历用户记录再检查当前生成图最后遍历内置图。当前生成图只有在结果中不存在同 ID 时才追加if(this.generatedPattern.id.indexOf(user-generated)0this.isFavorite(this.generatedPattern.id)!this.hasPatternInList(result,this.generatedPattern.id)){result.push(this.generatedPattern);}这一步说明“去重”不仅发生在保存时也发生在不同数据源合并时。若未来从磁盘加载记录应继续保证用户记录、当前编辑对象和内置仓库共享同一 ID 命名空间或使用{origin,id}复合身份。不要用对象引用去重。持久化反序列化后同一业务图案会变成新对象只有稳定 ID 才能跨生命周期识别。十、用小数组覆盖所有边界建议固定样本// 空旧列表 A - [A]// [A旧版] A新版 - [A新版]// [B,C] A - [A,B,C]// 10 条无 A A - A 前 9 条// 10 条且 A 在末尾 A新版 - A新版 前 9 个非 A// limit1 - [A]// limit0 - []页面验收再验证生成同一对象后多次导出不会重复生成 11 个不同对象后只显示最近 10 个收藏的内置图不会混入最近生成删除收藏不会改变最近顺序。若出现 11 条检查限长条件是否放在 push 之后若当前项重复核对 ID 是否有空格或类型差异若新内容没有替换旧内容确认初始数组放入的是传入对象而不是旧列表对象。十一、结语最近列表并不是简单的unshift()。一条可靠的更新需要同时守住来源、身份、顺序和容量。拼豆制图用“新对象先入、循环跳过同 ID、长度小于 10 才追加”的单次遍历实现了最小闭环收藏则复用相同 ID 保持跨集合一致。下一步演进的重点不是把循环改得更短而是把来源从 ID 前缀升级为字段、把数组规则提成纯函数、把最近摘要与完整作品分开持久化并让异步写入按顺序提交。这样“最近 10 条”才不会意外变成“作品最多只能有 10 条”。