HarmonyOS智能家居——设备数据的全链路追踪

📅 2026/7/13 13:48:53
HarmonyOS智能家居——设备数据的全链路追踪
前面几篇分别讲了每个页面怎么用数据但没有把数据的完整流转过程串起来。这篇从 DeviceData 的定义出发追踪 17 个设备数据从定义 → 读取 → 修改 → 刷新的完整链路搞清楚每个页面读了什么、改了什么、怎么触发 UI 更新。完整效果一、数据定义层DeviceData.ets 的完整结构exportinterfaceDevice{id:number;name:string;icon:string;type:string;room:string;on:booleanvalue:number;color:string}exportinterfaceRoom{name:string;icon:string;deviceCount:number;activeCount:number;color:string}exportinterfaceEnergyRecord{time:string;usage:number}exportinterfaceScene{name:string;icon:string;desc:string;active:boolean;color:string}四个接口定义了四种数据结构。逐个看每个字段的作用Device 接口是核心——8 个字段覆盖了设备的所有属性字段类型含义用途idnumber唯一标识toggleDevice/setDeviceValue 通过 id 定位设备namestring显示名称页面展示主灯空调等iconstringemoji 图标页面展示 ❄️ 等图标typestring设备类型区分 light/thermostat/appliance/curtainroomstring所属房间getDevicesByRoom 通过 room 过滤onboolean开关状态页面展示 /⚪切换后刷新valuenumber参数值亮度/温度/开合度等Slider 控制colorstring主题色卡片边框和强调色type字段决定了页面的条件渲染逻辑——type light显示亮度滑块type thermostat显示温度滑块其他类型不显示滑块。这个字段是 RoomPage 里三种设备卡片分支的依据。value字段的含义随设备类型变化灯是亮度1-100空调是温度16-30窗帘是开合度0-100冰箱是温度-18°C其他设备是 0。这种一个字段承载多种含义的设计比为每种设备定义独立字段更简洁代价是读取时需要根据 type 做类型判断。Room 接口有 5 个字段但deviceCount和activeCount是写死的——没有从 DEVICES 数组动态计算。这意味着如果 DEVICES 的设备变化了ROOMS 里的计数不会自动更新。在 mock 阶段这是可接受的真实 App 应该从 DEVICES 动态计算// 真实 App 应该这样计算而不是写死functiongetRoomDeviceCount(roomName:string):number{letcount0for(leti0;iDEVICES.length;i){if(DEVICES[i].roomroomName)count}returncount}二、数据存储方式模块级常量exportconstROOMS:Room[][{name:客厅,icon:️,deviceCount:5,activeCount:3,color:#FF6B6B},{name:卧室,icon:️,deviceCount:4,activeCount:2,color:#8A5DFA},{name:厨房,icon:,deviceCount:3,activeCount:1,color:#FF9F43},{name:书房,icon:,deviceCount:3,activeCount:2,color:#5DADE2},{name:浴室,icon:,deviceCount:2,activeCount:1,color:#00B894},]exportconstDEVICES:Device[][{id:1,name:主灯,icon:,type:light,room:客厅,on:true,value:80,color:#FFD93D},{id:2,name:空调,icon:❄️,type:thermostat,room:客厅,on:true,value:24,color:#5DADE2},// ... 共 17 个设备]exportconstENERGY_DATA:EnergyRecord[][{time:0时,usage:0.3},{time:4时,usage:0.2},{time:8时,usage:1.5},{time:12时,usage:2.1},{time:16时,usage:1.8},{time:20时,usage:2.8},{time:24时,usage:0.8},]exportconstSCENES:Scene[][{name:离家模式,icon:,desc:关闭所有设备启动安防,active:false,color:#FF6B6B},{name:回家模式,icon:,desc:开灯、空调、窗帘,active:true,color:#00B894},// ... 共 5 个场景]四个export const数组是整个 App 的数据源。关键字是export——每个导入 DeviceData 的页面都能读到同一份数据。为什么用const而不是let因为数组本身是常量不能重新赋值为另一个数组但数组的内容是可变的可以修改DEVICES[0].on false。ArkTS/TypeScript 里const只保护引用不被替换不保护对象属性不被修改。所以toggleDevice能直接写DEVICES[i].on !DEVICES[i].on不需要先把DEVICES改成let。17 个设备分布在 5 个房间每个房间 2-5 个设备。设备的id从 1 到 17 连续编号room字段用中文字符串匹配“客厅”卧室等。这种中文字符串做外键的做法在原型阶段最直观但真实 App 应该用数字 ID 关联避免中文编码问题。三、操作函数的完整实现exportfunctiontoggleDevice(id:number):boolean{for(leti:number0;iDEVICES.length;i){if(DEVICES[i].idid){DEVICES[i].on!DEVICES[i].onreturnDEVICES[i].on}}returnfalse}exportfunctionsetDeviceValue(id:number,val:number):void{for(leti:number0;iDEVICES.length;i){if(DEVICES[i].idid){DEVICES[i].valuevalreturn}}}exportfunctiongetDevicesByRoom(room:string):Device[]{constresult:Device[][]for(leti:number0;iDEVICES.length;i){if(DEVICES[i].roomroom)result.push(DEVICES[i])}returnresult}exportfunctiongetActiveCount():number{letc:number0for(leti:number0;iDEVICES.length;i){if(DEVICES[i].on)c}returnc}四个函数的实现有一个共同特点全部用 for 循环遍历数组。没有用find、filter、reduce等高阶函数。这是 ArkTS 的限制——部分高阶函数在 ArkTS 里的支持不如 TypeScript 完善用 for 循环最稳妥。toggleDevice返回 boolean——修改后的状态值。这让调用方可以根据返回值决定后续操作// 首页的调用方式.onClick((){toggleDevice(1)// 切换状态返回值被忽略this.devCountgetActiveCount()// 重新计算在线数})这里忽略了 toggleDevice 的返回值因为首页只关心在线设备总数不关心单个设备的新状态。如果需要根据新状态做不同操作比如设备关闭后弹提示可以用返回值判断。setDeviceValue返回 void——不需要返回值因为调用方RoomPage已经通过d.value Math.round(v)直接修改了引用对象的属性。注意这里有一个微妙的地方RoomPage 的 Slider 回调里同时做了两件事——修改源数据d.value Math.round(v)和刷新 Statethis.devices getDevicesByRoom(this.room)。修改源数据让 DEVICES 数组保持最新刷新 State 让 UI 重新渲染。getDevicesByRoom返回新数组——result.push(DEVICES[i])创建了一个新数组不是返回 DEVICES 的引用。这是正确的做法——如果返回 DEVICES 的过滤引用调用方修改返回值会影响原始数据。返回新数组隔离了原始数据和页面数据。getActiveCount返回数字——遍历所有设备统计on true的数量。每次调用都重新遍历整个数组时间复杂度 O(n)。在 17 个设备的规模下性能完全不是问题但如果设备增加到上千个可以考虑缓存计数。四、数据流向每个页面读写什么首页 Index.ets// 导入import{ROOMS,DEVICES,ENERGY_DATA,SCENES,Scene,toggleDevice,getActiveCount,EnergyRecord,Room}from../services/DeviceData// 读取的数据this.devCount/DEVICES.length// 设备状态卡片在线数/总数ForEach(ROOMS,...)// 房间列表DEVICES[0].on?:⚪// 快捷控制设备状态ForEach(ENERGY_DATA,...)// 能耗柱状图ForEach(this.scenes,...)// 智能场景从 SCENES 复制而来// 修改的数据toggleDevice(1)// 切换客厅主灯toggleDevice(2)// 切换客厅空调toggleDevice(3)// 切换电视toggleDevice(6)// 切换床头灯this.scenes[idx].activetrue// 激活场景首页是数据的总入口——读取了全部四组数据调用了 toggleDevice 和 getActiveCount。快捷控制的 4 个 GridItem 分别硬编码了 DEVICES[0]、DEVICES[1]、DEVICES[2]、DEVICES[5] 的 id1、2、3、6而不是用 ForEach 遍历。这种硬编码让每个 GridItem 的图标和名称是固定的不需要额外的数据映射。房间页 RoomPage.ets// 导入import{getDevicesByRoom,toggleDevice,setDeviceValue,Device}from../services/DeviceData// 读取的数据this.devicesgetDevicesByRoom(this.room)// 按房间名查询设备列表d.on// 设备开关状态d.value// 设备参数值亮度/温度// 修改的数据toggleDevice(d.id)// 切换设备开关setDeviceValue(d.id,Math.round(v))// 调节设备参数this.devicesgetDevicesByRoom(this.room)// 刷新设备列表房间页是数据的操作中心——所有设备控制都在这里完成。getDevicesByRoom根据路由参数room过滤出当前房间的设备返回一个新数组。每次操作后都重新调用getDevicesByRoom刷新 State。这里有一个关键的设计决策为什么操作后要重新查询而不是直接修改 State 数组里的属性// 方案 A当前做法重新查询d.valueMath.round(v)this.devicesgetDevicesByRoom(this.room)// 方案 B不推荐直接修改d.valueMath.round(v)// 没有 this.devices ...不触发 State 刷新方案 B 不会触发 UI 更新——因为d.value修改的是 DEVICES 数组里的对象属性不是 State 的引用。State 只检测引用变化不检测属性变化。所以必须重新查询得到新数组替换 State 的引用。安防页 SecurityPage.ets// 没有导入 DeviceData// 数据来源Statecams:Cam[][{name:大门摄像头,icon:,online:true},{name:客厅摄像头,icon:,online:true},{name:车库摄像头,icon:,online:false},]Statealarm:booleantrueStatedoorLocked:booleantrue// 修改的数据this.doorLocked!this.doorLockedthis.alarm!this.alarm安防页完全不依赖 DeviceData——摄像头列表、门锁状态、警报状态都是页面内部的 State。这意味着安防数据和设备数据是完全独立的修改设备状态不会影响安防状态反之亦然。这种数据隔离在原型阶段是合理的——安防数据摄像头、门锁、警报和设备数据灯、空调、电视在业务上确实是两个独立的域。真实 App 里可能需要关联比如离家模式触发门锁上锁但当前版本不需要。能耗页 EnergyPage.ets// 导入import{ENERGY_DATA,EnergyRecord}from../services/DeviceData// 读取的数据ForEach(ENERGY_DATA,(e:EnergyRecord){Text(e.usagekWh)// 柱状图数值Column(){}.height(e.usage*40)// 柱子高度Text(e.time)// 时间标签})// 修改的数据// 无——能耗数据只读能耗页是只读的——只导入 ENERGY_DATA 和 EnergyRecord不导入任何操作函数。页面展示的统计数据今日用电、预计本月、同比上月是硬编码的不从 ENERGY_DATA 动态计算。设置页 SettingsPage.ets// 没有导入 DeviceData// 数据来源StatenotifyOn:booleantrueStatedarkMode:booleanfalseStateautoUpdate:booleantrueStatevoiceWake:booleantrueStateenergySave:booleanfalse// 修改的数据this.notifyOn!this.notifyOnthis.darkMode!this.darkModethis.autoUpdate!this.autoUpdatethis.voiceWake!this.voiceWakethis.energySave!this.energySave设置页和安防页一样完全不依赖 DeviceData。五个开关都是页面内部的 State修改后只影响页面内的显示。五、两层状态的协作关系整个 App 有两层状态第一层DeviceData共享状态17 个设备的 on/value 属性通过操作函数修改首页和房间页读取并响应变化第二层页面 State本地状态安防页的 alarm/doorLocked设置页的 notifyOn/darkMode 等首页的 scenes从 SCENES 复制只在当前页面生效两层状态的关系可以用一个图表示DeviceData共享 ├→ Index 读取: ROOMS, DEVICES, ENERGY_DATA, SCENES │ 修改: toggleDevice(), scenes[idx].active ├→ RoomPage 读取: getDevicesByRoom() │ 修改: toggleDevice(), setDeviceValue() ├→ EnergyPage 读取: ENERGY_DATA │ 修改: 无 ├→ SecurityPage: 不读取不修改独立 State └→ SettingsPage: 不读取不修改独立 State首页和房间页共享 DeviceData 的状态变化——房间页修改设备后首页能看到变化。安防页和设置页完全独立——它们的 State 变化不会影响其他页面其他页面的变化也不会影响它们。这种部分共享、部分独立的架构适合智能家居的业务场景设备状态需要全局一致灯的开关在所有页面都一样但安防和设置是局部状态每个页面自己管理。六、数据修改的完整链路以在房间页调节客厅主灯亮度为例追踪完整的数据修改链路1. 用户拖动 Slider ↓ 2. Slider.onChange 触发回调 ↓ 3. d.value Math.round(v) ← 修改 DEVICES 数组里的对象属性 ↓ 4. this.devices getDevicesByRoom(this.room) ← 重新查询得到新数组 ↓ 5. State 检测到引用变化触发重新渲染 ↓ 6. UI 更新Slider 显示新值状态文字更新第 3 步修改的是 DEVICES 数组源数据第 4 步创建的是新数组State 副本。这两步缺一不可只做第 3 步不做第 4 步DEVICES 更新了但 UI 不刷新State 未变化只做第 4 步不做第 3 步UI 刷新了但 DEVICES 没更新下次查询还是旧值这个先改源数据、再查新数组的模式是整个项目里 State 刷新的核心范式。七、数据流的断裂点整个数据流有两个断裂点——数据不会自动同步的地方断裂点一ROOMS 的 activeCountexportconstROOMS:Room[][{name:客厅,icon:️,deviceCount:5,activeCount:3,color:#FF6B6B},// ...]activeCount是写死的 3不会随着设备开关变化。首页显示的3/5 设备永远是这个值即使用户关了客厅的所有灯。要修复这个问题需要把activeCount改成动态计算functiongetRoomActiveCount(roomName:string):number{letcount0for(leti0;iDEVICES.length;i){if(DEVICES[i].roomroomNameDEVICES[i].on)count}returncount}然后在首页的房间卡片里调用getRoomActiveCount(room.name)代替room.activeCount。断裂点二设备状态卡片的总数Text(this.devCount/DEVICES.length 在线)DEVICES.length是固定的 17this.devCount通过getActiveCount()动态计算。但如果以后设备可以动态添加或删除DEVICES.length也需要动态计算。当前版本不需要处理这个场景。这两个断裂点都是 mock 阶段的简化——数据是写死的不需要动态同步。真实 App 需要解决这些问题但不影响当前版本的功能演示。八、数据层的扩展方向如果要让这个数据层更接近真实 App需要做三件事第一引入设备状态持久化。当前的 DEVICES 是内存数据App 重启后恢复默认值。真实 App 需要将设备状态保存到本地存储Preferences或云端数据库。第二引入事件总线。当前首页和房间页通过共享 DEVICES 数组实现状态同步。如果页面数量增加需要引入事件总线EventEmitter来解耦页面间的数据依赖。第三引入数据校验。setDeviceValue没有校验参数范围——传入val 999也会直接赋值。真实 App 需要在操作函数里加参数校验exportfunctionsetDeviceValue(id:number,val:number):void{for(leti:number0;iDEVICES.length;i){if(DEVICES[i].idid){// 参数校验根据设备类型限制范围if(DEVICES[i].typelight){DEVICES[i].valueMath.max(1,Math.min(100,val))}elseif(DEVICES[i].typethermostat){DEVICES[i].valueMath.max(16,Math.min(30,val))}else{DEVICES[i].valueval}return}}}这三件事是原型 → 产品化的必经之路但不影响当前版本作为技术演示的价值。