简介这是一份面向前端开发者的网页即时聊天实战示例基于 LayIM 界面框架对接环信 WebIM适合想学习 PC 端聊天室搭建、消息收发与联系人列表渲染的初中级工程师参考。压缩包共 190 个文件约 1.7MB以 33 个 js 脚本、14 个 css 样式、6 个 html 页面为主另含 75 个 gif 与 37 个 png 图标素材、字体文件及少量 json、xml 配置结构完整可直接运行。资源已实现文字与表情发送、图片与文件发送入口、联系人列表等模块其中联系人接口使用作者自建的假数据替换为自己的后端接口即可接入环信部分需换成自己的 key且接口中必须以 id 作为用户名的 key。案例中仅张三与李四两个账号可互相通讯测试时选择这两个用户即可。目前已有 1529 人学习下载适合作为聊天功能原型或二次开发起点帮助读者快速理解 LayIM 与环信结合的目录组织与调用方式。1. 从零拆一个 layim环信 PC 端 WebIM 聊天 Demo它到底能解决什么如果你正在做一个后台管理系统、内部协作工具或者客服工作台绕不开的一个需求就是「把聊天功能嵌进 PC 网页里」。自己从零写 WebSocket 长连接、消息漫游、离线消息、已读回执工期至少按周算直接买一套成品 SDK 又担心被绑定。这套 layim环信 PC 端 WebIM 聊天 Demo 就是冲着这个场景来的——用 layui 生态里的 layim 做界面骨架用环信 Web SDK 做消息通道前端一把梭出一个能跑通的网页即时聊天原型。它适合三类人一是需要快速验证 IM 功能可行性的前端二是想拿一套可改的界面壳子做二次开发的人三是学生或转行者想搞懂「一个网页聊天室到底由哪些模块拼起来」。不适合指望开箱即用上生产的人——Demo 的定位就是骨架鉴权、敏感词、消息加密这些都得你自己补。下面我按「先跑起来、再拆结构、最后填坑」的顺序把这份资源从头到尾过一遍。2. 环境准备与 SDK 接入把环信 Web SDK 挂进 layim 页面2.1 先理清 layim 和环信各自负责什么很多人第一次接触这套组合会懵到底谁管界面、谁管消息说清楚分工后面调试才不会抓瞎。layim 是 layui 体系里的即时通讯 UI 模块它负责的是「长什么样」——主面板、好友列表、聊天窗口、消息气泡、右键菜单、表情面板这些 DOM 结构和交互它都给你了。它本身不带通信能力消息怎么发出去、怎么收回来它留了接口让你接。环信 Web SDK官方叫 easemob-websdk早期版本是 strophe.js 封装 XMPP负责的是「消息怎么走」——登录鉴权、建立长连接、收发文本/图片/文件消息、管理会话和好友关系。它不关心你界面长啥样只吐数据。所以这套 Demo 的本质是用 layim 的layim.config和layim.on事件钩子把环信 SDK 的回调数据喂进 layim 的渲染流程。理解这一层你就知道出问题时该往哪边查——界面不刷新找 layim消息收不到找环信。2.2 引入依赖与初始化配置先把两个库引进来。环信 SDK 建议用官方 CDN 或者下载到本地版本要和你的账号后台匹配别拿一个 3.x 的 SDK 去连 4.x 的服务端。!-- layui 全家桶layim 依赖它 -- link relstylesheet href./layui/css/layui.css script src./layui/layui.js/script !-- 环信 Web SDK注意版本要和后台一致 -- script src./sdk/easemob-websdk-4.x.min.js/script初始化分两步走顺序不能反先WebIM.conn建连接再layim.config配界面。因为 layim 的init里要拿环信返回的好友列表去渲染连接没建好列表就是空的。// 1. 环信连接初始化 const conn new WebIM.connection({ appKey: your_app_key, // 环信后台创建应用后拿到 isHttpDNS: true, // 开启 HTTP DNS减少连接失败 url: wss://im-api.easemob.com/ws, // 长连接地址按官方文档填 apiUrl: https://a1.easemob.com, // REST API 地址 isAutoLogin: false // Demo 里手动登录方便调试 }); // 2. layim 界面配置 layui.use([layim, layer], function () { const layim layui.layim; layim.config({ init: { // 这里的数据本该来自环信好友列表接口 mine: { id: user_001, username: 我, avatar: ./img/me.png }, friend: [] // 先留空登录后动态填 }, brief: false, // 不显示简略模式 min: false, // 不允许最小化 isAudio: true, // 开启声音提醒 isVideo: false, // Demo 先不开视频 right: 30px, // 面板距右 msgbox: ./msgbox.html // 消息盒子页面路径 }); });参数说明几个容易踩的appKey是环信后台应用的唯一标识格式类似orgName#appName填错会直接报 401isHttpDNS建议开移动网络下能明显降低连接超时isAutoLogin设 false 是为了让你手动控制登录时机方便在控制台看日志。layim.config里的init.friend先留空是故意的——真实好友数据要等环信登录成功后拉硬编码进去只会让你误以为通了。2.3 登录鉴权与好友列表拉取环信登录支持两种方式用 username/password 直接登录或者用 token 登录。Demo 里一般用前者方便演示生产环境建议后端签发 token 再传给前端避免密码暴露。// 用账号密码登录环信 conn.open({ user: user_001, pwd: your_password, // 登录成功回调 success: function () { console.log(环信登录成功); // 登录成功后拉取好友列表 conn.getRoster({ success: function (roster) { // roster 是好友数组转成 layim 需要的格式 const friends roster.map(item ({ id: item.name, username: item.name, avatar: ./img/default.png, sign: item.sign || })); // 把好友塞进 layim 的缓存并重新渲染 layim.addList({ type: friend, data: friends }); } }); }, // 登录失败回调 error: function (err) { console.error(登录失败, err); layer.msg(登录失败请检查账号或网络); } });这段逻辑的关键在于getRoster的回调里做数据格式转换。环信返回的roster每一项有name、subscription、groups等字段而 layim 要的是id、username、avatar。字段名对不上界面就是一片空白这是新手最常见的翻车点。layim.addList是动态追加列表的方法比重新init整个面板要轻量适合登录后异步加载好友的场景。3. 消息收发核心链路从 layim 事件到环信 send 的完整闭环3.1 发送消息监听 layim 的 sendMessage 事件layim 在用户点击发送按钮时会触发sendMessage事件把输入框里的内容、当前会话对象、消息类型都传给你。你要做的就是在这个回调里调环信的send方法。layim.on(sendMessage, function (res) { // res 结构{ mine: {...}, to: {...}, type: friend, content: 消息内容 } const { mine, to, type, content } res; // 构造环信消息体 const msg { id: conn.getUniqueId(), // 生成本地消息 ID用于去重 type: txt, // 消息类型txt/img/file targetId: to.id, // 接收方 ID chatType: type friend ? single : group, // 单聊还是群聊 msg: content, from: mine.id, ext: {} // 扩展字段可放头像、昵称 }; // 调用环信发送 conn.send(msg).then(() { console.log(消息发送成功); }).catch((err) { console.error(发送失败, err); layer.msg(消息发送失败); }); });逻辑说明conn.getUniqueId()生成的是一个本地唯一 ID环信服务端会返回真正的消息 ID两者要区分开本地 ID 用于界面上的消息去重和状态更新。chatType这个字段特别容易写错——单聊是single群聊是group写反了消息会发到错误的会话里而且不报错只是对方收不到排查起来很折磨。ext字段建议把发送者的头像和昵称塞进去因为环信默认只传 ID接收方渲染时拿不到头像就会显示默认图。3.2 接收消息用 onTextMessage 回调驱动 layim 渲染收消息比发消息要复杂一点因为要处理「当前窗口是否打开」「消息是否重复」这些情况。// 监听文本消息 conn.listen({ onTextMessage: function (message) { // message 结构{ from, to, msg, type, id, ext } const fromId message.from; const content message.msg; // 判断当前聊天窗口是否正对着这个发送者 const currentChat layim.getActiveChat(); // 伪代码实际用 layim 的缓存判断 if (currentChat currentChat.id fromId) { // 窗口已打开直接追加消息 layim.getMessage({ username: message.ext?.nickname || fromId, avatar: message.ext?.avatar || ./img/default.png, id: fromId, type: friend, content: content, timestamp: message.time || Date.now() }); } else { // 窗口没开更新未读数和消息盒子 layim.setChatMin(); // 最小化提示 // 同时把消息存进本地缓存等用户点开时再渲染 messageCache[fromId] messageCache[fromId] || []; messageCache[fromId].push(message); } }, // 其他类型消息 onImageMessage: function (message) { /* 图片消息处理 */ }, onFileMessage: function (message) { /* 文件消息处理 */ }, onError: function (err) { console.error(环信连接异常, err); } });这里有个关键判断当前会话窗口是否已经打开。如果用户正看着和 A 的聊天窗口A 发来消息直接layim.getMessage追加就行如果用户在看和 B 的窗口A 的消息就得走未读计数和消息盒子不能直接往当前窗口塞否则界面会串。messageCache是我自己加的一个本地缓存对象用来暂存未打开会话的消息用户点开时再批量渲染。环信本身有历史消息接口但每次点开都拉一次太慢本地缓存能明显提升体验。3.3 消息去重与本地 ID 管理WebSocket 长连接有个绕不开的问题网络抖动导致的重连可能让同一条消息被推送两次。如果不去重界面上就会出现两条一样的内容。// 已处理消息 ID 集合 const processedMsgIds new Set(); conn.listen({ onTextMessage: function (message) { // 用环信返回的消息 ID 去重 if (processedMsgIds.has(message.id)) { console.warn(重复消息已忽略, message.id); return; } processedMsgIds.add(message.id); // 集合太大时清理防止内存泄漏 if (processedMsgIds.size 1000) { const arr Array.from(processedMsgIds); processedMsgIds.clear(); arr.slice(-500).forEach(id processedMsgIds.add(id)); } // 后续渲染逻辑... } });Set的查找是 O(1)比数组includes快得多消息量大时差别明显。清理策略是保留最近 500 条 ID既控制了内存又覆盖了绝大多数重连场景。这个细节 Demo 里不一定有但你要上生产就必须加否则用户聊久了浏览器内存会一路涨。4. 避坑与常见问题排查那些让我熬夜的报错4.1 登录报 401 或 appKey 无效现象调conn.open后直接进 error 回调控制台提示 401 或appKey is invalid。原因九成是appKey格式不对或者和 SDK 版本不匹配。环信的 appKey 格式是orgName#appName中间那个#不能少也不能换成别的符号。另外 4.x SDK 连 3.x 的服务端地址也会报这个错。解决去环信后台确认应用详情里的 appKey 原文复制别手打检查 SDK 版本和后台应用创建时间是否对应老应用建议用老版本 SDK。4.2 好友列表拉回来但界面不显示现象getRoster的 success 回调里能打印出好友数组但 layim 面板上好友列表是空的。原因字段名没对上。环信返回的是namelayim 要的是id和username或者layim.addList的type传错了好友应该传friend传成group就渲染到群组栏去了。解决在getRoster回调里console.log一下原始数据逐字段对照 layim 文档的init.friend结构做映射。addList的 type 值只有friend和group两个别自己造。4.3 消息发出去了但对方收不到现象conn.send的 then 回调走了本地也显示发送成功但对方那边毫无反应。原因chatType写反了单聊写成了group消息被投递到群组通道或者targetId传的是用户名而不是环信 ID两者在环信体系里不是一回事。解决确认chatType单聊用single、群聊用grouptargetId必须用环信返回的用户 ID通常是roster里的name字段不要用你自己数据库里的用户 ID。4.4 重连后消息重复渲染现象网络断开重连后之前收到的消息又出现了一遍。原因环信在重连后会补推离线期间的消息如果本地没有去重机制就会重复渲染。解决按 3.3 节的方案用Set记录已处理的消息 ID收到消息先查再渲染。注意环信的消息 ID 在重推时是保持不变的所以用 ID 去重是可靠的。4.5 图片消息显示裂图现象文本消息正常图片消息发出去后对方看到的是裂图或者空白。原因环信图片消息传的是文件对象或 URLlayim 渲染时需要的是可直接访问的图片地址。如果传的是本地blob:地址对方当然访问不到。解决图片消息要先上传到环信的文件服务器用conn.uploadFile拿到返回的 URL 再构造消息体发送。别直接把input[typefile]拿到的本地路径塞进去。5. 进阶技巧把 Demo 改造成能用的工作台5.1 用 ext 字段携带业务数据环信消息体的ext字段是个被严重低估的能力。它是个自由对象你可以往里塞任何 JSON 可序列化的数据接收方在onTextMessage里能原样拿到。我一般会用它做两件事一是带发送者的头像和昵称省得接收方再去查用户信息二是带业务类型标记比如{ bizType: order, orderId: 12345 }这样聊天窗口里就能根据bizType渲染成订单卡片而不是普通文本。// 发送时携带业务数据 const msg { id: conn.getUniqueId(), type: txt, targetId: to.id, chatType: single, msg: 您有一笔新订单, ext: { avatar: mine.avatar, nickname: mine.username, bizType: order, orderId: ORD20250101 } }; // 接收时判断渲染方式 conn.listen({ onTextMessage: function (message) { if (message.ext?.bizType order) { // 渲染成订单卡片 renderOrderCard(message.ext.orderId, message.msg); } else { // 普通文本渲染 layim.getMessage({ /* ... */ }); } } });这个思路能让你在不改环信协议的前提下把 IM 通道变成业务消息总线。客服系统里的工单通知、协作工具里的任务提醒都可以走这条路。5.2 会话列表与未读数的本地维护layim 本身有会话列表但它的未读数逻辑比较简单。如果你要做「未读消息置顶」「免打扰会话不计数」这类功能得自己维护一份会话状态。字段类型说明conversationIdstring会话唯一标识单聊用对方 ID群聊用群 IDlastMessagestring最后一条消息内容用于列表预览unreadCountnumber未读数打开会话时清零updatedAtnumber最后更新时间戳用于排序mutedboolean是否免打扰true 时不增加未读数维护时机有三个收到新消息时更新lastMessage、updatedAt并递增unreadCount用户打开会话时把unreadCount清零用户主动删除会话时从列表移除。这份数据建议存localStorage刷新页面后还能恢复体验比每次重新拉取要好。5.3 断线重连的兜底策略环信 SDK 自带重连机制但默认的重连间隔和次数不一定适合你的场景。我一般会监听onDisconnected事件自己做一层兜底。let reconnectTimer null; conn.listen({ onDisconnected: function () { console.warn(连接断开准备重连); // 清除旧定时器防止叠加 if (reconnectTimer) clearTimeout(reconnectTimer); // 5 秒后尝试重连 reconnectTimer setTimeout(() { conn.reconnect({ success: () console.log(重连成功), error: (err) { console.error(重连失败稍后再试, err); // 递归重试间隔可以递增 } }); }, 5000); } });注意reconnect和open的区别reconnect用的是上次登录的凭证不需要重新传账号密码open是全新登录。断线场景用reconnect别用open否则会多一次鉴权开销。重连成功后记得把本地缓存里「发送中」状态的消息重新发一遍不然用户会以为消息丢了。从那以后我每次接 IM 需求都会先把「登录 → 拉好友 → 发消息 → 收消息 → 断线重连」这五步在控制台里手动走一遍确认每一步的日志都对得上再动界面。这套 Demo 的价值不在于它多完整而在于它把这五步的骨架给你搭好了你只需要往里面填业务逻辑。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?