尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

【天体运行模拟|06】HarmonyOS ArkTS 实验记录实战:保存参数快照并支持复现实验

【天体运行模拟|06】HarmonyOS ArkTS 实验记录实战:保存参数快照并支持复现实验 一次天体模拟结束后用户真正想留下的不是“我在某个时间点运行过稳定双体系统”而是当时使用了哪些初始条件、生成了哪些天体、时间倍率是多少、轨迹是否开启以及以后能否从这条记录恢复出同一组输入。若记录层只保存一段面向人阅读的摘要列表页虽然能展示历史却无法可靠复现实验。“天体运行模拟”的真实源码已经完成记录链路的第一阶段ExperimentSimPage.ets在进入结果分析前调用persistRecord()buildRecord()生成可 JSON 序列化对象DataStore.appendRecord()写入 PreferencesExperimentRecordsPage.ets再加载、分类筛选和删除记录。模型层还刻意把持久化结构StoredExperimentRecord与带Resource图标的视图结构ExperimentRecord分开这是很值得保留的设计。但必须先把能力边界说清楚当前版本保存的是实验 ID、名称、分类、摘要和分钟级时间并没有保存完整参数快照记录列表也没有点击“再次运行”的路由。因此本文一半复核现有实现一半给出从“可浏览历史”升级到“可复现实验”的强类型方案不把建议代码描述成已经上线的功能。唯一复核标记RECORD-ONE13-SNAPSHOT-REPLAY-20260726摘要用于展示结构化快照用于复现持久化模型不能直接保存 Resource。验证基线与源码范围本文面向 HarmonyOS 5.0 及以上版本。被复核工程的应用版本为1.0.0targetSdkVersion为6.0.2(22)compatibleSdkVersion为6.0.1(21)入口模块支持phone、tablet、2in1。实验记录相关验证范围如下文件本文复核点model/ExperimentRecord.ets存储模型、视图模型、图标补全、记录构建views/experiment/ExperimentSimPage.ets记录触发时机与当前摘要内容utils/DataStore.etsPreferences 读写、头插记录与异常处理views/mine/ExperimentRecordsPage.ets加载、筛选、空状态、编辑与删除model/Experiment.ets根据experimentId恢复展示图标静态源码复核结果为记录模型有 7 个 JSON 字段视图模型在此基础上增加 1 个Resource字段存储键固定为experiment_records新增记录使用unshift()放到列表首位记录页提供 5 个筛选项当前复现入口为 0 个当前结构化参数快照字段为 0 个。最后两项不是缺陷统计而是后续设计必须面对的事实。一、真实记录链路从结果按钮开始模拟页的“查看结果分析”按钮先保存记录再增加实验次数最后跳到结果页.onClick(() { this.persistRecord() DataStore.incrementExperimentCount() router.pushUrl({ url: views/experiment/ExperimentResultPage, params: { expId: this.expId, expName: this.title, bodyCount: this.bodies.length, stabilityScore: Math.round(this.getStabilityScore()), fastestSpeed: Number(this.getFastestSpeed().toFixed(2)), totalMass: Math.round(this.getTotalMass()), speedScale: Number(this.speed.toFixed(1)) } }) })这段顺序意味着“查看结果”同时承担“完成一次实验”的业务语义。它不是定时自动保存也不是每次修改参数都保存。优点是记录数量可控缺点是用户只要没进入结果页本次运行就不会留下历史。现有persistRecord()生成的摘要为const summary 天体 ${this.bodies.length} 个时间倍率 ${this.speed.toFixed(1)}x 轨迹${this.showTrail ? 开启 : 关闭}摘要适合卡片快速阅读但它没有包含每个天体的质量、位置、速度、半径也没有保存参数面板的paramValues。所以它不能反向解析成完整初始条件。二、存储模型与视图模型为什么要分开源码定义了两个结构export interface StoredExperimentRecord { id: string experimentId: string experimentName: string category: string sceneName: string paramSummary: string timestamp: string }export interface ExperimentRecord { id: string experimentId: string experimentName: string category: string icon: Resource sceneName: string paramSummary: string timestamp: string }Preferences 最终保存字符串JSON 只适合字符串、数字、布尔值、数组和普通对象。HarmonyOS 的Resource是运行时资源引用不应直接进入 JSON。当前实现只在读取后通过storedToRecord()补回图标边界是正确的export function storedToRecord( s: StoredExperimentRecord ): ExperimentRecord { const exp getAllExperiments() .find(e e.id s.experimentId) return { ...s, icon: exp ? exp.icon : fallbackIcon() } }这也是记录升级时的第一条原则持久化快照只保存值不保存Resource、页面对象、Canvas 上下文、定时器或回调函数。三、回退图标保证旧记录仍然可显示如果用户保存了一条记录后来实验目录删除或改名getAllExperiments()可能找不到对应 ID。源码没有让列表崩溃而是使用function fallbackIcon(): Resource { return $r(app.media.ic_orbit_stable) }这保证了记录仍可渲染。不过回退图标只能解决展示问题不能证明旧记录仍可复现。真正的兼容策略还要回答旧experimentId是否还能映射到新场景。快照字段缺失时用什么默认值。参数单位或含义变化后如何迁移。无法复现时是否只允许查看摘要。因此记录状态最好显式区分replayable、summaryOnly和unsupported而不是只根据图标是否存在来推断。四、buildRecord()当前保存了什么源码在构建记录时生成分钟级时间const now new Date() const pad (n: number): string n 10 ? 0 n : n const ts now.getFullYear() - pad(now.getMonth() 1) - pad(now.getDate()) pad(now.getHours()) : pad(now.getMinutes())记录 ID 使用id: r_ now.getTime()这种方式简单单设备普通点击下几乎不会冲突但它有三个限制时间字符串没有秒和时区不适合精确排序或跨区解释。ID 与本机时钟绑定时钟回拨会让顺序产生歧义。sceneName始终保存为空字符串当前没有形成实际语义。更稳的做法是同时保存数值时间createdAt显示时再格式化记录 ID 可以保留时间前缀再增加本地递增序号或随机片段。五、Preferences 适合这类轻量记录DataStore.loadRecordsT()从固定键读取 JSONstatic async loadRecordsT(): PromiseT[] { const json await DataStore.getString( experiment_records, [] ) try { return JSON.parse(json) as T[] } catch { return [] } }appendRecordT()读取全部记录、头插新对象再整体写回static async appendRecordT(record: T): Promisevoid { try { const list await DataStore.loadRecordsT() list.unshift(record) await DataStore.putString( experiment_records, JSON.stringify(list) ) } catch (_) { } }对于几十条、每条几百字节的本地学习记录Preferences 足够轻便。若快照升级为大量天体轨迹点整体 JSON 会快速膨胀每次新增都需要读取和重写全部内容这时应转向关系型数据库或文件存储。存储选型取决于数据规模而不是“历史记录”这个名字。六、现有记录页已经覆盖哪些状态ExperimentRecordsPage在aboutToAppear()与onPageShow()都调用loadRecords()。这样从模拟页返回后可以重新读取最新记录同时页面首次进入也能加载数据。页面状态包括records当前视图记录。selectedCategory筛选索引。isEditing删除模式。空列表时的提示。编辑模式下的删除按钮。当记录全部删除后源码主动退出编辑模式if (this.records.length 0) { this.isEditing false }这个细节避免了空页面仍显示“完成”的状态错位。筛选函数也保持纯粹private filteredRecords(): ExperimentRecord[] { const cat this.filterCategories[ this.selectedCategory ] if (cat 全部) { return this.records } return this.records.filter( r r.category cat ) }七、当前实现为什么还不能复现实验复现至少要求“从记录恢复输入”。但现有记录只保存experimentId用于展示的名称与分类一段paramSummary时间模拟页真正的运行状态还包括speed、showTrail、showGravity、paramValues以及bodies中每个天体的位置、速度、质量、半径和存活状态。摘要中的“天体 3 个”无法还原三个天体分别是什么“时间倍率 1.0x”也无法还原初始质量与速度。记录列表的非编辑状态只显示⋮没有onClick()跳回模拟页。因此当前能力应准确描述为“保存和浏览实验摘要”而不是“完整复现实验”。八、快照必须保存结构化值推荐新增纯数据结构export interface BodySnapshot { name: string type: string x: number y: number vx: number vy: number mass: number radius: number color: string alive: boolean } export interface SimulationSnapshot { speedScale: number showTrail: boolean showGravity: boolean parameterValues: number[] bodies: BodySnapshot[] }然后让存储记录持有快照export interface StoredExperimentRecordV2 { schemaVersion: 2 id: string experimentId: string experimentName: string category: string paramSummary: string createdAt: number snapshot: SimulationSnapshot }paramSummary仍然保留因为它适合列表展示snapshot则只用于恢复。展示文本与机器数据各司其职。九、快照时机要固定在用户确认完成处如果在每个动画帧保存数据写入会干扰模拟性能如果只在页面销毁时保存系统终止或异常退出可能丢失意图不明确的中间态。现有“查看结果分析”按钮是一个清晰的提交点可以继续作为快照时机。构建快照时应复制数组不能把可变bodies引用直接交给异步持久化private buildSnapshot(): SimulationSnapshot { const bodies: BodySnapshot[] this.bodies.map((body: Body) ({ name: body.name, type: body.type, x: body.x, y: body.y, vx: body.vx, vy: body.vy, mass: body.mass, radius: body.radius, color: body.color, alive: body.alive })) return { speedScale: this.speed, showTrail: this.showTrail, showGravity: this.showGravity, parameterValues: [...this.paramValues], bodies } }轨迹数组是否保存要谨慎。若目标是恢复初始条件就不必保存不断增长的trailX/trailY若目标是回放过程则需要独立的采样、压缩和容量策略。十、从记录进入模拟页的路由契约当前模拟页只接收interface SimRouterParams { expId?: string expName?: string }不要把完整快照塞进路由参数。更稳的入口只传记录 IDinterface SimRouterParams { expId?: string expName?: string replayRecordId?: string }记录页点击后router.pushUrl({ url: views/experiment/ExperimentSimPage, params: { replayRecordId: record.id } })模拟页根据 ID 从DataStore读取记录、校验版本再恢复状态。这样路由参数短小页面刷新或重建时仍可重新加载避免大对象序列化和参数过期。十一、恢复顺序决定复现是否稳定模拟页当前aboutToAppear()会先读取路由参数再调用resetSystem()。支持快照后顺序应明确读取replayRecordId。加载并校验记录。若记录可复现恢复快照。若只有摘要则按experimentId进入默认场景。加载收藏状态。刷新显示并绘制 Canvas。伪代码如下private async prepareSimulation(): Promisevoid { const params router.getParams() as SimRouterParams | undefined if (params?.replayRecordId) { const record await this.recordService .findById(params.replayRecordId) if (record?.snapshot) { this.restoreSnapshot(record.snapshot) return } } this.applyRouteExperiment(params) this.resetSystem() }不能先恢复快照再调用resetSystem()否则刚恢复的天体会被默认场景覆盖。十二、恢复前必须做边界校验本地数据也不能盲目信任。应用升级、异常写入或旧版本格式都可能产生非法值。最低校验包括function isFiniteNumber(value: number): boolean { return Number.isFinite(value) } function validateBody(body: BodySnapshot): boolean { return body.name.length 0 body.mass 0 body.radius 0 isFiniteNumber(body.x) isFiniteNumber(body.y) isFiniteNumber(body.vx) isFiniteNumber(body.vy) }还应限制天体数量上限防止一次恢复创建过多对象。质量、半径和速度的合理范围。颜色字符串格式。speedScale的最小值和最大值。参数数量与对应实验定义是否匹配。校验失败时应降级为默认场景并给出可理解提示不能让 Canvas 进入NaN状态。十三、版本迁移让旧记录继续可读当前记录没有schemaVersion。升级 V2 后加载函数应兼容旧数组type StoredRecord StoredExperimentRecord | StoredExperimentRecordV2 function migrateRecord( record: StoredRecord ): StoredExperimentRecordV2 | undefined { if (schemaVersion in record record.schemaVersion 2) { return record } // V1 没有完整快照只能保留为摘要记录 return undefined }实际产品不应直接丢弃 V1。可以在统一视图模型中标记export type ReplayState | replayable | summaryOnly | unsupported旧记录继续显示摘要操作区显示“按默认参数再次运行”而不是假装能恢复原始输入。十四、删除操作应该收口到数据服务记录页当前直接执行const stored await DataStore.loadRecordsStoredExperimentRecord() const next stored.filter(s s.id ! id) await DataStore.putString( experiment_records, JSON.stringify(next) )这段能工作但页面知道了存储键和 JSON 细节。新增复现、迁移、容量限制后建议建立ExperimentRecordServiceexport class ExperimentRecordService { async list(): PromiseStoredExperimentRecordV2[] { // load, parse, migrate, validate return [] } async append( record: StoredExperimentRecordV2 ): Promisevoid { // validate, limit, save } async remove(id: string): Promisevoid { // read, filter, save } async findById( id: string ): PromiseStoredExperimentRecordV2 | undefined { return undefined } }页面只处理用户交互服务层负责记录规则Preferences 访问继续留在DataStore。十五、异常不能全部静默吞掉appendRecord()当前捕获异常后不返回结果。用户点击“查看结果分析”时即使记录写入失败页面也会正常跳转用户可能以为历史已保存。推荐返回明确结果export interface SaveRecordResult { ok: boolean reason?: serialize | storage | capacity }模拟页可以选择不阻断结果页跳转但应给出轻量提示const result await this.recordService.append(record) if (!result.ok) { promptAction.showToast({ message: 结果可查看但本次记录保存失败 }) }错误日志应只记录错误类型和记录 ID不记录可能包含用户自定义内容的完整快照。十六、并发追加需要避免覆盖当前追加流程是“读全部、修改数组、写全部”。若用户快速重复点击两个异步操作可能都读取旧数组后写入的一次会覆盖另一次。页面层首先应防重复提交State private isSaving: boolean false private async saveAndOpenResult(): Promisevoid { if (this.isSaving) return this.isSaving true try { await this.persistRecord() this.openResult() } finally { this.isSaving false } }服务层还可以串行化写操作。对于本地单页面应用一个 Promise 队列已足够不必引入复杂锁。按钮保存期间应进入禁用或加载状态避免用户误触。十七、容量策略要在快照上线前确定当前每条记录很小源码没有上限。增加天体快照后推荐明确最多保留多少条例如最近 100 条。单条最多多少天体。是否保存轨迹采样。超限时删除最旧记录还是拒绝保存。用户清空记录后是否立即释放存储。简单的截断策略const MAX_RECORDS: number 100 list.unshift(record) const next list.slice(0, MAX_RECORDS)上限要结合实测序列化大小决定文章中的 100 只是设计示例不是当前源码已有常量。十八、分类筛选要处理目录演进记录页目前固定五个筛选项[ 全部, 基础认知, 轨道探索, 多体系统, 高级实验 ]记录保存了当时的category这有利于保留历史语义但如果产品改名为“高级挑战”旧记录会落在筛选之外。可以选择两种策略历史保真显示记录创建时的分类并动态生成筛选项。当前归类读取时根据experimentId映射最新分类。前者适合审计后者适合普通用户浏览。无论选哪一种都应把规则写进服务层避免列表页到处拼接字符串。十九、多设备布局不应改变记录语义应用声明支持手机、平板和 2in1。记录模型在三类设备上应完全一致只有列表布局变化手机使用单列列表与底部安全区。平板可增加摘要宽度或双栏详情。2in1 可支持鼠标悬停、键盘删除与确认弹窗。快照坐标如果直接保存 Canvas 像素会受画布尺寸影响。更适合复现的方式是保存归一化坐标normalizedX x / canvasWidth normalizedY y / canvasHeight恢复时再乘以当前画布尺寸。质量、速度等物理量保持原值屏幕尺寸只影响显示映射。二十、最小测试矩阵升级后的记录能力至少覆盖场景预期结果首次进入记录页显示空状态不进入编辑模式完成一次实验新记录位于首行连续快速点击结果只保存一次或按明确策略保存分类筛选只显示匹配分类删除最后一条回到空状态并退出编辑模式读取损坏 JSON降级为空列表并记录错误类型读取 V1 记录保留摘要标记summaryOnly读取 V2 快照恢复参数、天体与开关快照值越界拒绝恢复并回到默认场景实验 ID 已删除使用回退图标禁用精确复现手机到平板复现归一化位置保持相对布局当前源码可复核通过的是空状态、加载、分类、删除、最后一条删除后退出编辑模式以及损坏 JSON 返回空数组V2 快照和精确复现属于本文建议需实现后再做设备测试。二十一、发布前检查清单存储模型只包含 JSON 可序列化字段。Resource只在视图模型中恢复。摘要与结构化快照分开保存。记录具有schemaVersion和数值创建时间。复现入口只传记录 ID不传大型快照。恢复前校验天体数量和数值范围。V1 记录可以继续浏览。保存失败有状态反馈。快速重复点击不会互相覆盖。记录条数和单条体积有上限。清空、删除与设置页使用同一服务。手机、平板、2in1 使用同一记录语义。二十二、总结现有代码已经建立了一条清晰的基础链路模拟页在结果入口构建记录Preferences 保存纯 JSON读取后通过实验目录补回Resource记录页完成加载、筛选、空状态和删除。这里最有价值的设计是没有把 HarmonyOS 资源对象硬塞进持久化数据。要从“历史摘要”走到“可复现实验”关键不是把paramSummary写得更长而是增加带版本号的结构化快照固定提交时机建立恢复校验和迁移策略并让记录页通过记录 ID 进入模拟页。摘要服务于人快照服务于程序只有两者分工明确实验记录才既可读、可迁移也真正可复核。说明本文基于真实 HarmonyOS/ArkTS 源码进行整理部分文字与示例由 AI 辅助生成所有现有能力与建议改造已明确区分。
返回列表