文章目录概要整体架构流程技术名词解释技术细节小结概要技术:ArkTS ArkUIMVVM架构| WebSocketJSON消息体|Node.js ws服务端广播| 华为AccountKit认证|创建元服务页码:Index— 首页昵称/IP输入华为账号登录路由跳转ChatView— 聊天界面消息左右布局输入发送自动滚底ChatViewModel— 连接管理消息收发与senderID去重UI回调ChatModel— 消息模型头像、昵称、内容、是否自己WsChatClient— 封装鸿蒙原生WebSocket APIserver.js— Node.js服务端连接池管理广播转发EntryFormAbility— 元服务入口承载页面生命周期整体架构流程用户启动元服务↓EntryFormAbility入口承载↓Index 首页├── 华为账号登录 → 获取用户标识├── 输入昵称、服务器IP└── 点击加入 → router.pushUrl 传参跳转↓ChatView 聊天界面├── 接收路由参数userName, userID, serverIP├── 初始化 ChatViewModel├── connectToServer(ip, port) → 创建 WsChatClient└── 设置回调收到消息 → 刷新列表 → 自动滚底↓WsChatClientWebSocket连接└── ws://IP:3000 → 连接 Node.js 服务端↓server.js服务端├── 新连接加入连接池├── 收到消息 → 广播给所有客户端└── 断线 → 清理连接池↓消息收发流程发送输入框 → sendMessage() → 本地list.push ws.send(JSON)接收ws.onmessage → JSON解析 → senderID判断是否自己→ 不是自己则list.push → 回调通知ChatView刷新技术名词解释ArkTS鸿蒙原生开发语言TypeScript的超集用于编写元服务ArkUI鸿蒙声明式UI框架通过组件化方式构建界面MVVMModel-View-ViewModel架构数据驱动视图解耦UI与逻辑WebSocket全双工通信协议客户端与服务端建立长连接实时收发消息JSON轻量级数据交换格式消息体序列化/反序列化AccountKit华为账号服务提供登录授权获取用户标识EntryFormAbility元服务入口Ability承载页面生命周期和路由router.pushUrl鸿蒙路由API页面间跳转并传递参数Entry装饰器标记页面入口组件ComponentV2装饰器声明自定义组件V2版本Local装饰器组件级响应式状态变量senderID消息发送者唯一标识用于判断消息是否为自己所发wsNode.js的WebSocket库用于搭建服务端Scroller鸿蒙滚动控制器控制列表滚动到底部技术细节ChatModelexport class ChatModel { img: Resource; name: string; text: string; isMe: boolean; constructor(img: Resource, name: string, text: string, isMe: boolean false) { this.img img; this.name name; this.text text; this.isMe isMe; } }WsChatClientimport { webSocket } from kit.NetworkKit; import { BusinessError } from kit.BasicServicesKit; import { util } from kit.ArkTS; export class WsChatClient { private ws: webSocket.WebSocket | null null; private url: string; private onMessage: ((msg: string) void) | null null; private onStatus: ((status: string) void) | null null; private isConnected: boolean false; constructor(url: string) { this.url url; } connect(onMessage: (msg: string) void, onStatus?: (status: string) void): void { this.onMessage onMessage; this.onStatus onStatus || null; this.ws webSocket.createWebSocket(); this.ws.on(open, (err: BusinessError, value: Object) { if (!err) { this.isConnected true; this.onStatus?.(已连接); } else { this.onStatus?.(连接失败 JSON.stringify(err)); } }); this.ws.on(message, (err: BusinessError, value: string | ArrayBuffer) { if (!err this.onMessage) { let msg: string; if (typeof value string) { msg value; } else { let decoder new util.TextDecoder(); msg decoder.decodeWithStream(new Uint8Array(value)); } this.onMessage(msg); } }); this.ws.on(close, (err: BusinessError, value: webSocket.CloseResult) { this.isConnected false; this.onStatus?.(已断开); }); this.ws.on(error, (err: BusinessError) { this.onStatus?.(错误 JSON.stringify(err)); }); this.ws.connect(this.url, (err: BusinessError, value: boolean) { if (err) { this.onStatus?.(连接失败 JSON.stringify(err)); } }); } send(msg: string): void { if (this.ws this.isConnected) { this.ws.send(msg, (err: BusinessError, value: boolean) { if (err) { console.error(发送失败 JSON.stringify(err)); } }); } } close(): void { if (this.ws) { this.ws.close(); this.ws null; this.isConnected false; } } }ChatViewModelimport { ChatModel } from ../Model/ChatModel; import { WsChatClient } from ../Utils/WsChatClient; interface MessageData { sender: string; senderID: string; text: string; time: number; } export class ChatViewModel { list: ArrayChatModel []; private wsClient: WsChatClient | null null; private myName: string 我; private myID: string ; private onListChanged: (() void) | null null; constructor() { this.list []; } // 设置列表更新回调 setOnListChanged(callback: () void): void { this.onListChanged callback; } // 设置用户信息 setMyName(name: string): void { this.myName name; } setMyID(id: string): void { this.myID id; } // 启动服务器创建房间 startServer(): void { console.info(启动聊天服务器); } // 连接WebSocket服务器 connectToServer(ip: string, port: number 3000): void { let url: string ws://${ip}:${port}; this.wsClient new WsChatClient(url); this.wsClient.connect( (msg: string): void { this.handleReceivedMessage(msg); }, (status: string): void { console.info(连接状态 status); } ); } // 处理接收到的消息 private handleReceivedMessage(msg: string): void { try { let messageData: MessageData JSON.parse(msg) as MessageData; // 通过senderID判断是否是自己发的消息 let isMe: boolean messageData.senderID this.myID; // 如果是自己发的消息就不重复添加因为在sendMessage中已经添加了 if (isMe) { return; } let chatMsg: ChatModel new ChatModel( $r(app.media.A), messageData.sender, messageData.text, false // 别人发的 ); this.list.push(chatMsg); // 通知视图更新 this.onListChanged?.(); } catch (e) { let chatMsg: ChatModel new ChatModel( $r(app.media.A), 对方, msg, false ); this.list.push(chatMsg); // 通知视图更新 this.onListChanged?.(); } } // 发送消息 sendMessage(text: string): void { this.list.push(new ChatModel($r(app.media.B), this.myName, text, true)); if (this.wsClient) { let messageData: MessageData { sender: this.myName, senderID: this.myID, text: text, time: new Date().getTime() }; this.wsClient.send(JSON.stringify(messageData)); } } // 断开连接 disconnect(): void { if (this.wsClient) { this.wsClient.close(); this.wsClient null; } } }ChatView// view/chatView.ets import { ChatModel } from ../Model/ChatModel; import { ChatViewModel } from ../ViewModel/ChatViewModel; import { router } from kit.ArkUI; interface RouterParams { userName: string; userID: string; isHost: boolean; serverIP?: string; } Entry ComponentV2 export struct ChatView { Local chatList: ChatModel[] []; Local inputText: string ; private viewModel: ChatViewModel new ChatViewModel(); private scroller: Scroller new Scroller(); private userName: string ; private userID: string ; private isHost: boolean false; aboutToAppear(): void { // 获取路由参数 const params: RouterParams router.getParams() as RouterParams; this.userName params.userName; this.userID params.userID; this.isHost params.isHost; // 设置用户信息 this.viewModel.setMyName(this.userName); this.viewModel.setMyID(this.userID); // 设置列表更新回调当收到新消息时自动刷新UI this.viewModel.setOnListChanged(() { this.chatList this.viewModel.list; // 滚到底部 this.scroller.scrollEdge(Edge.Bottom); }); // 连接服务器 if (this.isHost) { this.viewModel.startServer(); } else { const ip: string params.serverIP || 192.168.1.100; this.viewModel.connectToServer(ip); } // 初始化数据 this.chatList this.viewModel.list; } build() { Column() { // 顶部标题栏 Row() { Button(← 返回) .fontSize(14) .fontColor(#007AFF) .backgroundColor(Color.Transparent) .onClick(() { this.viewModel.disconnect(); router.back(); }) Text(聊天室) .fontSize(18) .fontWeight(FontWeight.Bold) .layoutWeight(1) .textAlign(TextAlign.Center) Text(this.userName) .fontSize(12) .fontColor(#888888) .padding({ right: 12 }) } .width(100%) .height(48) .backgroundColor(#FFFFFF) .padding({ left: 8 }) // 消息列表区域 List({ scroller: this.scroller }) { ForEach(this.chatList, (item: ChatModel, index: number) { ListItem() { Row() { if (item.isMe) { // ----- 自己发的消息 ----- Blank() Column({ space: 4 }) { Text(item.text) .fontSize(16) .padding(12) .borderRadius(12) .backgroundColor(#007AFF) .fontColor(Color.White) .constraintSize({ maxWidth: 70% }) } Image(item.img) .width(40) .height(40) .borderRadius(20) .objectFit(ImageFit.Cover) .margin({ left: 8 }) } else { // ----- 别人发的消息 ----- Image(item.img) .width(40) .height(40) .borderRadius(20) .objectFit(ImageFit.Cover) .margin({ right: 8 }) Column({ space: 4 }) { Text(item.name) .fontSize(12) .fontColor(#888888) .padding({ left: 4 }) Text(item.text) .fontSize(16) .padding(12) .borderRadius(12) .backgroundColor(#E5E5EA) .fontColor(Color.Black) .constraintSize({ maxWidth: 70% }) } } } .width(100%) .padding({ left: 12, right: 12, top: 6, bottom: 6 }) .alignItems(VerticalAlign.Top) } }, (item: ChatModel, index: number) index.toString()) } .layoutWeight(1) .backgroundColor(#FAFAFA) // 底部输入区域 Row({ space: 10 }) { TextInput({ placeholder: 输入消息..., text: this.inputText }) .layoutWeight(1) .height(44) .padding({ left: 12, right: 12 }) .backgroundColor(#FFFFFF) .borderRadius(22) .border({ width: 1, color: #E0E0E0 }) .onChange((value: string) { this.inputText value; }) Button(发送) .height(44) .padding({ left: 16, right: 16 }) .borderRadius(22) .backgroundColor(#007AFF) .fontColor(Color.White) .fontSize(14) .onClick(() { if (this.inputText.trim() ! ) { this.viewModel.sendMessage(this.inputText.trim()); // 更新列表 this.chatList this.viewModel.list; this.inputText ; this.scroller.scrollEdge(Edge.Bottom); } }) } .width(100%) .padding({ left: 12, right: 12, top: 8, bottom: 8 }) .backgroundColor(#F5F5F5) } .width(100%) .height(100%) } }ChatWidgetCard:Entry Component struct ChatWidgetCard { /* * The title. */ readonly title: string 局域网多人聊天; /* * The action type. */ readonly actionType: string router; /* * The ability name. */ readonly abilityName: string EntryAbility; /* * The message. */ readonly message: string add detail; /* * The width percentage setting. */ readonly fullWidthPercent: string 100%; /* * The height percentage setting. */ readonly fullHeightPercent: string 100%; build() { Row() { Column() { Text(this.title) .fontSize($r(app.float.font_size)) .fontWeight(FontWeight.Medium) .fontColor($r(sys.color.font)) } .width(this.fullWidthPercent) } .height(this.fullHeightPercent) .backgroundColor($r(sys.color.comp_background_primary)) .onClick(() { postCardAction(this, { action: this.actionType, abilityName: this.abilityName, params: { targetPage: pages/Index } }); }) } }Index:import { authentication } from kit.AccountKit; import { BusinessError } from kit.BasicServicesKit; import { hilog } from kit.PerformanceAnalysisKit; import router from ohos.router; const DOMAIN 0x0000; Entry ComponentV2 struct Index { Local inputIP: string 192.168.3.237; Local userName: string ; build() { Column({ space: 30 }) { Text(局域网聊天室) .fontSize(32) .fontWeight(FontWeight.Bold) .fontColor(#333333) .margin({ top: 80, bottom: 20 }) // 用户昵称 Column({ space: 8 }) { Text(你的昵称) .fontSize(14) .fontColor(#888888) .alignSelf(ItemAlign.Start) .padding({ left: 5 }) TextInput({ placeholder: 请输入昵称, text: this.userName }) .width(75%) .height(48) .fontSize(16) .borderRadius(12) .backgroundColor(#F5F5F5) .padding({ left: 16 }) .onChange((value: string) { this.userName value; }) } // 服务器IP Column({ space: 8 }) { Text(服务器IP地址) .fontSize(14) .fontColor(#888888) .alignSelf(ItemAlign.Start) .padding({ left: 5 }) TextInput({ placeholder: 请输入服务器IP, text: this.inputIP }) .width(75%) .height(48) .fontSize(16) .borderRadius(12) .backgroundColor(#F5F5F5) .padding({ left: 16 }) .onChange((value: string) { this.inputIP value; }) } // 加入按钮 Button(加入聊天室) .width(60%) .height(50) .fontSize(18) .fontWeight(FontWeight.Medium) .backgroundColor(#007AFF) .fontColor(Color.White) .borderRadius(25) .margin({ top: 20 }) .onClick(() { const name: string this.userName.trim() || 用户 Math.floor(Math.random() * 1000); const ip: string this.inputIP.trim() || 192.168.3.237; router.pushUrl({ url: View/ChatView, params: { userName: name, userID: user_ Date.now(), serverIP: ip } }).catch((err: BusinessError) { hilog.error(DOMAIN, testTag, 跳转失败: %{public}s, JSON.stringify(err)); }); }) Text(提示手机和电脑需连接同一WiFi。) .fontSize(12) .fontColor(#BBBBBB) .margin({ top: 20 }) Text(需在终端安装依赖 npm install ws) .fontSize(12) .fontColor(#BBBBBB) .margin({ top: 5 }) Text(需在终端开启服务端 node server.js。) .fontSize(12) .fontColor(#BBBBBB) .margin({ top: 5 }) Text(房主可在终端输入 ipconfig 查看IPv4地址) .fontSize(12) .fontColor(#BBBBBB) .margin({ top: 5 }) } .width(100%) .height(100%) .alignItems(HorizontalAlign.Center) } aboutToAppear(): void { hilog.info(DOMAIN, testTag, %{public}s, Ability onCreate); this.loginWithHuaweiID(); } private loginWithHuaweiID(): void { const loginRequest: authentication.HuaweiIDProvider new authentication.HuaweiIDProvider(); const request: authentication.LoginWithHuaweiIDRequest loginRequest.createLoginWithHuaweiIDRequest(); request.forceLogin false; const controller: authentication.AuthenticationController new authentication.AuthenticationController(); controller.executeRequest(request).then((data: authentication.LoginWithHuaweiIDResponse) { const authCode: string data.data?.authorizationCode || ; // 用华为账号信息作为用户名 if (authCode) { this.userName HW_ authCode.substring(0, 6); } hilog.info(DOMAIN, testTag, 登录成功); }).catch((error: BusinessError) { hilog.error(DOMAIN, testTag, error: %{public}s, JSON.stringify(error)); // 登录失败用默认昵称 this.userName 用户 Math.floor(Math.random() * 1000); }); } }EntryFormAbility:import { formBindingData, FormExtensionAbility, formInfo } from kit.FormKit; import { Want } from kit.AbilityKit; export default class EntryFormAbility extends FormExtensionAbility { onAddForm(want: Want) { // Called to return a FormBindingData object. const formData ; return formBindingData.createFormBindingData(formData); } onCastToNormalForm(formId: string) { // Called when the form provider is notified that a temporary form is successfully // converted to a normal form. } onUpdateForm(formId: string) { // Called to notify the form provider to update a specified form. } onFormEvent(formId: string, message: string) { // Called when a specified message event defined by the form provider is triggered. } onRemoveForm(formId: string) { // Called to notify the form provider that a specified form has been destroyed. } onAcquireFormState(want: Want) { // Called to return a {link FormState} object. return formInfo.FormState.READY; } }小结本项目实现了一个基于鸿蒙元服务的局域网多人聊天室采用ArkTS ArkUI构建客户端通过WebSocket与Node.js服务端实时通信。核心成果实现了用户登录、房间加入、消息收发、界面刷新等完整聊天流程采用 MVVM 架构分离视图与逻辑代码结构清晰通过 senderID 去重机制避免消息重复显示支持华为账号认证获取用户身份存在问题端口配置不统一服务端3000 / 客户端8080缺少连接状态提示、自动重连、心跳保活服务端广播包含发送者自身浪费带宽我没有其它手机验证能不能连成功改进方向统一端口配置并增加连接状态UI添加断线重连与心跳机制优化服务端广播逻辑排除发送者
简易版鸿蒙元服务局域网多人聊天室
文章目录概要整体架构流程技术名词解释技术细节小结概要技术:ArkTS ArkUIMVVM架构| WebSocketJSON消息体|Node.js ws服务端广播| 华为AccountKit认证|创建元服务页码:Index— 首页昵称/IP输入华为账号登录路由跳转ChatView— 聊天界面消息左右布局输入发送自动滚底ChatViewModel— 连接管理消息收发与senderID去重UI回调ChatModel— 消息模型头像、昵称、内容、是否自己WsChatClient— 封装鸿蒙原生WebSocket APIserver.js— Node.js服务端连接池管理广播转发EntryFormAbility— 元服务入口承载页面生命周期整体架构流程用户启动元服务↓EntryFormAbility入口承载↓Index 首页├── 华为账号登录 → 获取用户标识├── 输入昵称、服务器IP└── 点击加入 → router.pushUrl 传参跳转↓ChatView 聊天界面├── 接收路由参数userName, userID, serverIP├── 初始化 ChatViewModel├── connectToServer(ip, port) → 创建 WsChatClient└── 设置回调收到消息 → 刷新列表 → 自动滚底↓WsChatClientWebSocket连接└── ws://IP:3000 → 连接 Node.js 服务端↓server.js服务端├── 新连接加入连接池├── 收到消息 → 广播给所有客户端└── 断线 → 清理连接池↓消息收发流程发送输入框 → sendMessage() → 本地list.push ws.send(JSON)接收ws.onmessage → JSON解析 → senderID判断是否自己→ 不是自己则list.push → 回调通知ChatView刷新技术名词解释ArkTS鸿蒙原生开发语言TypeScript的超集用于编写元服务ArkUI鸿蒙声明式UI框架通过组件化方式构建界面MVVMModel-View-ViewModel架构数据驱动视图解耦UI与逻辑WebSocket全双工通信协议客户端与服务端建立长连接实时收发消息JSON轻量级数据交换格式消息体序列化/反序列化AccountKit华为账号服务提供登录授权获取用户标识EntryFormAbility元服务入口Ability承载页面生命周期和路由router.pushUrl鸿蒙路由API页面间跳转并传递参数Entry装饰器标记页面入口组件ComponentV2装饰器声明自定义组件V2版本Local装饰器组件级响应式状态变量senderID消息发送者唯一标识用于判断消息是否为自己所发wsNode.js的WebSocket库用于搭建服务端Scroller鸿蒙滚动控制器控制列表滚动到底部技术细节ChatModelexport class ChatModel { img: Resource; name: string; text: string; isMe: boolean; constructor(img: Resource, name: string, text: string, isMe: boolean false) { this.img img; this.name name; this.text text; this.isMe isMe; } }WsChatClientimport { webSocket } from kit.NetworkKit; import { BusinessError } from kit.BasicServicesKit; import { util } from kit.ArkTS; export class WsChatClient { private ws: webSocket.WebSocket | null null; private url: string; private onMessage: ((msg: string) void) | null null; private onStatus: ((status: string) void) | null null; private isConnected: boolean false; constructor(url: string) { this.url url; } connect(onMessage: (msg: string) void, onStatus?: (status: string) void): void { this.onMessage onMessage; this.onStatus onStatus || null; this.ws webSocket.createWebSocket(); this.ws.on(open, (err: BusinessError, value: Object) { if (!err) { this.isConnected true; this.onStatus?.(已连接); } else { this.onStatus?.(连接失败 JSON.stringify(err)); } }); this.ws.on(message, (err: BusinessError, value: string | ArrayBuffer) { if (!err this.onMessage) { let msg: string; if (typeof value string) { msg value; } else { let decoder new util.TextDecoder(); msg decoder.decodeWithStream(new Uint8Array(value)); } this.onMessage(msg); } }); this.ws.on(close, (err: BusinessError, value: webSocket.CloseResult) { this.isConnected false; this.onStatus?.(已断开); }); this.ws.on(error, (err: BusinessError) { this.onStatus?.(错误 JSON.stringify(err)); }); this.ws.connect(this.url, (err: BusinessError, value: boolean) { if (err) { this.onStatus?.(连接失败 JSON.stringify(err)); } }); } send(msg: string): void { if (this.ws this.isConnected) { this.ws.send(msg, (err: BusinessError, value: boolean) { if (err) { console.error(发送失败 JSON.stringify(err)); } }); } } close(): void { if (this.ws) { this.ws.close(); this.ws null; this.isConnected false; } } }ChatViewModelimport { ChatModel } from ../Model/ChatModel; import { WsChatClient } from ../Utils/WsChatClient; interface MessageData { sender: string; senderID: string; text: string; time: number; } export class ChatViewModel { list: ArrayChatModel []; private wsClient: WsChatClient | null null; private myName: string 我; private myID: string ; private onListChanged: (() void) | null null; constructor() { this.list []; } // 设置列表更新回调 setOnListChanged(callback: () void): void { this.onListChanged callback; } // 设置用户信息 setMyName(name: string): void { this.myName name; } setMyID(id: string): void { this.myID id; } // 启动服务器创建房间 startServer(): void { console.info(启动聊天服务器); } // 连接WebSocket服务器 connectToServer(ip: string, port: number 3000): void { let url: string ws://${ip}:${port}; this.wsClient new WsChatClient(url); this.wsClient.connect( (msg: string): void { this.handleReceivedMessage(msg); }, (status: string): void { console.info(连接状态 status); } ); } // 处理接收到的消息 private handleReceivedMessage(msg: string): void { try { let messageData: MessageData JSON.parse(msg) as MessageData; // 通过senderID判断是否是自己发的消息 let isMe: boolean messageData.senderID this.myID; // 如果是自己发的消息就不重复添加因为在sendMessage中已经添加了 if (isMe) { return; } let chatMsg: ChatModel new ChatModel( $r(app.media.A), messageData.sender, messageData.text, false // 别人发的 ); this.list.push(chatMsg); // 通知视图更新 this.onListChanged?.(); } catch (e) { let chatMsg: ChatModel new ChatModel( $r(app.media.A), 对方, msg, false ); this.list.push(chatMsg); // 通知视图更新 this.onListChanged?.(); } } // 发送消息 sendMessage(text: string): void { this.list.push(new ChatModel($r(app.media.B), this.myName, text, true)); if (this.wsClient) { let messageData: MessageData { sender: this.myName, senderID: this.myID, text: text, time: new Date().getTime() }; this.wsClient.send(JSON.stringify(messageData)); } } // 断开连接 disconnect(): void { if (this.wsClient) { this.wsClient.close(); this.wsClient null; } } }ChatView// view/chatView.ets import { ChatModel } from ../Model/ChatModel; import { ChatViewModel } from ../ViewModel/ChatViewModel; import { router } from kit.ArkUI; interface RouterParams { userName: string; userID: string; isHost: boolean; serverIP?: string; } Entry ComponentV2 export struct ChatView { Local chatList: ChatModel[] []; Local inputText: string ; private viewModel: ChatViewModel new ChatViewModel(); private scroller: Scroller new Scroller(); private userName: string ; private userID: string ; private isHost: boolean false; aboutToAppear(): void { // 获取路由参数 const params: RouterParams router.getParams() as RouterParams; this.userName params.userName; this.userID params.userID; this.isHost params.isHost; // 设置用户信息 this.viewModel.setMyName(this.userName); this.viewModel.setMyID(this.userID); // 设置列表更新回调当收到新消息时自动刷新UI this.viewModel.setOnListChanged(() { this.chatList this.viewModel.list; // 滚到底部 this.scroller.scrollEdge(Edge.Bottom); }); // 连接服务器 if (this.isHost) { this.viewModel.startServer(); } else { const ip: string params.serverIP || 192.168.1.100; this.viewModel.connectToServer(ip); } // 初始化数据 this.chatList this.viewModel.list; } build() { Column() { // 顶部标题栏 Row() { Button(← 返回) .fontSize(14) .fontColor(#007AFF) .backgroundColor(Color.Transparent) .onClick(() { this.viewModel.disconnect(); router.back(); }) Text(聊天室) .fontSize(18) .fontWeight(FontWeight.Bold) .layoutWeight(1) .textAlign(TextAlign.Center) Text(this.userName) .fontSize(12) .fontColor(#888888) .padding({ right: 12 }) } .width(100%) .height(48) .backgroundColor(#FFFFFF) .padding({ left: 8 }) // 消息列表区域 List({ scroller: this.scroller }) { ForEach(this.chatList, (item: ChatModel, index: number) { ListItem() { Row() { if (item.isMe) { // ----- 自己发的消息 ----- Blank() Column({ space: 4 }) { Text(item.text) .fontSize(16) .padding(12) .borderRadius(12) .backgroundColor(#007AFF) .fontColor(Color.White) .constraintSize({ maxWidth: 70% }) } Image(item.img) .width(40) .height(40) .borderRadius(20) .objectFit(ImageFit.Cover) .margin({ left: 8 }) } else { // ----- 别人发的消息 ----- Image(item.img) .width(40) .height(40) .borderRadius(20) .objectFit(ImageFit.Cover) .margin({ right: 8 }) Column({ space: 4 }) { Text(item.name) .fontSize(12) .fontColor(#888888) .padding({ left: 4 }) Text(item.text) .fontSize(16) .padding(12) .borderRadius(12) .backgroundColor(#E5E5EA) .fontColor(Color.Black) .constraintSize({ maxWidth: 70% }) } } } .width(100%) .padding({ left: 12, right: 12, top: 6, bottom: 6 }) .alignItems(VerticalAlign.Top) } }, (item: ChatModel, index: number) index.toString()) } .layoutWeight(1) .backgroundColor(#FAFAFA) // 底部输入区域 Row({ space: 10 }) { TextInput({ placeholder: 输入消息..., text: this.inputText }) .layoutWeight(1) .height(44) .padding({ left: 12, right: 12 }) .backgroundColor(#FFFFFF) .borderRadius(22) .border({ width: 1, color: #E0E0E0 }) .onChange((value: string) { this.inputText value; }) Button(发送) .height(44) .padding({ left: 16, right: 16 }) .borderRadius(22) .backgroundColor(#007AFF) .fontColor(Color.White) .fontSize(14) .onClick(() { if (this.inputText.trim() ! ) { this.viewModel.sendMessage(this.inputText.trim()); // 更新列表 this.chatList this.viewModel.list; this.inputText ; this.scroller.scrollEdge(Edge.Bottom); } }) } .width(100%) .padding({ left: 12, right: 12, top: 8, bottom: 8 }) .backgroundColor(#F5F5F5) } .width(100%) .height(100%) } }ChatWidgetCard:Entry Component struct ChatWidgetCard { /* * The title. */ readonly title: string 局域网多人聊天; /* * The action type. */ readonly actionType: string router; /* * The ability name. */ readonly abilityName: string EntryAbility; /* * The message. */ readonly message: string add detail; /* * The width percentage setting. */ readonly fullWidthPercent: string 100%; /* * The height percentage setting. */ readonly fullHeightPercent: string 100%; build() { Row() { Column() { Text(this.title) .fontSize($r(app.float.font_size)) .fontWeight(FontWeight.Medium) .fontColor($r(sys.color.font)) } .width(this.fullWidthPercent) } .height(this.fullHeightPercent) .backgroundColor($r(sys.color.comp_background_primary)) .onClick(() { postCardAction(this, { action: this.actionType, abilityName: this.abilityName, params: { targetPage: pages/Index } }); }) } }Index:import { authentication } from kit.AccountKit; import { BusinessError } from kit.BasicServicesKit; import { hilog } from kit.PerformanceAnalysisKit; import router from ohos.router; const DOMAIN 0x0000; Entry ComponentV2 struct Index { Local inputIP: string 192.168.3.237; Local userName: string ; build() { Column({ space: 30 }) { Text(局域网聊天室) .fontSize(32) .fontWeight(FontWeight.Bold) .fontColor(#333333) .margin({ top: 80, bottom: 20 }) // 用户昵称 Column({ space: 8 }) { Text(你的昵称) .fontSize(14) .fontColor(#888888) .alignSelf(ItemAlign.Start) .padding({ left: 5 }) TextInput({ placeholder: 请输入昵称, text: this.userName }) .width(75%) .height(48) .fontSize(16) .borderRadius(12) .backgroundColor(#F5F5F5) .padding({ left: 16 }) .onChange((value: string) { this.userName value; }) } // 服务器IP Column({ space: 8 }) { Text(服务器IP地址) .fontSize(14) .fontColor(#888888) .alignSelf(ItemAlign.Start) .padding({ left: 5 }) TextInput({ placeholder: 请输入服务器IP, text: this.inputIP }) .width(75%) .height(48) .fontSize(16) .borderRadius(12) .backgroundColor(#F5F5F5) .padding({ left: 16 }) .onChange((value: string) { this.inputIP value; }) } // 加入按钮 Button(加入聊天室) .width(60%) .height(50) .fontSize(18) .fontWeight(FontWeight.Medium) .backgroundColor(#007AFF) .fontColor(Color.White) .borderRadius(25) .margin({ top: 20 }) .onClick(() { const name: string this.userName.trim() || 用户 Math.floor(Math.random() * 1000); const ip: string this.inputIP.trim() || 192.168.3.237; router.pushUrl({ url: View/ChatView, params: { userName: name, userID: user_ Date.now(), serverIP: ip } }).catch((err: BusinessError) { hilog.error(DOMAIN, testTag, 跳转失败: %{public}s, JSON.stringify(err)); }); }) Text(提示手机和电脑需连接同一WiFi。) .fontSize(12) .fontColor(#BBBBBB) .margin({ top: 20 }) Text(需在终端安装依赖 npm install ws) .fontSize(12) .fontColor(#BBBBBB) .margin({ top: 5 }) Text(需在终端开启服务端 node server.js。) .fontSize(12) .fontColor(#BBBBBB) .margin({ top: 5 }) Text(房主可在终端输入 ipconfig 查看IPv4地址) .fontSize(12) .fontColor(#BBBBBB) .margin({ top: 5 }) } .width(100%) .height(100%) .alignItems(HorizontalAlign.Center) } aboutToAppear(): void { hilog.info(DOMAIN, testTag, %{public}s, Ability onCreate); this.loginWithHuaweiID(); } private loginWithHuaweiID(): void { const loginRequest: authentication.HuaweiIDProvider new authentication.HuaweiIDProvider(); const request: authentication.LoginWithHuaweiIDRequest loginRequest.createLoginWithHuaweiIDRequest(); request.forceLogin false; const controller: authentication.AuthenticationController new authentication.AuthenticationController(); controller.executeRequest(request).then((data: authentication.LoginWithHuaweiIDResponse) { const authCode: string data.data?.authorizationCode || ; // 用华为账号信息作为用户名 if (authCode) { this.userName HW_ authCode.substring(0, 6); } hilog.info(DOMAIN, testTag, 登录成功); }).catch((error: BusinessError) { hilog.error(DOMAIN, testTag, error: %{public}s, JSON.stringify(error)); // 登录失败用默认昵称 this.userName 用户 Math.floor(Math.random() * 1000); }); } }EntryFormAbility:import { formBindingData, FormExtensionAbility, formInfo } from kit.FormKit; import { Want } from kit.AbilityKit; export default class EntryFormAbility extends FormExtensionAbility { onAddForm(want: Want) { // Called to return a FormBindingData object. const formData ; return formBindingData.createFormBindingData(formData); } onCastToNormalForm(formId: string) { // Called when the form provider is notified that a temporary form is successfully // converted to a normal form. } onUpdateForm(formId: string) { // Called to notify the form provider to update a specified form. } onFormEvent(formId: string, message: string) { // Called when a specified message event defined by the form provider is triggered. } onRemoveForm(formId: string) { // Called to notify the form provider that a specified form has been destroyed. } onAcquireFormState(want: Want) { // Called to return a {link FormState} object. return formInfo.FormState.READY; } }小结本项目实现了一个基于鸿蒙元服务的局域网多人聊天室采用ArkTS ArkUI构建客户端通过WebSocket与Node.js服务端实时通信。核心成果实现了用户登录、房间加入、消息收发、界面刷新等完整聊天流程采用 MVVM 架构分离视图与逻辑代码结构清晰通过 senderID 去重机制避免消息重复显示支持华为账号认证获取用户身份存在问题端口配置不统一服务端3000 / 客户端8080缺少连接状态提示、自动重连、心跳保活服务端广播包含发送者自身浪费带宽我没有其它手机验证能不能连成功改进方向统一端口配置并增加连接状态UI添加断线重连与心跳机制优化服务端广播逻辑排除发送者