
这期想聊一个很实在的项目我用纯血鸿蒙搭了一个聊天Demo能跑在HarmonyOS手机上同时让PC浏览器、另一台手机浏览器一起进同一个聊天室互聊。整条链路跑通之后我才发现所谓“鸿蒙、PC、手机端互通”这件事真正卡住的不是App代码而是通信协议和联调环境。这篇文章完整记录了我的技术选型、服务端设计、ArkTS界面实现以及一路上填过的坑适合准备开始写鸿蒙应用、或者想快速验证跨端通信方案的开发者参考。这里说的“纯血鸿蒙”指的是HarmonyOS NEXT这一套也就是不兼容Android APK、用ArkTS ArkUI 系统API开发的全新生态。很多老项目迁过来会有一堆编译报错而我们从零写一个聊天Demo反而能最快摸清它的脾气。后面你也会看到ArkTS对动态类型限制得很死这在联调阶段特别容易把人搞崩溃。1. 这个Demo到底在做什么需求拆解与总体架构1.1 “纯血鸿蒙”意味着什么在我们动手前有必要先锚定一下“纯血”这两个字。HarmonyOS NEXT现在已经不挂载AOSP兼容层也就意味着以前那套“直接把APK装上去就能跑”的打法彻底失效了。你开发一个App必须使用鸿蒙自己的工程结构Stage模型、自己的UI框架ArkUI声明式、自己的语言ArkTS构建产物也只能是HAP。这个Demo就是在这样的前提下做的从建工程开始没有引用任何安卓时代的三方库纯纯用系统自带API写完一个聊天App。听起来很干净实际上你在写的过程中会发现ArkTS虽然长得像TypeScript但它为了编译期安全做了很多约束比如不允许任意类型、不允许any、JSON解析之后的类型转换也没那么顺畅。这些细节后面我会单独拉一个小节讲它们才是真正能帮你省时间的部分。1.2 三端互通的技术路线为什么选WebSocket要做跨端互通首先得选一个所有端都能参与的通信协议。我第一时间排除了HTTP轮询聊天消息延迟要求高轮询既浪费带宽又做不到消息到达的实时性。也考虑过MQTT和Socket.IO但前者需要一个额外的Broker后者本身还算是个半封装协议调试链路一旦出问题你就得猜是库的问题还是服务端问题。最后定的是WebSocket。理由很直接原生支持全双工客户端和服务端都能主动发消息天然适合聊天室。鸿蒙系统自带webSocket模块调用起来不需要额外配置。浏览器端直接用new WebSocket(url)就能连PC和手机浏览器零安装。服务端用Node.js几行代码就能搭一个广播型中转非常轻量。这套方案里所有客户端都连接到一个中心服务端服务端的工作只有一件事收到某个人发来的消息然后把它转发给当前在线的大厅里所有人。三个端之间并不直接建立连接而是通过服务端做中转架构上很好理解。实际的部署也如上面所说并不是说PC端必须跑一套鸿蒙的PC版、手机端必须跑鸿蒙App。让所有人打开浏览器访问同一个聊天页面再跟鸿蒙App连同一个WebSocket通道就能实现“看起来大家都在同一个聊天室”的效果。这其实就是标题里“互通”的本质。1.3 项目结构分工整个项目分为三个部分模块运行环境使用技术作用WebSocket中转服务Node.js本机或云服务器ws消息广播、在线连接管理鸿蒙聊天AppHarmonyOS手机/模拟器ArkTS ArkUI发送/接收聊天消息网页聊天端PC/手机浏览器HTML JavaScript以“游客身份”进入同一个聊天室顺带一提开发时完全可以把中转服务跑在本地电脑上鸿蒙手机或者模拟器通过局域网IP连上来即可。但如果你的设备不在同一个WiFi下那就得把它部署到公网服务器上这一步在后面的联调部分也会展开聊。2. 核心细节解析通信协议、状态管理与ArkTS的限制2.1 设计一个最简可扩展的消息协议很多新手写聊天Demo时容易忽略协议设计直接拿字符串就发。我当时特意定了一个JSON协议每条消息包含类型、发送者、消息内容和时间戳后续就算要加图片、加系统通知也只需要在type字段上扩展不需要动整个通信框架。{ type: chat, from: HarmonyOS-Phone, message: 你好PC端, time: 1737260000000 }我还会让服务端在客户端连接成功后主动回一条系统通知比如“欢迎加入聊天室”这样客户端连上之后能第一时间在界面上看到反馈而不是以为程序卡死了。协议虽然简单但它承担了几个关键职责区分不同的消息体、确定在哪里渲染系统提示、打印日志时能快速定位是哪一端发出来的消息。等你真正联调的时候就会知道字段越规范越省事。2.2 鸿蒙侧的状态管理怎么设计聊天界面本质是一个消息列表加一个输入框用ArkUI来做并不复杂。这里的核心难点在于WebSocket收到消息后如何把数据更新到UI列表上。我在页面里声明了一个State messages: MessageItem[]数组同时用一个自定义类来存放单条消息的外形字段。这个数组就是整个界面的“数据源”List组件通过ForEach来渲染它。收到新消息时我把解析出来的MessageItem依次追加进去再让列表滚动到底部。这里有一个鸿蒙开发非常关键的点在回调函数里拿到的数据要很快地“交还”给主线程上的状态数组来触发渲染。我在真实环境里测试过直接在WebSocket的message回调里调用this.messages.push(newMsg)多是除能触发刷新的但为了保险我有时也会用“创建新数组再整体赋值”的方式把不可变性做到位避免旧引用导致渲染不更新。2.3 ArkTS的几个硬性限制明白规则能少掉一半头发从我个人的体感来说纯血鸿蒙开发最难受的阶段就是刚写完TypeScript代码结果编译期被ArkTS各种拦截。这里整理几个我踩得最深的限制不支持any。你在TS里经常会写let data: any JSON.parse(text)在ArkTS里这样写直接被判死刑。我推荐把解析结果当作Recordstring, Object来读取字段再手动转成具体类型。JSON解析结果的类型转换很别扭。JSON.parse后直接as MessageItem这种写法在不同版本里会收到编译告警或错误。我更建议拆字段构造对象虽然代码多一些但类型安全且不会在发布时炸掉。ForEach必须给稳定的key。如果你直接用数组下标当key一旦消息变多或者发生删除操作UI容易渲染错乱。我给每条消息加了一个自增id用这个id作为唯一标识实测下来非常稳定。这些限制不是Bug而是ArkTS在编译期强制你做正确的事。一旦你适应了它的规则后面写复杂页面反而会降低运行时崩溃的概率。3. 实操过程从零到三端互聊的完整步骤3.1 准备开发环境并创建纯血鸿蒙工程我的开发环境是DevEco Studio 5.x自带ArkTS编译器和模拟器。鸿蒙SDK选择API 12或更高版本我用的API 12。Node.js 18用来跑中转服务。一台Windows电脑一台华为手机真机调试时用。安装好DevEco Studio后新建一个Empty Ability工程语言模板默认就是Stage模型加ArkTS。这里有个小建议工程名不要带中文否则一些路径相关的工具链可能出幺蛾子。然后运行起来看到Hello World页面就说明开发链路是通的。3.2 先搭一个Node.js聊天中转服务端服务端是整个Demo的心脏。我用ws包搭建这个库稳定、干净官方文档也非常短。先新建一个server.js内容大致如下const WebSocket require(ws); const wss new WebSocket.Server({ port: 18880 }); function broadcast(data) { wss.clients.forEach((client) { if (client.readyState WebSocket.OPEN) { client.send(data); } }); } wss.on(connection, (ws) { console.log(新客户端接入); ws.send(JSON.stringify({ type: system, from: 服务端, message: 欢迎加入聊天室, time: Date.now() })); ws.on(message, (message) { console.log(收到消息:, message.toString()); broadcast(message.toString()); }); ws.on(close, () { console.log(客户端断开); }); }); console.log(WebSocket服务已启动在端口 18880);然后执行npm init -y npm install ws node server.js注意我指定的监听端口是18880不填host时默认会监听在所有网卡上客户端用局域网IP也能连上。如果你发现外部设备连不上排查看是不是防火墙把端口拦了。3.3 鸿蒙App消息列表与WebSocket连接逻辑新建一个页面作为聊天室主界面页面结构包含三大部分顶部标题栏、中间消息列表、底部输入发送区。UI部分用ArkUI写Entry Component struct ChatPage { State messages: MessageItem[] [] State inputText: string private ws?: webSocket.WebSocket private listScroller: Scroller new Scroller() aboutToDisappear() { this.ws?.close() } build() { Column() { Text(纯血鸿蒙聊天室) .fontSize(18) .fontWeight(FontWeight.Bold) .height(56) List({ scroller: this.listScroller }) { ForEach(this.messages, (item: MessageItem) { ListItem() { MessageRow({ item: item }) } }, (item: MessageItem) item.id.toString()) } .layoutWeight(1) .width(100%) Row() { TextInput({ placeholder: 请输入消息, text: this.inputText }) .layoutWeight(1) .onChange((value: string) { this.inputText value }) Button(发送) .onClick(() { this.sendMessage() }) } .height(56) .padding(8) } .width(100%) .height(100%) } }连接的部分在aboutToAppear里创建WebSocketimport { webSocket } from kit.NetworkKit aboutToAppear() { this.ws webSocket.createWebSocket() this.ws.on(open, () { console.log(连接已打开) }) this.ws.on(message, (err: BusinessError, data: string | ArrayBuffer) { const text data as string const obj JSON.parse(text) as Recordstring, Object const newMsg: MessageItem { id: this.nextId, from: obj[from] as string, message: obj[message] as string, time: obj[time] as number, type: obj[type] as string } this.messages [...this.messages, newMsg] this.listScroller.scrollEdge(Edge.Bottom) }) this.ws.connect(ws://你的电脑局域网IP:18880) }发送消息时把输入框内容包装成JSONsendMessage() { if (!this.inputText.trim() || !this.ws) return const payload JSON.stringify({ type: chat, from: 鸿蒙手机, message: this.inputText, time: Date.now() }) this.ws.send(payload) this.inputText }这里有一个值得注意的小细节每次收到或发送消息后我都主动调用scrollEdge(Edge.Bottom)让列表滚到底部不然消息多了以后看不到最新内容。这个操作虽然简单但是在新手写演示Demo时非常容易漏掉。3.4 PC端和手机浏览器端的聊天页面为了让PC和另一台手机都能快速参与聊天我没写第二个原生客户端而是做了一个单文件HTML页面。这样PC上打开浏览器就能用手机端也只需要输入网址即可进入省去了各种安装和打包的麻烦。!DOCTYPE html html langzh-CN head meta charsetUTF-8 / title网页聊天端/title /head body div idlist/div input idmsg autocompleteoff / button onclicksend()发送/button script const ws new WebSocket(ws://你的电脑局域网IP:18880); ws.onmessage (event) { const data JSON.parse(event.data); const div document.createElement(div); div.textContent [${data.from}] ${data.message}; document.getElementById(list).appendChild(div); }; function send() { const input document.getElementById(msg); ws.send(JSON.stringify({ type: chat, from: PC浏览器, message: input.value, time: Date.now() })); input.value ; } /script /body /html页面虽然简陋但它承载了核心验证意义鸿蒙App发消息PC浏览器和手机浏览器都能在同一时间看到反过来也一样。如果你后续想做得更精致可以在这个页面上叠CSS美化或者加入昵称设置、在线人数显示等等。3.5 联调验证三个端点如何确认跑通联调这一步我专门提一下因为它隐藏着很多新手必踩的坑。我在电脑上启动了Node服务端之后先在浏览器里打开http://localhost:18880是不能用的——WebSocket不是HTTP请求直接访问端口会失败我直接用浏览器脚本连接正常情况下不会有日志显示。最简单的方式是看服务端的控制台输出只要鸿蒙App或浏览器页面连上来就会打印“新客户端接入”。实际演示时我会按下面这个顺序来做用手机浏览器打开HTML页面起名“手机浏览器A”。用电脑浏览器再打开一个页面起名“PC浏览器”。启动鸿蒙App连上服务端。在鸿蒙App里发一句“你好PC端”观察两个浏览器页面是否立刻收到。在PC浏览器里回一句“收到鸿蒙你好”观察鸿蒙App消息列表是否出现。如果全程都没有断连、没有延迟堆积那这个Demo就算真正跑通了。我第一次跑通这个链路的时候其实是有一种很微妙的满足感的因为你终于看到一个跨系统、跨设备的实时通信流程在自己手底下运转起来了。4. 我在过程中踩过的坑常见问题与排查技巧4.1 连接失败、秒断开多半是网络地址写错了这个是概率最高的坑鸿蒙App在模拟器里面模拟器里的localhost指向的是模拟器自身不是你电脑。如果你在App里写ws://localhost:18880大概率连接失败因为你压根没有在模拟器内部启动服务。正确的做法是用电脑的局域网IP比如ws://192.168.1.100:18880并且确保鸿蒙手机和电脑连的是同一个WiFiWebSocket服务也在监听所有网卡。如果真机连不上还需要检查Windows防火墙或路由器AP隔离。我遇到过一次手机能上网但连不上本地服务最后发现是防火墙把Node进程拦截了明确放行端口后立刻解决。4.2 消息发出去但界面一动不动先看数据有没有进数组这个现象很迷惑服务端打印日志能看到消息过来浏览器也能收到但鸿蒙UI死活不刷新。最常见的原因是this.messages.push()没有触发ArkUI更新。我在实际调试中把写法改成了this.messages [...this.messages, newMsg]用展开运算符生成一个新数组赋值这样状态管理一定能感知到变化。另外也再检查一下ForEach的key生成逻辑如果key重复UI的复用机制会认为没有新元素也不会渲染。4.3 键盘弹起遮挡输入框以及切后台掉线键盘遮挡输入框在手机上很常见。鸿蒙开发里可以在EntryAbility的windowStage.loadContent中配置键盘避让或者直接监听键盘高度做布局适配。做Demo时最简单的做法是把输入框放在底部并设置布局的expandSafeArea让键盘弹起时页面能自动上推。切后台掉线的问题则直接和WebSocket生命周期有关。我在aboutToDisappear里主动close连接防止页面销毁后连接泄漏。但这里有个更细的坑如果只是App退到后台页面并没有销毁连接也可能被系统挂起。要做到稳健的恢复机制可以在onPageShow时检查连接状态如果发现断开就重新连接。4.4 常见问题速查表现象可能原因解决方法鸿蒙App连不上服务端地址写成了localhost改成电脑局域网IP浏览器能连鸿蒙不能模拟器网络隔离/防火墙使用真机、放行端口消息发出但界面不刷新push未触发UI更新用展开运算符创建新数组列表渲染错乱ForEach的key重复使用唯一id闪烁或白屏后恢复页面生命周期里连续建连在aboutToDisappear里关闭连接JSON.parse报错ArkTS类型断言过严用Recordstring, Object读字段这张表基本上就是我整个联调过程的精华浓缩遇到任何一个问题先按表格里的顺序排查远比从零看日志高效。5. 一批人用起来之后后续可以怎样扩展这个Demo最让我觉得有价值的地方不只是三端互通本身而是整个消息链路的扩展性。你只要把消息协议里多加一个type就能扩展出在线列表、私聊、图片消息、历史消息存储等等玩法。服务端那边也只需增加对应的消息处理分支。如果想更贴近真实项目可以把Node服务端换成带数据库的版本数据落库后新加入的客户端就可以请求历史消息而不只是看“上线之后”的内容。又或者把中转服务部署到一台云服务器上那么鸿蒙App、PC浏览器、手机浏览器就不再受局域网限制随时都能互聊。我自己后续的做法是给网页端加了一个简单的昵称输入再把鸿蒙App的UI换成了更适合聊天室的样式比如气泡消息、头像占位图、时间戳格式化。这些都是建立在同一个WebSocket通道之上改动起来非常顺手。如果你正好也在折腾纯血鸿蒙的通信功能建议你就拿聊天Demo当练手项目先把这一套跑通后面再往自己真实业务里迁移时你就知道哪些地方能省时间哪些地方必须提前设计好了。