上一篇文章《小程序 im 聊天功能实现:基于 ZEGO ZIM 的接入教程》做的是单聊功能。这篇做聊天室:直播间的公屏、语聊房的在线列表、活动页的讨论区,本质都是同一件事即一群人进同一个房间,实时收发消息,有人进出你能看见。
环境准备沿用上一篇(开通 ZIM 服务、配置服务器域名、服务端签发 Token、SDK 初始化封装),本文从已经登录成功开始,跟着做大概 30 分钟。
第一步:先分清聊天室和单聊
很多人上手聊天室,第一反应是把单聊的 conversationType 从 0 改成 2,当群聊用。方向就错了。
ZEGO ZIM 里聊天室是房间,和单聊、群聊是三套不同的东西:
| 单聊 | 聊天室(房间) | |
|---|---|---|
conversationType | 0 | 1 |
toConversationID | 对方 userID | roomID |
| 会话列表 | 有 | 不在会话列表范畴 |
| 未读数 | 有,可以清 | 没有这一说 |
| 用户离开后 | 离线消息会补 | 收不到,也没有补 |
| 消息存储 | 云端保存,可翻历史 | 房间解散后不存储 |
| 规模 | 一对一 | 免费 20 / 专业 500 / 旗舰 6000 |
这几条差异会直接改写你的页面设计:
- 聊天室没有会话列表入口。用户是从直播间、活动页带着 roomID 进来的,不是从消息列表点进来的。所以你不需要
queryConversationList,也不需要维护最后一条消息和红点。 - 不需要清未读。没有未读数这个概念,
clearConversationUnreadMessageCount在房间场景用不上。 - 不要做消息补偿。用户退出房间再进来,中间的消息是真的看不到,普通房间消息在房间解散后不存储,只有旗舰版才支持进房后拉最近 2000 条。
第二步:进入房间
三个接口都能进房间,区别只有一个:房间不存在时怎么办。
| 接口 | 房间不存在时 | 什么时候用 |
|---|---|---|
createRoom(roomInfo, config) | 创建并加入 | 必须由当前用户建房,房间已存在会报错(6000323) |
joinRoom(roomID) | 直接失败 | 确定房间已经存在 |
enterRoom(roomInfo, config) | 自动创建再进入 | 聊天室,推荐 |
聊天室的 roomID 一般由业务生成(直播间 ID、活动 ID),第一个用户点进来时房间本来就不存在。用 enterRoom 一步到位,不用在客户端判断「该建还是该进」:
import { ZIMRoomInfo, ZIMRoomAdvancedConfig } from 'zego-zim-miniprogram';
import { getZim } from '../../utils/zim';
export async function enterChatRoom(roomID: string, roomName: string) {
const roomInfo: ZIMRoomInfo = { roomID, roomName };
const config: ZIMRoomAdvancedConfig = {
roomAttributes: { title: roomName },
roomDestroyDelayTime: 60, // 最后一个人离开后延迟 60 秒销毁,上限 3 小时
};
await getZim().enterRoom(roomInfo, config);
}
两条规则先记住:
- roomID 最长 128 字节,只允许数字、英文字母和常见英文标点。不要用中文、空格、emoji。roomName 最长 64 字节,没有字符限制。
- roomName 和 roomAttributes 只在「第一个进房的人」身上生效。后面进来的人传什么都覆盖不了。所以房间标题、公告这类会变的内容,不要指望进房参数,要用第六步的
setRoomAttributes改。
2.1 注册房间事件监听
当前 SDK 注册事件用 zim.on(type, listener),移除用 zim.off(type)。上一篇的 initZimEvents 只处理了单聊事件,房间相关的在 utils/zim.ts 里补一个房间版本:
// utils/zim.ts
import { ZIMEventHandler } from 'zego-zim-miniprogram';
export function initRoomEvents(handler: {
roomStateChanged?: ZIMEventHandler['roomStateChanged'];
roomMemberJoined?: ZIMEventHandler['roomMemberJoined'];
roomMemberLeft?: ZIMEventHandler['roomMemberLeft'];
roomAttributesUpdated?: ZIMEventHandler['roomAttributesUpdated'];
}) {
const zim = getZim();
if (handler.roomStateChanged) zim.on('roomStateChanged', handler.roomStateChanged);
if (handler.roomMemberJoined) zim.on('roomMemberJoined', handler.roomMemberJoined);
if (handler.roomMemberLeft) zim.on('roomMemberLeft', handler.roomMemberLeft);
if (handler.roomAttributesUpdated) zim.on('roomAttributesUpdated', handler.roomAttributesUpdated);
}
注意别在页面里反复注册。官方类型声明里写明重复调用会覆盖上一次注册的回调,所以 App 里最好统一在入口注册一次,再按当前页面分发。
2.2 进房成功的信号
enterRoom 的 Promise resolve 只代表请求被受理了,不代表房间连接已经就绪。这个时间差里发消息会失败。
真正的信号是 roomStateChanged 事件:
// pages/chatroom/index.ts
Page({
data: { roomID: '', roomName: '', messages: [], onlineCount: 0 },
inRoom: false,
seenMessageIds: new Set<string>(),
onLoad(options) {
this.setData({ roomID: options.roomID, roomName: options.roomName || '' });
initRoomEvents({
roomStateChanged: (zim, result) => {
const { state, event } = result;
if (state === 2 && event === 0) {
// Connected(2) + Success(0):进房成功,此刻才能发消息
this.inRoom = true;
this.syncOnlineCount(); // 第五步实现
} else if (state === 0) {
// Disconnected(0):退房成功、网络断开、被顶下线、房间被后台销毁
this.inRoom = false;
}
},
roomMemberJoined: (zim, result) => {
this.setData({ onlineCount: this.data.onlineCount + result.memberList.length });
},
roomMemberLeft: (zim, result) => {
this.setData({ onlineCount: Math.max(0, this.data.onlineCount - result.memberList.length) });
},
});
getZim().on('messageReceived', (zim, result) => this.handleRoomMessage(result));
enterChatRoom(this.data.roomID, this.data.roomName);
},
});
event 要一起看。同样是 state === 0,event === 10 是被别的设备顶下线(多端登录),不是网络抖动,这时候去重连会立刻又被踢。
先跑一次:进房后控制台应该看到 state=2, event=0。没等到就依次查三件事:ZIM 服务是否在控制台开通了;roomID 有没有中文或超长;免费版单个用户只能同时在 1 个房间内,之前的房间没退出会报 6000325。
第三步:发送消息
房间消息和单聊走同一个接口,只换两个参数:toConversationID 传 roomID,conversationType 传 1。
export async function sendRoomText(roomID: string, text: string) {
if (!text.trim()) return; // 空消息会被拒,错误码 6000001
// 消息对象直接用对象字面量,type = 1 表示文本消息
const message = { type: 1, message: text };
const result = await getZim().sendMessage(
message,
roomID,
1, // 1 = Room
{ priority: 1 } // 1 = Low,2 = Medium,3 = High
);
return result.message;
}
房间场景有个单聊不那么容易撞上的限制:两次发送的间隔要大于 100ms(单客户端 10 次/秒)。用户连点、或者你做了「发送后自动补一条系统提示」,很容易撞上频率限制。加个最小间隔:
let lastSendAt = 0;
export async function sendThrottled(roomID: string, text: string) {
const wait = 100 - (Date.now() - lastSendAt);
if (wait > 0) await new Promise((r) => setTimeout(r, wait));
lastSendAt = Date.now();
return sendRoomText(roomID, text);
}
如果你做的是直播弹幕那种高频场景,别用文本消息硬扛。ZIM 有专门的 ZIMBarrageMessage(type 20),不限频、不存储、允许丢,就是为弹幕设计的。普通聊天室的公屏还是用文本消息,保住送达和顺序。
第四步:接收消息
messageReceived 是统一的,房间消息也从这里出来,靠 conversationType 和 conversationID 过滤:
handleRoomMessage(result) {
const { messageList, receivedInfo } = result;
if (receivedInfo.conversationType !== 1) return; // 只要房间消息
if (receivedInfo.conversationID !== this.data.roomID) return; // 只要当前房间
messageList.forEach((msg) => {
if (this.seenMessageIds.has(msg.messageID)) return;
this.seenMessageIds.add(msg.messageID);
this.appendMessage(msg);
});
}
和单聊相比少了三步:不用清未读、不用更新会话列表、不用管总未读角标。
但有一件事不能省:去重。弱网重连时同一条消息可能被回调两次,房间消息量大,出现重复气泡的概率比单聊更高。seenMessageIds 这个 Set 记得在页面卸载时清掉,不然页面来回切几次会一直涨。
另外记住:退出房间后收不到房间内任何消息。这是设计如此,不是 bug——退房就等于彻底断开,你不用额外做任何屏蔽逻辑。
第五步:在线人数和成员列表
聊天室顶部一般要显示「当前 128 人在线」。三个接口分工:
| 接口 | 拿到什么 | 限制 |
|---|---|---|
queryRoomOnlineMemberCount(roomID) | 只拿数字 | 必须人在房间内,否则 6000321 |
roomMemberJoined / roomMemberLeft | 谁进来、谁走了 | 增量,断线期间会漏 |
queryRoomMemberList(roomID, config) | 完整成员列表 | 需要联系 ZEGO 开通,最多返回 500 人 |
在线人数直接查:
async syncOnlineCount() {
// roomID 必须是你当前所在的房间,否则查询失败
const res = await getZim().queryRoomOnlineMemberCount(this.data.roomID);
this.setData({ onlineCount: res.count });
}
成员进出靠回调做增量,进房成功后调一次 syncOnlineCount 兜底。这条链路在正常网络下够用。
坑在重连之后。 断线期间的成员变动,SDK 不会补发。你重连回来,roomMemberJoined 只会从重连那一刻开始报,中间走了谁、来了谁你都不知道。所以重连成功必须重新拉一次全量:
if (state === 2 && event === 0) {
this.syncOnlineCount();
this.syncMemberList(); // 重连后补全量,别只靠增量
}
需要成员头像和昵称的话,用 queryRoomMemberList(roomID, { nextFlag: '', count: 100 }):
async syncMemberList() {
let nextFlag = '';
const members = [];
do {
const res = await getZim().queryRoomMemberList(this.data.roomID, {
nextFlag,
count: 100,
});
members.push(...res.memberList);
nextFlag = res.nextFlag;
} while (nextFlag);
this.setData({ members });
}
这个接口默认没开,要联系 ZEGO 技术支持开通。如果只是想知道某几个人在不在房间里(比如判断好友有没有进直播间),用 queryRoomMembers(userIDs, roomID),一次最多查 10 个。
第六步:房间属性
房间属性是挂在房间上的 Key-Value,房间销毁时一起销毁。聊天室常见放法:房间标题、公告、背景图、当前话题、是否全员禁言。
const roomAttributes = {
notice: '今晚 8 点抽奖,别走开',
topic: '聊聊你最近在做的项目',
muted: '0',
};
await getZim().setRoomAttributes(roomAttributes, roomID, {
isForce: false, // true = 允许改别人设置的属性
isUpdateOwner: false, // true = 把自己变成该属性的 owner
isDeleteAfterOwnerLeft: false, // true = owner 离开房间时自动删除
});
上限先记住,超了直接报错(6000330 ~ 6000337):
| 项 | 上限 |
|---|---|
| 每个房间的属性个数 | 20 |
| Key 长度 | 16 字节 |
| Value 长度 | 1024 字节 |
| 所有属性总大小 | 5120 字节 |
真正卡人的是 5120 字节,不是 20 个。几条长公告塞进去就满了。内容长的东西只存 ID 或简短文案,正文放你自己的服务端,客户端拿到 ID 再去拉。
变更靠回调同步给房间内所有人:
getZim().on('roomAttributesUpdated', (zim, result) => {
console.log('[ZIM] room attrs updated', result.roomID, result.infos);
this.syncAllAttributes(); // 增量合并容易出错,直接重拉全量
});
result.infos 是变更项列表,每项包含变更类型和属性键值。做 UI 时最省事的做法就是收到通知后重拉一次 queryRoomAllAttributes(roomID)——属性总量本来就只有几 KB,一次请求换掉一堆合并逻辑,划算。
和成员列表同理:重连成功后要重新读一次全量,别只依赖增量回调。
第七步:断线和退房
7.1 SDK 不会给房间无限重连
这是房间和单聊最大的运维差异,也是最容易上线后才发现的坑:单聊断线后 SDK 会自己持续重连,但房间连接 SDK 只做有限次重连,之后就不管了。
结果是:用户安安静静地待在聊天室页面,界面看起来正常,实际一条消息都收不到。你不主动兜,这个 bug 会一直存在。
兜底思路是监听 roomStateChanged,掉线后自己按退避策略重进房:
async reconnect() {
if (this.reconnecting) return;
this.reconnecting = true;
let delay = 1000;
for (let i = 0; i < 3; i++) {
try {
await enterChatRoom(this.data.roomID, this.data.roomName);
this.reconnecting = false;
return;
} catch (err) {
await new Promise((r) => setTimeout(r, delay));
delay *= 2; // 1s → 2s → 4s
}
}
this.reconnecting = false;
wx.showToast({ title: '连接失败,请退出重进', icon: 'none' });
}
四个细节:
- 必须等状态落定再重连。 收到
state === 0的 DISCONNECTED 之后才调进房接口。连接状态还在中间态时重复调,会返回 6000326。 - 被顶下线不要重连。
event === 10是多端登录被踢,重连只会立刻再被踢一次,应该提示用户「该账号已在其他设备进入」。 - 房间被后台销毁也别重连。 SDK 2.13.0 起,服务端销毁房间时回调里的事件是
ROOM_NOT_EXIST,不是网络问题,重连也进不去,直接提示用户房间已结束。 - 重连成功要补全量。 成员和房间属性都要重拉,见第五、六步。
7.2 一定要退房
onUnload() {
getZim().leaveRoom(this.data.roomID).catch(() => {});
}
不退房的后果很具体:
- 房间一直算你在线,人数虚高
- 你还在房间里,房间每条消息都会推给你,白耗流量和电量
- 免费版单个用户只能同时进 1 个房间,不退房就进不了下一个
- 所有人都离开房间后房间才会自动销毁,你挂着就销毁不了
页面栈里跨了多个房间、或者拿不准自己在哪,用 leaveAllRoom() 一把清空(2.15.0 起支持)。
小结
- 聊天室用
conversationType = 1,toConversationID传 roomID - 房间不进会话列表、没有未读数,入口由业务页面带 roomID 进来
- 用
enterRoom一步建/进,别在客户端判断房间存不存在 - 进房成功的信号是
roomStateChanged的 Connected,不是 Promise resolve - 房间连接不会无限重连,必须自己兜重连,重连后补全量成员和属性
- 一定要
leaveRoom,否则人数虚高、消息白推、下个房间进不去 - 房间历史消息只有旗舰版有,聊天室不要做消息补偿
继续往下做:想给用户加角色、等级、麦位,用房间用户属性(setRoomMembersAttributes);想做禁言,把状态写进房间属性,客户端读属性判断能不能发;需要服务端管控(批量踢人、撤回房间消息)走 ZIM 服务端 API。群聊是另一套模型(conversationType = 2),不在本篇范围。
参考资料
本文来自作者投稿,版权归原作者所有。如需转载,请注明出处:https://www.nxrte.com/jishu/im/73194.html