
简介一套鸿蒙智能家居App的TypeScript源码工程面向鸿蒙应用开发初学者及物联网项目爱好者可用于学习智能家居场景下的多页面设计与交互实现。工程包含登录注册页、首页、设备页和个人页四大核心模块覆盖账号验证、注册跳转、设备管理、场景配置等常见功能并采用TypeScript强类型特性提升代码可维护性便于跨平台构建。压缩包共89个文件以ets页面文件、png/jpg图片资源和json5/json配置文件为主另有ts脚本、说明文档及gitignore等文件整体约7.91MB目录结构清晰可快速定位页面入口与资源目录。目前已有101人学习。资料完整呈现登录与注册页的无缝跳转流程并展示首页概览、设备分组、个人中心设置的具体实现可帮助读者理解页面生命周期、状态管理及UI布局思路适合需要搭建智能家居界面或学习鸿蒙多页面工程的开发者。1. 用 TypeScript 做鸿蒙智能家居 App不等生态成熟再入场的理由某物联网团队接了一个网关量产后的 App 需求设备是自有协议网关支持局域网控制买家要求三个月内上线。团队清一色前端最稳的语言就是 TypeScript。鸿蒙应用开发的首选语言是 ArkTS而 ArkTS 本身是 TypeScript 的一个受限子集这意味着用 TypeScript 源码直接写鸿蒙智能家居 App从语法上不存在跨语言阻力。这个标题真正回答的问题不是能不能写而是怎么写、坑在哪、值不值得投入。核心诉求落在设备配网、局域网控制、多设备状态同步、场景联动这几件事上。适合读的人是帮团队评估技术路线的 TL、有 TypeScript 经验但没碰过鸿蒙的开发者以及想验证自研 App 比买第三方方案更划算的 IoT 产品负责人。2. 鸿蒙智能家居的技术底座ArkTS 与 TypeScript 的关系和工程结构2.1 ArkTS 不是新语言是套着 TypeScript 壳的鸿蒙原生开发方式ArkTS 经常被误认为一门新语言实际上它保留了 TypeScript 的类、接口、泛型、装饰器、异步语法只是去掉了any、unknown这类运行时才知道类型的动态能力。鸿蒙的方舟编译器需要在编译期拿到完整类型信息才能生成高效机器码对开发者来说这份限制恰好换来更可维护的代码。智能家居 App 里到处是指令字符串、设备 ID、状态枚举如果允许any满天飞等测试阶段才暴露拼写错误会非常痛苦。选型结论也由此而来团队有 TypeScript 背景ArkTS 源码读起来零负担唯一的适应成本就是放弃any。对比 Java 或 Kotlin语法层面要重新捡的东西多得多对比跨端方案控制链路要过桥接层配网阶段要频繁切换 Wi-Fi 并监听系统网络事件桥接层性能损耗会被成倍放大。所以对智能家居这个场景TypeScript 源码直接构建是性价比最高的路径。工程上还要注意一个认知差异编译器报的类型不兼容在普通 TS 里可能只是警告在 ArkTS 里直接编译失败。把它当成一个更严格的 linter 而不是麻烦大多数报错改起来也就一两行。实际迁移的时候常见报错集中在这几类对象字面量没显式标注类型、JSON.parse返回值直接当具体类型用、回调参数没写类型。这些都是几秒钟能解决的问题真正耗时间的反而是网络层的接口设计。2.2 从零搭一个可扩展的工程目录分层与依赖注入创建鸿蒙工程的常见做法是打开开发工具选择空能力模板语言选 ArkTS。工程生成后业务代码基本是.ets结尾的 TypeScript 源码IDE 的语法高亮、自动补全和重构功能都按 TS 的方式工作。我第一次看到.ets后缀时也犹豫了一下实际用下来和.ts没有区别团队协作时统一用.ets写业务代码避免编译器对不同后缀的检查策略不一致。项目结构通常长这样smart-home/ ├─ entry/ │ └─ src/main/ets/ │ ├─ entryability/ │ │ └─ EntryAbility.ets │ ├─ pages/ │ │ ├─ IndexPage.ets │ │ ├─ DeviceListPage.ets │ │ └─ DeviceDetailPage.ets │ ├─ services/ │ │ ├─ DeviceClient.ts │ │ └─ PairingService.ts │ ├─ viewmodel/ │ │ └─ DeviceStore.ts │ └─ common/ │ ├─ constants/ │ └─ utils/目录划分的逻辑是pages 只放页面组件不直接碰网络请求services 统一封装对外通信viewmodel 放跨页面共享状态common 放常量和工具。这样划分后配网、控制、状态同步三条主链路的改动可以并行团队协作时不容易互相踩到。services 层我一般会先定义接口再写实现ArkTS 编译器对接口的约束比普通 TS 更强强行依赖具体类会带来不少编译期报错。示例// services/DeviceClient.ts // 先定义接口后续替换协议实现时页面代码不需要改 export interface IDeviceClient { control(deviceId: string, action: string, params?: Recordstring, number): Promiseboolean; getStatus(deviceId: string): PromiseRecordstring, string; } export class HttpDeviceClient implements IDeviceClient { async control(deviceId: string, action: string, params?: Recordstring, number): Promiseboolean { // 具体实现放到第 3 章控制链路里展开 return true; } async getStatus(deviceId: string): PromiseRecordstring, string { return {}; } }这段接口代码的价值不在于功能而在于把“能控制”和“怎么控制”解耦早期用模拟数据接口跑通 UI后期换真实网关协议时只动 imports。参数上deviceId 是网关里的唯一标识action 建议用字符字面量联合类型而不是 string编译期就能拦住拼写错误。依赖注入不用引入 IoC 容器。我通常在最外层创建一个 AppContext 对象把 DeviceClient、PairingService 等实例挂上去页面从 context 取用。智能家居 App 的模块数量有限IoC 容器带来的配置复杂度远大于收益。构建时如果遇到莫名其妙的报错先检查依赖缓存是不是没刷新开发工具里清缓存重新构建能解决大半玄学问题。2.3 生命周期与页面路由设备列表页到详情页的骨架页面骨架是智能家居 App 的地基。列表页按房间或场景展示设备点击设备跳详情页做控制。鸿蒙的路由方式常用router.pushUrl示例// pages/DeviceListPage.ets import { router } from kit.ArkUI; function goToDetail(deviceId: string): void { router.pushUrl({ url: pages/DeviceDetailPage, params: { deviceId: deviceId } }); }router.pushUrl的 params 会原样透传给目标页详情页在aboutToAppear里通过参数取 deviceId再调 store 拉取设备详情。这里有个关键点不要在页面构造函数里发请求而是放到aboutToAppear或onPageShow因为页面从栈里恢复时也需要刷新数据。对应的销毁生命周期里要清理定时器和取消订阅否则列表页来回切换会叠出一堆重复轮询。页面之间共享数据不要用路由参数传对象只能传可序列化的基本类型。设备状态这种频繁变化的数据应该放到第 4 章的 store 里页面只存一个设备 ID 的引用。我习惯在每个页面的aboutToDisappear里打印当前页面栈深度智能家居 App 页面栈一深返回时数据不刷新、控制按钮状态错位这类问题靠日志很快就能定位。3. 配网、发现与控制智能家居 App 的两条主链路实现设备从买回家到能正常用要经过两条链路配网链路解决“设备怎么连上家里 Wi-Fi”的问题控制链路解决“App 怎么让设备干活”的问题。配网是首次使用必经之路控制是日常使用主链路两条链路的工程质量直接决定用户会不会退货。这一章先把时序讲清楚再给可落地的 TypeScript 源码。3.1 配网链路从连接设备热点到切回家庭网络的时序控制配网的本质是告诉设备你家 Wi-Fi 的账号密码。最常见的做法是 softAP设备开机后发出一个临时热点App 连上去向设备内置的配置接口提交家庭 Wi-Fi 的 SSID 和密码设备再重启连回家庭网络。整个过程里 App 的网络环境要切换两次任何一次切换失败都可能导致配网中断这也是配网成为智能家居 App 最大退货原因的本质。配网最容易翻车的点是没有状态机管理。开发者把流程写成一串顺序 await一旦某一步失败整个流程卡死用户只能杀掉 App 重来。我习惯把配网拆成五个阶段每个阶段独立计时、独立报错// services/PairingService.ts // 配网阶段用枚举定义方便打点和记录失败现场 export enum PairStage { Idle idle, ConnectingToDeviceAp connecting_to_device_ap, SendingCredential sending_credential, SwitchingBackToHomeWifi switching_back_to_home_wifi, DiscoveringDevice discovering_device, Bound bound } export class PairingService { stage: PairStage PairStage.Idle; private timerIds: number[] []; start(deviceApSsid: string, deviceApPassword: string): void { this.stage PairStage.ConnectingToDeviceAp; this.connectToDeviceAp(deviceApSsid, deviceApPassword); } private connectToDeviceAp(ssid: string, password: string): void { // 调用系统 Wi-Fi 能力连接设备热点密码为空则使用 open 网络 // 连接结果通过回调返回成功后进入下一阶段 } private sendCredential(homeSsid: string, homePassword: string): void { // 向设备配置接口发送 homeSsid / homePassword // 私有协议一般是 HTTP POST 或自定义 TCP 包 } private discoverAndBind(): void { // 切回家庭网络后扫描设备并执行绑定 } cancel(): void { this.timerIds.forEach((t) clearTimeout(t)); this.stage PairStage.Idle; } }代码逻辑说明每个阶段启动时都要注册一个超时定时器比如连接设备热点超过 15 秒就主动取消并提示用户“请靠近设备”发送凭据这步要重试三次间隔两秒因为设备写入 flash 需要时间。超时和失败都要记日志落盘否则售后阶段只能靠猜。参数说明deviceApSsid是设备热点的名称一般前缀固定方便识别deviceApPassword为空说明设备默认开放热点。homeSsid和homePassword才是用户家中 Wi-Fi 的真实信息发送前要做合法性校验避免把错误凭据写进设备导致设备反复重启。配网完后 App 要立即切回家庭 Wi-Fi切网成功后不要马上发发现请求先探测网关 IP 是否可达通常是对网关 IP 做一次 ping 或 TCP 连接等网络栈就绪再继续。这个“等就绪”的动作是配网成功率的分水岭我在第 6 章会给出完整排查套路。3.2 控制链路指令编码、超时重试与状态回读配网完成后日常控制就是 App 到网关的指令往返。控制链路我关注三件事指令编码、超时重试、状态回读。指令编码用强类型开关、亮度、色温这类操作直接用联合类型写出合法值参数用Recordstring, number约束// services/DeviceClient.ts import { http } from kit.NetworkKit; type DeviceAction on | off | brightness | color_temperature; export class DeviceClient { private client http.createHttp(); // 局域网控制网关网关再转发给子设备 async sendCommand(deviceId: string, action: DeviceAction, params?: Recordstring, number): Promiseboolean { const response await this.client.request( http://192.168.1.1/v1/device/command, { method: http.RequestMethod.POST, header: { Content-Type: application/json }, extraData: JSON.stringify({ deviceId, action, params: params ?? {} }), expectDataType: http.HttpDataType.STRING, timeout: 5000 } ); return response.responseCode 200 response.responseCode 300; } }逻辑说明网关地址通过配网后的设备发现拿到不要硬编码request的timeout设 5000是因为局域网网关响应通常在 200ms 内5 秒足够覆盖设备休眠唤醒的极端情况。expectDataType按网关实际返回设置返回 JSON 字符串就用STRING拿到后再JSON.parse。参数说明DeviceAction用联合类型而不是 string是 ArkTS 编译器给的强约束拼错britness在编译期直接报错。params里的数值上限比如亮度 0-100由页面控件约束网关也要兜底校验防止越界指令。超时重试要区分操作类型开关这种幂等操作可以放心重试但像“色温渐变”这种非幂等动作重复下发会导致状态错乱。我的做法是给每类 action 定义重试策略表on/off允许重试一次brightness这种值操作不重试但要返回设备最新值。状态回读是控制链路最容易被低估的环节。控制指令发出后不能假设设备一定执行成功要通过轮询或长连接回调把设备真实状态拿回来更新 UI。否则就会出现“点了开关灯没反应但界面显示已关”的差评来源。3.3 设备发现与绑定从广播扫到设备上线设备发现有两种场景配网刚完成后发现新设备以及 App 每次启动后恢复设备列表。前者的关键是等网络就绪后者的关键是缓存优先启动后直接用本地缓存渲染列表后台再异步探测在线状态避免每次冷启动都要等广播扫描完成。发现协议常见的做法是 UDP 广播或多播网关收到查询后返回设备列表。TypeScript 源码里我用接口抽象发现流程方便切换协议// services/DiscoveryService.ts // 设备信息以网关返回为准 export interface DeviceInfo { deviceId: string; // 网关内唯一 ID绑定关系以它为准 productName: string; // 产品名用于 UI 展示 ip: string; // 设备或网关当前 IP online: boolean; } export class DiscoveryService { // 广播发现的具体实现依赖网关协议这里只定义语义 async discover(): PromiseDeviceInfo[] { // 1. 向网段广播地址发送 Discover 报文 // 2. 收集 2 秒内的响应 // 3. 把响应解析成 DeviceInfo[] return []; } }逻辑说明discover返回的是原始设备列表加不加入 UI 还需要结合绑定关系判断。绑定动作一般在配网成功后的发现阶段自动执行App 发现新设备后直接调网关绑定接口把设备划到“客厅”之类的默认房间。参数说明online字段不是发现时的瞬时值网关返回的设备列表里通常自带在线状态App 拿到后先存到 storeUI 再决定是否展示可点击状态。如果发现响应超过 2 秒没回来不要扩大超时先检查 App 是否还连着家庭 Wi-Fi多半是切网没切成功。4. 状态管理与数据持久化让每个页面都拿到最新的设备状态智能家居 App 和普通 App 最大的区别是状态源在外部设备真实状态不在手机里。页面多、设备多、状态变化频繁界面需要一套一致性方案。我的选择是手写一个 Store 做内存状态管理再用偏好存储做本地持久化兜底两条路一起保证“重启 App 不丢设备设备状态变了界面必刷新”。4.1 手写轻量 Store不用重型框架也能做到单向数据流同一个设备的状态可能同时展示在列表页、详情页和场景编辑页。状态管理的核心目标是改一处、所有页面联动。外部状态管理框架在鸿蒙里引入成本高还可能和 ArkTS 编译器约束冲突。我一般直接用类加订阅回调手写一个 Store几十行就够// viewmodel/DeviceStore.ts // 轻量 Store页面订阅、数据变更、取消订阅 type Listener () void; export class DeviceStore { private deviceMap: Mapstring, DeviceInfo new Map(); private listeners: SetListener new Set(); subscribe(listener: Listener): () void { this.listeners.add(listener); // 返回退订函数页面在 aboutToDisappear 里调用 return () { this.listeners.delete(listener); }; } setDevices(devices: DeviceInfo[]): void { this.deviceMap.clear(); devices.forEach((d) this.deviceMap.set(d.deviceId, d)); this.emit(); } updateDevice(deviceId: string, patch: PartialDeviceInfo): void { const prev this.deviceMap.get(deviceId); if (!prev) return; this.deviceMap.set(deviceId, { ...prev, ...patch }); this.emit(); } getDevice(deviceId: string): DeviceInfo | undefined { return this.deviceMap.get(deviceId); } private emit(): void { this.listeners.forEach((fn) fn()); } }逻辑说明subscribe返回退订函数页面在aboutToAppear里调用subscribe并持有返回值在aboutToDisappear里执行退订防止页面销毁后还收到状态变更通知导致内存泄漏。deviceMap放在 store 内部页面只能通过setDevices和updateDevice修改数据保证所有变更路径都经过emit。参数说明updateDevice的patch用PartialDeviceInfo调用方只传要改的字段比如控制完灯之后只传{ online: false }不会误覆盖其他字段。setDevices用于全量刷新比如从缓存恢复或配网完成后重建列表。实际项目里我会让这个 Store 变成单例在 AppContext 里创建一次页面通过 context 获取。这样所有页面拿到的都是同一个实例天然具备跨页共享能力。比引入依赖注入框架简单得多也更好调试。4.2 用 Preferences 做本地缓存设备列表和用户配置落地Store 在内存里App 一被杀就没了。智能家居用户的设备列表和房间划分不能每次重来本地持久化必须有。鸿蒙提供的偏好存储是 key-value 形式适合存设备列表、用户配置这类可序列化的简单数据// common/LocalCache.ts import { preferences } from kit.ArkData; export class LocalCache { // 保存设备列表key 固定value 是 JSON 字符串 static async saveDeviceList(devices: DeviceInfo[]): Promisevoid { const store await preferences.getPreferences(getContext(this), smart_home_cache); await store.put(device_list, JSON.stringify(devices)); await store.flush(); // 不 flush数据可能只留在内存 } static async loadDeviceList(): PromiseDeviceInfo[] { const store await preferences.getPreferences(getContext(this), smart_home_cache); const raw await store.get(device_list, []); return JSON.parse(raw) as DeviceInfo[]; } }逻辑说明getPreferences的第一个参数需要 context在非 UI 场景里很容易拿不到我通常在最外层创建一次 context 缓存复用。put之后必须调flush否则数据只更新内存App 进程被系统回收时会丢。参数说明缓存文件名smart_home_cache里不要放版本号迁移时写个新 key 就行value 容量有限设备列表超过几百条就该考虑按房间分 key 存储。加载时JSON.parse可能抛异常要包一层 try/catch缓存损坏时回退到空数组去走发现流程。这套缓存策略有个绕不开的坑设备列表里如果存了 IP 和 online 状态下次启动时这些字段大概率已过期。我的原则是只把deviceId、productName、roomId这种稳定字段持久化online状态一律启动后实时探测不信任缓存里的在线标记。4.3 状态同步策略缓存快照、轮询刷新、长连接推送界面上看到的是数据快照真实状态在外面。如果 App 启动先等广播扫描再渲染体验会很差如果先渲染缓存再异步刷新又可能出现界面闪一下的抖动。我的做法是三分法冷启动读缓存秒开页面可见时轮询保活长连接推送即时修正。轮询间隔我一般取 10 秒页面在onPageShow时启动onPageHide时停掉避免不可见页面还在白白跑流量。长连接推送比如网关的 MQTT 或自定义 TCP 长连接收到设备状态变更后直接调 store 的updateDevice通知所有页面。两种数据流都汇入同一个 store就不会出现同一页面两个数据源的冲突。状态同步里最容易忽略的细节是“控制后的乐观更新”和“真实回读”要分开。用户点开关界面立刻置为 on这叫乐观更新体验好但如果回读发现设备实际是 off要能正确回滚并提示用户。代码上我会在发送指令时调updateDevice做一次乐观更新收到真实回读后再调一次updateDevice覆盖并且带一个同步时间戳只有新时间戳的数据才允许覆盖旧数据。这个时间戳机制帮我挡住过好几轮“假在线”的投诉。5. 鸿蒙智能家居开发最容易翻车的 5 个坑现象、原因、解决办法5.1 坑一配网切到设备热点之前的网络请求全部卡死现象配网流程走到“连接设备热点”后App 里所有请求都超时配网状态机卡住用户只能杀掉 App 重来。 原因系统切换 Wi-Fi 后网络栈完全重建之前建立的 socket 全部失效代码如果没感知这个变化还继续用旧连接就会一直等。 解决配网期间所有请求都用状态机管理切网动作完成后先等网络就绪回调再重新走发现流程。网络就绪的判定不要只看 Wi-Fi 连上要真的去探测网关 IP。这一步是配网成功率从 60% 提到 95% 的关键动作。5.2 坑二ArkTS 编译器不认 anyTS 源码迁移过来满屏报红现象从普通 TS 项目搬代码编译器报一堆 “Type ‘any’ is not allowed” 的错误同事以为是环境没配好折腾半天。 原因ArkTS 为了让编译器拿到完整类型信息禁用了any和unknown这是设计使然不是环境问题。 解决迁移前先开启 TypeScript 的严格模式把所有any改成具体类型或泛型。改动量集中在网络返回数据上把JSON.parse的返回值先断言成接口类型就好。遇到实在不好定类型的场景优先用联合类型收窄而不是退回any。5.3 坑三页面销毁了异步回调还在更新 UI直接抛异常现象从详情页返回列表页偶尔弹出一条 JS Error页面白一下又恢复。 原因HTTP 请求还没回来页面已经销毁回调里访问了页面状态或调用了界面更新接口框架发现页面不在栈里就抛错。 解决网络层统一走 store页面只订阅 store 变化不直接在回调里改页面状态。如果团队里还有旧代码直接回调操作 UI可以用一个isActive标志包一层页面销毁时置为 false回调里先判断再更新。5.4 坑四App 退到后台几分钟长连接断了回前台数据全旧现象用户切出去回个消息再回 App看到的还是几分钟前的设备状态点控制也没反应。 原因App 退后台后系统挂起长连接被网络栈回收定时器也不准了回前台时没有重新拉取。 解决不要寄希望于后台保活。在前台生命周期回调里强制重新走一次设备探测和状态拉取。长连接要自带断线重连和心跳回前台发现心跳超时就主动重建连接而不是等用户手动下拉刷新。5.5 坑五跨页面共享状态改不动原来各页面各存了一份副本现象在详情页把灯关了回到列表页列表上的灯还是绿色在线。 原因早期实现图省事每个页面在aboutToAppear时把设备快照复制到页面变量里之后就各改各的。 解决状态只允许存在唯一 store 里页面通过订阅拿最新值不要保存副本哪怕要缓存到本地也走统一接口。这条原则执行到位后我遇到的状态不同步类 bug 几乎绝迹。判断标准很简单代码里如果出现第二个Map或数组来存设备状态就该重构了。6. 进阶把配网成功率从 60% 提到 95% 的排查套路配网是智能家居 App 退货率的风向标。我见过不少项目功能都做完了唯独配网成功率长期在 60% 左右用户投诉集中在“App 连不上设备”“配好了又掉线”。排查这类问题与其去现场抓包不如把配网过程变成一条可回放的日志链路。我的做法是给配网的每个阶段打一个带时间戳和错误码的埋点统一写进本地文件并提供导出入口售后拿到日志后 5 分钟就能定位卡点。配网各阶段的时间区间如下阶段正常区间异常信号连接设备热点2~8 秒超过 15 秒说明热点信号弱或密码错误发送配网凭据1~3 秒重试三次仍失败多半是设备的配置接口响应异常切回家庭 Wi-Fi3~10 秒超过 15 秒说明家庭路由器识别设备慢发现并绑定2~5 秒超时通常是网络栈未就绪需探测网关后再重试日志落盘示例// common/PairingLogger.ts // 每个配网阶段写一条结构化日志便于售后定位 export function logPair(key: string, stage: string, ok: boolean, costMs: number): void { const line ${Date.now()}\t${key}\t${stage}\t${ok}\t${costMs}\n; // 写入本地日志文件按天滚动保留最近 7 天 }这个套路帮我解决过一个真实难题某次测试反馈配网成功率骤降拿到日志后发现“切回家庭 Wi-Fi”阶段耗时暴涨问题出在网关固件升级后路由器识别变慢而不是 App 逻辑。把等待时间从 5 秒放宽到 10 秒成功率立刻回到 95%。类似问题不沉淀成日志靠肉眼是永远抓不到的。如果你正在规划一个鸿蒙智能家居 App我最想提醒的就是先把配网日志链路做好再去做炫酷的界面日志这步偷了懒后面每一轮测试都会加倍还回来。希望帮到你。本文还有配套的精品资源点击获取