基于 HarmonyOS ArkTS 的局域网多人基础聊天室开发
一、项目定位
这是一个基于 HarmonyOS(ArkTS)开发的局域网多人实时聊天室应用,配合 Node.js 后端服务器,支持多台设备同时在线聊天。项目的核心目标是:让多台 HarmonyOS 设备通过同一局域网内的服务器,实现实时消息收发、在线状态同步、用户信息管理及数据持久化。
客户端负责 UI 展示与用户交互,服务端负责消息路由、用户管理和在线统计,两者通过 HTTP + WebSocket 双协议通信。
二、核心功能清单
1.用户登录 :输入账号密码登录,服务器在线则服务端登录,离线则本地登录;支持上次登录账号密码自动填充 。
2.实时消息: 通过 WebSocket 实时收发消息,消息气泡区分自己/他人,系统消息居中展示 。
3. 历史消息 :通过 HTTP 拉取服务器历史消息,与本地消息合并去重显示 。
4.在线统计:30 秒心跳包检测,标题栏实时显示在线人数和连接状态
5.修改昵称 : 在个人信息页修改昵称,实时同步到聊天消息和历史记录
6.数据持久化: 用户信息用 Preferences 存储,聊天记录用 JSON 文件存储,应用重启数据不丢失
7.服务器配置 :设置页可修改 HTTP/WebSocket 地址,适配模拟器和真机不同网络环境
8.重复登录防护: 同一账号不允许在两台设备上同时登录
9.退出登录 : 断开 WebSocket 连接,广播离开消息,清除登录态
三、项目架构与文件结构
整体文件架构:
┌─────────────────────┐ ┌─────────────────────┐
│ HarmonyOS 客户端 │ │ Node.js 服务端 │
│ │ │ │
│ Pages (UI 层) │ HTTP │ HTTP Server :3000 │
│ ├─ LoginPage │ ───────> │ ├─ /api/login │
│ ├─ ChatPage │ <────── │ ├─ /api/messages │
│ ├─ ProfilePage │ │ └─ /api/online │
│ └─ SettingsPage │ │ │
│ │ WS │ WebSocket :8080 │
│ Services (业务层) │ ───────> │ ├─ join/leave │
│ ├─ WebSocketSvc │ <────── │ ├─ message │
│ ├─ HttpService │ │ ├─ rename │
│ ├─ AxiosService │ │ └─ heartbeat │
│ ├─ UserStore │ │ │
│ ├─ MessageStore │ │ 数据存储 │
│ └─ ServerConfig │ │ ├─ users Map │
│ │ │ ├─ messages [] │
│ Models (数据层) │ │ └─ registeredUsers│
│ ├─ UserInfo │ │ │
│ └─ ChatMessage │ │ │
└─────────────────────┘ └─────────────────────┘
客户端文件架构:
entry/src/main/ets/
├── entryability/
│ └── EntryAbility.ets # 应用入口 Ability,设置键盘避让模式
├── model/
│ └── UserModel.ets # 数据模型:UserInfo、ChatMessage、colorForNickname
├── service/
│ ├── AxiosService.ets # Axios 登录/注册请求
│ ├── HttpService.ets # HTTP 拉取历史消息
│ ├── MessageStore.ets # JSON 文件读写聊天记录
│ ├── ServerConfig.ets # Preferences 存储服务器地址
│ ├── UserStore.ets # Preferences 存储用户信息
│ └── WebSocketService.ets # WebSocket 实时通信 + 心跳
├── pages/
│ ├── Index.ets # 应用根页面,初始化服务 + 路由分发
│ ├── LoginPage.ets # 登录页
│ ├── ChatPage.ets # 聊天主页面
│ ├── ProfilePage.ets # 个人信息页
│ └── SettingsPage.ets # 服务器设置页
└── entrybackupability/
└── EntryBackupAbility.ets # 备份扩展
server/
└── server.js # Node.js 后端:HTTP + WebSocket 合一服务器
分层设计原则:
项目遵循三层架构:
1.Model 层 (model/UserModel.ets):纯数据定义,不含业务逻辑。定义了 UserInfo(用户信息)和 ChatMessage(聊天消息)两个核心类,以及根据昵称生成头像颜色的 `colorForNickname` 工具函数。
2. Service 层(service/ 目录):所有业务逻辑和网络通信都封装在此,采用单例模式,全局共享同一实例。每个 Service 职责单一:
- WebSocketService:只管 WebSocket 连接、消息收发、心跳
- HttpService:只管 HTTP 请求(历史消息)
- AxiosService:只管 Axios 请求(登录注册)
- UserStore:只管用户信息的读写持久化
- MessageStore:只管聊天消息的读写持久化
- ServerConfig:只管服务器地址配置
3. Page 层(pages/ 目录):纯 UI 展示与交互,通过调用 Service 层完成业务操作,不直接处理网络请求和数据存储。
四、数据模型设计
1.UserInfo - 用户信息
export class UserInfo {
userId: string = ''; // 用户唯一ID,由服务器生成或本地生成
nickname: string = ''; // 昵称
avatarColor: string = ''; // 头像颜色,根据昵称 hash 生成
joinTime: number = 0; // 首次加入时间
isLogin: boolean = false; // 是否已登录
}
关键设计:userId 是用户的唯一标识,不会因昵称修改而改变。这保证了消息归属判断的稳定性——用 userId 判断 isMine,而非用 nickname。
2.ChatMessage - 聊天消息
export class ChatMessage {
id: string = ''; // 消息唯一ID(timestamp_random)
userId: string = ''; // 发送者 userId
nickname: string = ''; // 发送者昵称(可变)
avatarColor: string = ''; // 头像颜色
content: string = ''; // 消息内容
timestamp: number = 0; // 时间戳
isMine: boolean = false; // 是否是自己发的(UI 展示用)
isSystem: boolean = false; // 是否是系统消息
}
关键设计:id 使用 timestamp_random 格式生成,保证唯一性。当需要触发 ForEach 重渲染时(如昵称修改后更新历史消息),会给 id 追加 _r${Date.now()} 或 _s${Date.now()} 后缀,因为 ArkUI 的 ForEach 通过比对 id 来决定是否重新渲染,直接修改对象属性不会触发 UI 更新。
3.头像颜色生成
const AVATAR_COLORS = [
'#E57373', '#4FC3F7', '#81C784', '#FFB74D',
'#BA68C8', '#4DD0E1', '#FF8A65', '#AED581',
'#F06292', '#7986CB', '#DCE775', '#FFD54F',
];
export function colorForNickname(nickname: string): string {
let hash = 0;
for (let i = 0; i < nickname.length; i++) {
hash = nickname.charCodeAt(i) + ((hash << 5) - hash);
}
return AVATAR_COLORS[Math.abs(hash) % AVATAR_COLORS.length];
}
对昵称做 hash 运算后映射到 12 种预设颜色,同一昵称始终对应同一颜色,不同昵称大概率不同颜色。
五、服务端实现
服务端基于 Node.js,一个文件 `server.js` 实现了 HTTP + WebSocket 双协议服务器。
HTTP 服务(端口 3000)
| 接口 | 方法 | 功能 |
|------|------|------|
| `/api/login` | POST | 用户登录,返回 userId + nickname;检测重复登录返回 code -2 |
| `/api/register` | POST | 用户注册 |
| `/api/messages` | GET | 拉取历史消息,支持 `since` 参数增量获取 |
| `/api/online` | POST | 查询在线人数 |
| `/api/user/:userId` | GET | 查询用户信息 |
WebSocket 服务(端口 8080)
| 消息类型 | 方向 | 功能 |
|---------|------|------|
| `join` | 客户端→服务端 | 用户上线,服务端广播给其他客户端 |
| `message` | 客户端→服务端 | 发送消息,服务端广播给其他客户端 |
| `rename` | 客户端→服务端 | 修改昵称,服务端广播通知 |
| `heartbeat` | 客户端→服务端 | 心跳包,服务端返回在线人数 |
| `leave` | 服务端→客户端 | 用户下线广播 |
| `online_count` | 服务端→客户端 | 在线人数更新 |
核心数据结构
const users = new Map(); // userId → { userId, nickname },在线用户
const registeredUsers = new Map(); // nickname → userId,已注册用户(跨登录保留)
const messages = []; // 历史消息数组,最多保留 500 条
const wsClients = new Map(); // ws → { userId, nickname },WebSocket 连接映射
广播机制
服务端广播时跳过发送者的 WebSocket 连接(client !== excludeWs),而非按 userId 过滤。这保证了:
- 发送者不会收到自己的消息回声
- 同一 userId 如果有多连接(被 code -2 阻止前的情况)也能正确过滤
重复登录检测
if (userId && users.has(userId)) {
res.end(JSON.stringify({ code: -2, message: '该账号已在线,不能重复登录' }));
return;
}
登录时检查该 userId 是否已在 users Map 中,如果已在线则拒绝登录。
昵称修改与 registeredUsers 同步
当用户修改昵称时,服务端不仅更新 users Map,还会同步更新 registeredUsers Map:删除旧昵称→userId 的映射,添加新昵称→userId 的映射。这保证了用户修改昵称后再退出登录,下次用新昵称登录时仍能获得相同的 userId。
六、客户端核心实现
1.应用入口与初始化
Index.ets 是应用根页面,在 `aboutToAppear` 中完成所有服务的初始化:
async aboutToAppear(): Promise<void> {
const context = getContext(this) as common.UIAbilityContext;
// 1. 初始化用户存储
const userStore = UserStore.getInstance();
await userStore.init(context);
// 2. 初始化服务器配置
const serverConfig = ServerConfig.getInstance();
await serverConfig.init(context);
// 3. 将服务器地址注入各 Service
const wsService = WebSocketService.getInstance();
wsService.setServerUrl(serverConfig.wsUrl);
const httpService = HttpService.getInstance();
httpService.setBaseUrl(serverConfig.httpBaseUrl);
const axiosService = AxiosService.getInstance();
axiosService.setBaseUrl(serverConfig.httpBaseUrl);
// 4. 检查登录态,路由分发
const user = userStore.getUserInfo();
if (user.isLogin) {
this.navPathStack.replacePathByName('Chat', undefined);
} else {
this.navPathStack.replacePathByName('Login', undefined);
}
}
页面导航使用 Navigation + NavPathStack,路由配置在 route_map.json 中声明,支持 replacePathByName 和 pushPathByName 两种跳转方式。
2.登录页 - LoginPage
登录页的 handleLogin 方法是整个登录流程的核心:
登录流程:
1. 检测网络:调用 WebSocketService.checkNetworkAvailability() 检查网络是否可用
2. 服务端登录:网络可用时,通过 AxiosService.login() 向服务端发送 POST 请求
- 成功(code 0):获取服务端分配的 userId
- 重复登录(code -2):提示"该账号已在线",停止登录
- 失败(code -1):降级为本地登录
3. 本地补充:检查本地 UserStore 是否已有该用户信息
- 老用户:从本地获取已有的 userId 和 joinTime
- 新用户:使用服务端返回的 userId 或本地生成
4. 持久化:调用 saveUserInfo() 保存用户信息,调用 saveLastPassword() 保存密码
5. 跳转:导航到聊天页
自动填充:aboutToAppear 时从 UserStore 读取上次登录的账号密码,自动填入输入框。
3.聊天页 - ChatPage
聊天页是项目最复杂的页面,负责消息展示、收发、历史加载、昵称同步等。
aboutToAppear 初始化
aboutToAppear(): void {
const context = getContext(this) as common.UIAbilityContext;
const userStore = UserStore.getInstance();
userStore.init(context).then(async () => {
const user = userStore.getUserInfo();
if (!user.isLogin) { this.navPathStack.replacePathByName('Login', undefined); return; }
this.currentUser = user;
// 1. 加载本地消息,用当前用户信息刷新 isMine/nickname/avatarColor
this.messageStore.setContext(context);
this.messages = this.messageStore.loadMessages();
// ... 遍历消息,根据 userId 重算 isMine,覆盖自己的 nickname/avatarColor
this.messageStore.saveMessages(this.messages);
// 2. 拉取服务器历史消息,与本地去重合并
await this.loadHistoryMessages();
// 3. 添加系统消息
const sysMsg = ChatMessage.createSystem(`${this.currentUser.nickname} 加入了群聊`);
this.messages = [...this.messages, sysMsg];
// 4. 注册 WebSocket 回调
this.wsService.setOnMessage(...);
this.wsService.setOnConnect(...);
this.wsService.setOnDisconnect(...);
this.wsService.setOnOnlineCount(...);
this.wsService.setOnRename(...);
// 5. 建立 WebSocket 连接
this.wsService.connect(user.userId);
});
}
历史消息去重
服务端返回的历史消息可能与本地已有的重复(同一用户、同一时间、同一内容),使用 userId + timestamp + content 组合键去重:
const existingKeys: Record<string, boolean> = {};
for (let i = 0; i < this.messages.length; i++) {
const key = `${this.messages[i].userId}_${this.messages[i].timestamp}_${this.messages[i].content}`;
existingKeys[key] = true;
}
// 遍历历史消息,只添加 existingKeys 中不存在的
不用 ID 去重是因为服务端和客户端各自生成的 ID 不同(都含随机数),无法匹配。
onShown - 页面恢复时的数据同步
这是解决"修改昵称后历史消息不更新"的关键。当从 ProfilePage 返回或从后台恢复时,onShown 回调触发:
.onShown(() => {
const user = userStore.getUserInfo();
this.currentUser = user;
// 遍历所有消息,自己的消息用最新 nickname/avatarColor 覆盖
// 系统消息中包含旧昵称的,替换为新昵称
// 任何字段变化则给 id 加 _r 后缀触发 ForEach 重渲染
this.messageStore.saveMessages(this.messages);
// 如果昵称变了,通过 WebSocket 广播 rename
if (oldNickname !== user.nickname) {
this.wsService.sendRename(user.userId, user.nickname, user.avatarColor);
}
})
消息发送
private sendMessage(): void {
const content = this.inputText.trim();
this.inputText = '';
const timestamp = Date.now();
// 1. 本地立即展示
const myMsg = ChatMessage.create(this.currentUser.userId, this.currentUser.nickname, content, true);
myMsg.timestamp = timestamp;
this.messages = [...this.messages, myMsg];
this.messageStore.saveMessages(this.messages);
// 2. 通过 WebSocket 发送给服务端广播
this.wsService.sendMessage(
this.currentUser.userId, this.currentUser.nickname,
content, timestamp, myMsg.avatarColor,
);
}
发送时使用单一 Date.now() 同时赋给 id 和 timestamp,保证一致性。传递真实的 avatarColor(而非硬编码),确保服务端广播时其他客户端能正确显示头像颜色。
消息气泡 UI
聊天消息通过 isMine 区分左右布局:
- 自己的消息:右侧显示,蓝色气泡,右下角小圆角(bottomRight: 4),左侧大圆角
- 他人的消息:左侧显示,白色气泡,左下角小圆角(bottomLeft: 4),右侧大圆角,上方显示昵称
- 系统消息:居中显示,灰色圆角背景
使用 constraintSize({ maxWidth: '70%' }) 限制气泡最大宽度,textOverflow({ overflow: TextOverflow.Ellipsis }) 处理昵称溢出。
键盘弹起自动滚动
通过 onAreaChange 监听页面高度变化,当高度减小时(键盘弹起),自动滚动到底部:
.onAreaChange((oldArea: Area, newArea: Area) => {
const oldH = oldArea.height as number;
const newH = newArea.height as number;
if (newH < oldH) {
setTimeout(() => { this.listScroller.scrollEdge(Edge.Bottom); }, 100);
}
})
同时在 EntryAbility 中设置了 KeyboardAvoidMode.RESIZE,让页面内容随键盘弹起而调整。
4.WebSocket 服务 - WebSocketService
连接管理
connect(userId: string): void {
// 1. 如果已有连接,先关闭旧连接的所有回调
if (this.ws !== undefined) {
this.ws.off('open');
this.ws.off('message');
this.ws.off('close');
this.ws.off('error');
this.ws.close();
}
this.currentUserId = userId;
// 2. 创建新 WebSocket
this.ws = webSocket.createWebSocket();
// 3. 注册回调
this.ws.on('open', ...);
this.ws.on('message', ...);
this.ws.on('close', ...);
this.ws.on('error', ...);
// 4. 发起连接
this.ws.connect(this.serverUrl);
}
消息接收与分发
on('message') 回调中根据 `type` 字段分发处理:
- message:过滤掉自己发的消息(data.userId === this.currentUserId),构造 ChatMessage 通知 UI
- join:构造系统消息 "xxx 加入了群聊"
- leave:构造系统消息 "xxx 离开了群聊,当前还有 N 人在线"
- rename:触发 onRenameCallback,通知 ChatPage 更新历史消息中的昵称
- online_count:更新 this.onlineCount,触发 onOnlineCountCallback
心跳机制
连接成功后启动 30 秒间隔心跳:
private startHeartbeat(): void {
this.stopHeartbeat();
this.sendHeartbeat();
this.heartbeatTimer = setInterval(() => { this.sendHeartbeat(); }, 30000);
}
private sendHeartbeat(): void {
const data = { type: 'heartbeat', userId: this.currentUserId, ... };
this.ws.send(JSON.stringify(data));
}
服务端收到心跳后返回 { type: 'online_count', count: users.size },客户端据此更新标题栏在线人数。
网络检测
checkNetworkAvailability(): boolean {
const netHandle = connection.getDefaultNetSync();
return netHandle.netId !== 0;
}
登录前检测网络可用性,不可用时降级为本地登录模式。
5.数据持久化
UserStore - Preferences 存储
使用 HarmonyOS 的 @kit.ArkData 提供的 Preferences API,键值对存储用户信息:
| 键 | 值 | 说明 |
|----|-----|------|
| `user_id` | string | 当前登录用户 ID |
| `nickname` | string | 当前昵称 |
| `avatar_color` | string | 当前头像颜色 |
| `is_login` | boolean | 登录状态 |
| `join_time_{nickname}` | number | 每个昵称的首次加入时间 |
| `user_id_{nickname}` | string | 每个昵称对应的 userId |
| `last_username` | string | 上次登录账号 |
| `last_password` | string | 上次登录密码 |
关键细节:
1. 必须 flush:putSync只写入内存缓存,必须调用 await store.flush() 才能持久化到磁盘。所有写操作后都跟了flush()。
2. 单例 + initialized 标志init() 只在首次调用时创建 Preferences 实例,后续调用直接返回。避免重复创建实例导致缓存数据丢失。
3. 昵称迁移:saveUserInfo 检测到昵称变化时,自动将旧昵称的 joinTime 和 userId 映射复制到新昵称下。
4. 批量写入 + 单次 flush:所有 putSync 操作完成后才调用一次 flush(),避免多次异步 flush 导致的竞争条件。
MessageStore - JSON 文件存储
使用 @kit.CoreFileKit 的文件读写 API,将聊天消息序列化为 JSON 存储到应用沙箱目录:
private getFilePath(): string {
return `${this.context.filesDir}/chat_messages.json`;
}
- 读取:fs.readTextSync 读取文件内容,JSON.parse 反序列化为 ChatMessage 数组
- 写入:fs.openSync 以 TRUNC 模式打开(清空重写),fs.writeSync 写入 JSON 字符串
每次消息变化(发送、接收、昵称更新)都会立即调用 saveMessages 写入文件,保证数据不丢失。
ServerConfig - 服务器地址存储
同样使用 Preferences,存储 HTTP 基地址和 WebSocket 地址,默认值为模拟器地址 10.0.2.2。
七、权限声明与网络配置
权限
在module.json5 中声明:
"requestPermissions": [
{ "name": "ohos.permission.INTERNET" },
{ "name": "ohos.permission.GET_NETWORK_INFO" }
]
- INTERNET:允许网络访问(HTTP/WebSocket 通信必须)
- GET_NETWORK_INFO:允许检测网络状态(登录前判断网络可用性)
网络地址配置
- 模拟器:使用 10.0.2.2 访问宿主机(模拟器的特殊地址),HTTP 地址 http://10.0.2.2:3000,WebSocket 地址 ws://10.0.2.2:8080
- 真机:需要使用电脑的局域网 IP(如 192.168.x.x),在设置页修改
服务器地址可在应用内动态修改,修改后保存到 Preferences,下次启动自动加载。
八、页面导航设计
使用 Navigation 组件 +NavPathStack 实现页面导航:
Index(根页面)
├─ Login(登录页)── replacePathByName ──→ Chat
├─ Chat(聊天页)── pushPathByName ──→ Profile / Settings
│ ── replacePathByName ──→ Login(退出登录)
├─ Profile(个人信息页)── pop ──→ Chat
└─ Settings(服务器设置页)── pop ──→ Login
路由配置在 route_map.json 中声明,每个页面通过@Builder 导出函数注册:
{
"routerMap": [
{ "name": "Login", "pageSourceFile": "src/main/ets/pages/LoginPage.ets", "buildFunction": "LoginBuilder" },
{ "name": "Chat", "pageSourceFile": "src/main/ets/pages/ChatPage.ets", "buildFunction": "ChatBuilder" },
{ "name": "Profile", "pageSourceFile": "src/main/ets/pages/ProfilePage.ets", "buildFunction": "ProfileBuilder" },
{ "name": "Settings", "pageSourceFile": "src/main/ets/pages/SettingsPage.ets", "buildFunction": "SettingsBuilder" }
]
}
@Consume('navPathStack') 在各页面中获取共享的 NavPathStack 实例,实现统一导航控制。
九、踩坑与解决方案
1.ForEach 不刷新问题
问题:修改 ChatMessage 对象的属性后,UI 不更新。
原因:ArkUI 的 ForEach 通过比对 key(第二个参数返回的 `msg.id`)来决定是否重新渲染。如果 id 不变,即使对象属性变了也不会更新。
解决:需要修改消息时,创建新的 ChatMessage 对象,并给 id 追加后缀(如 _r${Date.now()}),使 ForEach 检测到 key 变化从而重渲染。
2.服务端广播回声
问题:发送消息后,自己收到了自己的消息,导致重复显示。
解决:服务端广播时跳过发送者的 WebSocket 连接(client !== excludeWs);客户端也在 onMessage 中增加userId === currentUserId 的过滤,双保险。
3. TCP + WebSocket 双连接冗余
问题:最初用 TCP 长连接做心跳/在线统计,WebSocket 做消息收发,两个连接、两个端口,维护复杂。
解决:将心跳合并到 WebSocket 中,客户端每 30 秒发送 heartbeat 类型消息,服务端返回在线人数。删除了独立的 TCP 服务,架构更简洁。
4. 修改昵称后历史消息不更新
问题:在个人信息页修改昵称后返回聊天页,历史消息仍显示旧昵称。
原因:ChatPage 的 currentUser 在 aboutToAppear时设置,从 ProfilePage 返回时不会重新触发 aboutToAppear。
解决:使用 NavDestination 的 onShown 回调,每次页面显示时重新从 UserStore 读取用户信息,遍历所有消息用最新数据覆盖,并给 id 加后缀触发重渲染。同时在 aboutToAppear 中也做同样的覆盖,确保冷启动时也能修正。
5.关闭后台后登录态丢失
问题:应用切到后台再回来,登录态消失了。
原因:UserStore.init() 每次被调用都会创建新的 Preferences 实例,新实例从磁盘读取数据时,可能读到 flush 尚未完成时的旧数据。
解决:给 UserStore 加 initialized 标志,init() 只在首次调用时创建 Preferences 实例,后续调用直接返回,保证整个生命周期使用同一个内存缓存实例。
6. 同一账号多设备登录
问题:同一账号可以在两台设备上同时登录。
解决:服务端登录接口检查 userId 是否已在users Map 中,如果已在线则返回 code: -2。客户端收到 -2 后显示错误提示,阻止登录。
7. 退出登录再进来消息丢失
问题:退出登录后重新登录,之前的聊天消息没了。
原因:aboutToDisappear 不一定每次都会执行(如应用被系统杀死),导致消息没有及时保存。
解决:在 onShown 中增加检测——如果本地文件中的消息数量大于内存中的消息数量,说明有消息丢失,从文件恢复。同时在每次消息变化时都立即调用 saveMessages,减少丢失风险。
8. 换账号登录消息归属错误
问题:A 账号登录发了消息,退出后用 B 账号登录,A 的消息仍然显示在右边。
原因:JSON 文件中保存的 isMine 是 A 账号时的值(true),B 账号加载后没有重新计算。
解决:aboutToAppear 加载消息后,遍历所有消息,根据 userId === currentUser.userId 重新计算 isMine,并用当前用户的 nickname/avatarColor 覆盖自己消息的显示信息。
9. Preferences 不落盘
问题:修改个人信息后关闭应用,再次打开发现修改没保存。
原因:putSync 只写入内存缓存,不保证立即写入磁盘。
解决:所有putSync 操作后必须调用 await store.flush() 强制落盘。并且在 saveUserInfo 中采用"批量 putSync + 单次 flush"策略,避免多个异步 flush 的竞争条件。
十、项目运行
启动服务端
cd server
npm install ws
node server.js
服务端启动后会监听:
- HTTP API:http://localhost:3000
- WebSocket:ws://localhost:8080
启动客户端
1. 在 DevEco Studio 中打开项目
2. 连接 HarmonyOS 设备或启动模拟器
3. 如果使用真机,在设置页将服务器地址改为电脑的局域网 IP
4. 运行应用
多设备测试
1. 确保所有设备和运行服务端的电脑在同一局域网
2. 在每台设备上安装应用
3. 在设置页配置正确的服务器地址
4. 各自登录不同账号即可开始聊天
十一、总结
这个聊天室项目虽然功能不算复杂,但覆盖了 HarmonyOS 应用开发的多个核心领域:
1.ArkUI 声明式 UI:@State/@Consume 状态管理、@Builder 组件封装、ForEach 列表渲染、Navigation 导航
2. 网络通信:HTTP 请求、第三方库 Axios、WebSocket 实时通信、心跳机制
3. 数据持久化:Preferences 键值存储、文件读写 JSON
4.权限管理:INTERNET、GET_NETWORK_INFO 声明与使用
5. 生命周期管理:aboutToAppear/aboutToDisappear、onShown/onHidden、组件销毁前清理资源
6. 前后端联调:HTTP 接口定义、WebSocket 消息协议、数据格式约定
7. 边界情况处理:重复登录、昵称同步、消息去重、数据持久化时机、ForEach 重渲染
项目中遇到的很多坑(如 ForEach 不刷新、Preferences 不落盘、aboutToDisappear 不保证执行等)都是 HarmonyOS/ArkTS 开发中特有的问题,解决这些问题的过程也是深入理解 ArkUI 响应式机制和 HarmonyOS 数据存储机制的过程。
更多推荐

所有评论(0)