当前位置: 首页 > news >正文

【open harmony/harmonyos】HarmonyOS 应用中的数据模型分层:以星图节点 Store 为例

【open harmony/harmonyos】HarmonyOS 应用中的数据模型分层:以星图节点 Store 为例

前言 🧠

在 HarmonyOS / OpenHarmony 应用开发中,很多初学项目会直接把数据、交互和 UI 写在同一个页面里。

小功能这样写没有问题,但当页面开始出现节点、连线、选中状态、缩放、旋转、删除、生成等逻辑时,如果还全部堆在组件中,代码会很快变乱。

我的项目星图 Xingtu是一个 3D 知识星图应用,里面涉及:

  • 节点管理
  • 连线管理
  • 节点选中
  • 连线模式
  • 3D 相机状态
  • 关键词生成星图
  • 节点删除与关系清理

这篇文章就以XingtuGraphStore为例,分享如何在 ArkTS 项目中做一个清晰的数据模型分层。✨

一、为什么要抽出 Store

星图页面中有很多 UI 组件:

  • 星图场景组件
  • 节点组件
  • 顶部 HUD
  • 底部导航
  • 节点详情弹层
  • 新增节点弹层
  • 词图生成弹层

这些组件都可能需要读取或修改图谱数据。

如果每个组件都自己维护一份状态,会出现很多问题:

  • 数据不同步
  • 删除节点后连线残留
  • 选中状态混乱
  • 组件之间传参复杂
  • 测试困难

所以项目把图谱相关逻辑集中到XingtuGraphStore

二、Store 的核心状态

XingtuGraphStore中保存了图谱运行所需的核心状态:

exportclassXingtuGraphStore{currentTab:'starMap'|'elements'|'graphs'|'mine'='starMap';nodes:XingtuNode[] = [];edges:XingtuEdge[] = [];selectedNodeId:string|null=null;linkingSourceId:string|null=null;camera:CameraState=defaultCamera(); }

这些状态可以分为三类:

  • 图谱数据:nodesedges
  • 交互状态:selectedNodeIdlinkingSourceId
  • 视角状态:camera

把它们放在同一个 Store 中,可以让图谱逻辑保持完整。

三、节点新增

新增节点时,Store 会创建节点、加入数组,并自动选中新节点。

addNode(draft: XingtuNodeDraft): XingtuNode {constnode: XingtuNode = createNodeAtFront(this.camera, draft);this.nodes = [...this.nodes, node];this.selectedNodeId = node.id;returnnode; }

这里有两个设计点:

  • 使用createNodeAtFront让新节点生成在当前视角前方
  • 新增后自动选中,方便用户继续编辑或连接

UI 层只需要调用store.addNode(...),不用关心节点 id、位置和选中状态怎么处理。

四、节点选择

节点选择逻辑很简单:

selectNode(nodeId: string |null): void {this.selectedNodeId = nodeId; } selectedNode(): XingtuNode |null{returnthis.nodes.find((node: XingtuNode) => node.id ===this.selectedNodeId) ??null; }

一个方法负责写入选中 id,一个方法负责返回完整节点对象。

这样弹层组件可以直接拿到:

node: this.selectedNode()

而不是自己在 UI 层遍历节点数组。

五、节点连接

图谱应用中比较重要的是节点连接。

Store 中把连接分成两步:

startLink(nodeId:string):void{this.linkingSourceId = nodeId;this.selectedNodeId = nodeId; }
finishLink(targetId: string): XingtuEdge |null{if(!this.linkingSourceId ||this.linkingSourceId === targetId) {returnnull; }constexists: boolean =this.edges.some((edge: XingtuEdge) => (edge.fromId ===this.linkingSourceId && edge.toId === targetId) || (edge.fromId === targetId && edge.toId ===this.linkingSourceId) );if(exists) {this.linkingSourceId =null;returnnull; }constedge: XingtuEdge = { id: `edge-${++edgeSequence}`, fromId:this.linkingSourceId, toId: targetId };this.edges = [...this.edges, edge];this.linkingSourceId =null;this.selectedNodeId = targetId;returnedge; }

这样 UI 层的点击逻辑会非常清晰:

  • 没有连线起点:点击节点就是选中
  • 有连线起点:点击另一个节点就是完成连线

六、删除节点时维护数据一致性

删除节点不能只删节点,还要删除相关边。

deleteNode(nodeId: string): void {this.nodes =this.nodes.filter((node: XingtuNode) => node.id !== nodeId);this.edges =this.edges.filter((edge: XingtuEdge) => edge.fromId !== nodeId && edge.toId !== nodeId );if(this.selectedNodeId === nodeId) {this.selectedNodeId =null; }if(this.linkingSourceId === nodeId) {this.linkingSourceId =null; } }

这段逻辑体现了 Store 的价值。

如果删除逻辑散落在 UI 组件里,很容易只删了节点、忘记删边,导致图谱出现脏数据。

七、相机状态也属于图谱模型

这个项目中,图谱不只是数据,还有视角。

所以相机状态也放在 Store 中:

updateCamera(deltaYaw: number, deltaPitch: number): void {this.camera = { yaw:this.camera.yaw + deltaYaw, pitch: clampPitch(this.camera.pitch + deltaPitch), distance:this.camera.distance, scale:this.camera.scale }; }

缩放也是一样:

updateScale(nextScale: number): void {this.camera = { yaw: this.camera.yaw, pitch: this.camera.pitch, distance: this.camera.distance, scale: Math.max(0.6, Math.min(2.2, nextScale)) }; }

这样星图场景组件只负责把触摸变化转换成deltaYawdeltaPitchscale,不负责管理相机内部细节。

八、投影节点对 UI 友好

UI 不应该直接处理复杂 3D 坐标,所以 Store 提供了投影后的节点:

projectedNodes(viewport: ViewportSize): ProjectedNode[] {returnthis.nodes .map((node: XingtuNode)=>projectNode(node, this.camera, viewport)) .sort((left: ProjectedNode, right: ProjectedNode)=>right.depth - left.depth); }

这样场景组件拿到的已经是screenXscreenYscaleopacity,可以直接用于渲染。

这就是分层的好处:

  • Store 负责数据计算
  • Scene 负责布局渲染
  • Node 负责单个节点显示

九、关键词生成星图

Store 还负责把用户输入的关键词转换成图谱。

generateWordMap(themeTitle:string,rawWords:string): number { const words:string[]= this.parseWordList(rawWords); const normalizedTheme:string= themeTitle.trim();if(words.length===0&&normalizedTheme.length===0) { return0; }letcenterTitle:string= normalizedTheme;letorbitWords:string[]= words;if(centerTitle.length===0) { centerTitle = words[0]; orbitWords = words.slice(1); } const centerNode: XingtuNode = createNodeAtPosition({...}, {x: 0,y: 0,z: 40 }); const generatedNodes: XingtuNode[]=[centerNode]; const generatedEdges: XingtuEdge[]=[]; orbitWords.forEach((word:string,index:number)=> { const position = this.createOrbitPosition(index,orbitWords.length); const node: XingtuNode = createNodeAtPosition({...},position); generatedNodes.push(node); generatedEdges.push(this.createEdge(centerNode.id,node.id)); }); this.nodes = generatedNodes; this.edges = generatedEdges; this.selectedNodeId = centerNode.id; this.camera = defaultCamera(); return generatedNodes.length; }

这个方法不仅生成数据,还处理了体验状态:

  • 替换当前节点和连线
  • 默认选中中心节点
  • 重置相机
  • 返回生成数量

UI 层根据返回数量决定是否切回星图页面。

十、测试 Store 比测试 UI 更稳定

数据模型分层后,可以直接测试 Store。

it('removes edgeswhena node is deleted',0,()=>{ const store =newXingtuGraphStore(false); const first = store.addNode({title: 'A',note: '',tags: [] }); const second = store.addNode({title: 'B',note: '',tags: [] }); store.startLink(first.id); store.finishLink(second.id); store.deleteNode(first.id); expect(store.nodes.length).assertEqual(1); expect(store.edges.length).assertEqual(0); expect(store.nodes[0].id).assertEqual(second.id); });

这类测试不依赖页面渲染,运行更快,也更容易定位问题。

十一、分层后的组件职责

最终项目中的职责大致是:

  • XingtuGraphStore:节点、边、相机、选中、生成逻辑
  • XingtuGraphMath:旋转、投影、坐标计算
  • XingtuScene:星图场景渲染与触摸事件
  • XingtuSceneNode:单个节点视觉
  • XingtuNodeSheet:节点详情和操作
  • XingtuCreateNodeSheet:新增节点表单
  • XingtuGenerateWordMapSheet:词图生成输入

这种结构比较适合持续扩展。

后续如果要加搜索、保存、多图谱、AI 推荐关系,都可以继续围绕 Store 扩展,而不是把页面组件越写越重。

十二、总结 🌟

这篇文章以星图项目为例,介绍了 HarmonyOS / OpenHarmony 应用中的数据模型分层思路。

核心经验是:

  • 不要把复杂业务逻辑都写进 UI 组件
  • 用 Store 管理节点、连线、选中、相机等核心状态
  • UI 组件通过方法调用修改数据
  • Store 对外提供适合 UI 使用的数据结果
  • 图谱关系逻辑要集中处理,避免数据不一致
  • 数据层逻辑适合写单元测试

对于 ArkTS 应用来说,清晰的数据模型分层不只是代码好看,更重要的是让项目后续能继续长大。✨

http://www.jsqmd.com/news/1099879/

相关文章:

  • 2026年论文查重免费网站靠谱吗?这5个平台实测对比
  • 基于STM32单片机智能窗帘窗户光敏定时遥控温湿度语音物联网设计1(设计源文件+万字报告+讲解)(支持资料、图片参考_相关定制)_
  • 09502黄大年茶思屋榜文95期 第2题 高性能、适用于NPU硬件的Training-free大模型剪枝算法
  • openGauss 还原成功了,用户却喊“数据库里啥也没有“:一个 search_path 坑实录
  • 国家标准起草单位是什么?有什么价值?企业如何申请参与国标制定
  • Claude Code 深度实战指南:从环境配置到 Agent 自动化进阶
  • 开源AI绘画工作台infinite-canvas:本地部署与高效工作流构建指南
  • SIM 卡克隆工具指南:安全移动 SIM 卡数据
  • 上门按摩APP小程序开发公司,获客新思路:酒店渠道为什么值得做
  • 如何在一部手机上实现工作与生活数据的完全隔离?
  • 如何快速构建轻量级多模态AI:3步实现模型融合的终极指南
  • 一键提取爆款短视频文案,批量采集竞品素材
  • Linux生产环境硬盘挂载:为何必须用UUID替代设备名?
  • API受限下15种LLM幻觉抑制创新方法
  • 如何利用多人协作在线表格提升团队效率?告别协作混乱与数据勒索
  • Unreal Engine 5.7 C++ 完整说明(C++ 标准、内置库、第三方库、内存 GC)
  • 微信好友上限是多少?为什么不建议好友加满?
  • VS Code十六进制编辑器终极指南:从二进制分析到专业调试
  • 课堂时间总不够用?这5个环节压缩技巧让教学节奏更从容
  • 主流AI热词总结
  • Gum:让 Shell 脚本拥有交互界面
  • Claude Opus 4.8快速模式集成GitHub Copilot:AI编码响应速度实测与提效指南
  • 2026最新智慧园区厂商挑选指南 国内哪家服务专业更靠谱?
  • 制造企业数字化转型中AI智能体的角色是什么
  • 汇编指令补充
  • 基于STM32单片机智能手环心率血氧体温GPS定位跌倒计步器系统设计1(设计源文件+万字报告+讲解)(支持资料、图片参考_相关定制)_
  • 哈夫曼树的构造、编码生成与带权路径长度计算——基于C语言的实验实现与分析 P12114068王勇豪
  • 湘美谈教育湘美书院成功学系列:AI时代的,图书的意义
  • P1375 小猫【洛谷算法习题】
  • 为什么你的vmx文件压缩后反而增大?深度解析NTFS稀疏文件、零填充与TRIM指令协同失效原理