)
freeCodeCamp 高级 Node.js 与 Express用 Socket.IO 广播用户上线/下线公告Announce New Users 挑战详解【免费下载链接】freeCodeCampfreeCodeCamp.orgs open-source codebase and curriculum. Learn math, programming, and computer science for free.项目地址: https://gitcode.com/GitHub_Trending/fr/freeCodeCamp本篇技术文章围绕 freeCodeCamp 课程「Advanced Node.js and Express」认证中编号为589fc832f9fc0f352b528e78的项目型挑战Announce New Users公告新用户的上线/下线展开。该挑战是聊天室项目序列中的关键一步在前序挑战中你已经实现了currentUsers计数与user count事件的收发、disconnect处理和 Socket.IO 的 Passport 会话鉴权本篇将把这两个独立的「用户数」事件合并升级为一个携带username、currentUsers、connected三个字段的user事件并在浏览器端用 jQuery 更新在线人数、追加「某某已进入/离开了聊天室」的公告。读完后你将掌握 Socket.IO 广播事件的数据设计方式、io.emit与socket.on的服务端/客户端配对关系以及 freeCodeCamp 平台如何用正则断言对server.js和client.js的代码做自动化验收。1. 挑战在聊天室项目中的位置该挑战位于 advanced-node-and-express 挑战顺序文件 的第 21 位共 21 个挑战处于四个连续聊天室挑战的中间环节。按 挑战结构 中的challengeOrder排列前后关系是Set up the Environment589fc830f9fc0f352b528e74在 Express 应用上挂载http.createServer(app)用require(socket.io)(http)实例化io监听io.on(connection, ...)客户端在client.js中let socket io();。Communicate by Emitting589fc831f9fc0f352b528e75定义let currentUsers 0;在 connect 监听器中currentUsers;后io.emit(user count, currentUsers);客户端用socket.on(user count, ...)接收。Handle a Disconnect589fc831f9fc0f352b528e76在 connect 监听器内部再挂socket.on(disconnect, ...)断开时把currentUsers减一并再次 emituser count。Authentication with Socket.IO589fc831f9fc0f352b528e77通过passport.socketiocookie-parserconnect-mongo解析express.sid会话 Cookie把用户对象反序列化后挂到socket.request.user上。Announce New Users本文档589fc832f9fc0f352b528e78合并并升级事件广播用户上线/下线公告。Send and Display Chat Messages589fc832f9fc0f352b528e79最终接入chat message事件的收发完成整个聊天室。理解这个序列很重要本挑战的三块原料——currentUsers来自挑战 2、disconnect监听来自挑战 3、socket.request.user.username来自挑战 4——都是前序挑战埋好的伏笔。若跳过前置步骤直接做本挑战socket.request.user会是undefined。2. 服务端把两个user count事件合并为一个user事件原始文档Announce New Users 挑战文件给出的核心要求是把事件名改为user随事件传递一个包含username、currentUsers、connected字段的对象连接时为true断开时用户为false。务必修改两处user count事件的发射点并把 disconnect 那处的connected设为false而不是像 connect 事件那样发送true。2.1 事件数据结构的三个字段为什么是这三块数据因为客户端要完成两件事显示当前在线人数、公告某个用户进入了或离开了聊天。把信息一次性打包进一个事件比让客户端自行拼接多个事件更可靠避免两次 emit 之间的竞态也少一个网络往返。三个字段的语义字段类型说明usernamestring发生连接/断开行为的用户名取自socket.request.user.usernamecurrentUsersnumber广播时刻的在线用户总数connect 时已自增、disconnect 时已自减后的值connectedbooleantrue表示该用户上线false表示下线2.2 完整的服务端改造结合前序挑战的铺垫本挑战完成后的server.js中 Socket.IO 部分应当长成这样currentUsers与 emit 的顺序、--currentUsers的位置均沿用前序挑战的写法// 依赖来自前序挑战http 服务、io 实例、passport.socketio 鉴权 let currentUsers 0; io.on(connection, socket { // 来自 Authentication with Socket.IO 挑战socket.request.user 已由 // passport.socketio 中间件从 express.sid cookie 反序列化得到 currentUsers; io.emit(user, { username: socket.request.user.username, currentUsers, connected: true }); // 来自 Handle a Disconnect 挑战disconnect 必须在 socket 上监听 socket.on(disconnect, () { --currentUsers; io.emit(user, { username: socket.request.user.username, currentUsers, connected: false }); }); });原始文档明确给出的最小核心代码片段是io.emit(user, { username: socket.request.user.username, currentUsers, connected: true });文档同时强调「Be sure to change bothuser countevents」——即 connect 处和 disconnect 处两处都要改且 disconnect 处的connected必须是false。这是本挑战最常见的扣分点只改了连接处、忘了断开处或者断开处照抄了true客户端就会把「离开」错误地显示成「进入」。2.3 关键细节username在 disconnect 时依然可读一个容易踩的坑是断开连接时socket.request.user还在吗从 Authentication with Socket.IO 挑战 的实现方式看passportSocketIo.authorize是在连接建立之前的中间件阶段完成会话解析并挂上socket.request.user的之后整个 socket 生命周期内包括触发disconnect时该对象都保持可读。因此 disconnect 分支里同样可以用socket.request.user.username报出是哪个用户离开。3. 客户端监听user事件并用 jQuery 更新页面文档原文给出的客户端实现如下这里完整保留并补充说明socket.on(user, data { $(#num-users).text(data.currentUsers users online); let message data.username (data.connected ? has joined the chat. : has left the chat.); $(#messages).append($(li).html(b message /b)); });文档对页面行为的描述是用 jQuery 把#num-users的文本更新为{NUMBER} users online并向id为messages的无序列表追加一个li内容为{NAME} has {joined/left} the chat.。逐行拆解socket.on(user, data {...})客户端监听的事件名必须与服务端 emit 的user完全一致data就是服务端传出的那个三字段对象。$(#num-users).text(data.currentUsers users online)直接以服务端下发的计数为准覆盖文本不做本地累加保证多客户端之间始终与服务端单一事实源server 端的currentUsers一致。三元运算符data.connected ? has joined the chat. : has left the chat.是connected布尔字段的消费方式——字段值在这里转化为「joined/left」两种文案。$(#messages).append($(li).html(b message /b))公告消息以加粗的列表项追加进消息区与后续挑战Send and Display Chat Messages中真正的聊天消息共用同一个#messages列表。注意此时user count事件的旧监听器应当被本监听器取代——事件名已改名旧监听器即使保留也不会再收到数据但它已无意义清理掉是合理的。4. 自动化验收平台如何用测试断言检查你的代码freeCodeCamp 的challengeType: 2项目型挑战不是靠快照对比而是把你在平台编辑器里保存的代码当作真实运行的服务由测试代码向/_api/server.js与/public/client.js发请求取回源码文本再用正则做结构性断言。这一点从 Announce New Users 挑战文件 内嵌的--hints--测试代码可以直接读出来完整继承如下断言一服务端必须 emit 携带三个字段user事件。const url new URL(/_api/server.js, code); const res await fetch(url); const data await res.text(); // Regex is lenient to match both username and name as the key on purpose. assert.match( data, /io.emit.*(|)user\1.*name.*currentUsers.*connected/s, You should have an event emitted named user sending name, currentUsers, and connected );注意正则里\1反向引用保证事件名的引号成对且字段顺序被刻意放宽只要nameusername或name均可出现在currentUsers之前、currentUsers出现在connected之前即可。注释也说明了这是有意为之——字段键名name与username都能通过。断言二客户端必须在user监听器里更新#num-users文本。const url new URL(/public/client.js, code); const res await fetch(url); const data await res.text(); assert.match( data, /socket.on.*(|)user\1[^]*num-users/s, You should change the text of #num-users within on your client within the user event listener to show the current users connected );断言三客户端必须在同一监听器里向#messages追加li。assert.match( data, /socket.on.*(|)user\1[^]*messages.*li/s, You should append a list item to #messages on your client within the user event listener to announce a user came or went );这三条断言解释了为什么服务端必须 emit 到io而不是某个单个socketio.emit才是广播到全部已连接 socket 的语义。它同时也解释了第 2 节里为什么 disconnect 处一定要补一条 emit——虽然断言本身只检查io.emit.*user是否存在但「两处都改」是功能正确性的要求只改一处会导致在线人数与公告文案错乱。从源码结构看这套new URL(/_api/server.js, code)fetchassert.match的测试写法是该 block 所有挑战共享的项目测试范式例如 Communicate by Emitting 与 Handle a Disconnect 中的断言结构与之完全一致客户端渲染入口则涉及 independent-lower-jaw 组件它负责在挑战页面中呈现这类项目型挑战的编辑器与测试输出区域。5. 常见错误与自检清单结合文档要求与断言逻辑提交前可以对照检查两处 emit 是否都改了connect 与 disconnect 分支都必须 emituser且connected分别为true/false。漏改 disconnect 处不会让断言一失败正则只要求存在一条io.emit.*user但功能上是错的——用户离开时在线人数不再更新。currentUsers的自增/自减位置connect 处必须「先自增、后广播」disconnect 处必须「先自减、后广播」否则广播出去的是旧值。客户端事件名拼写服务端 emit 的是user客户端socket.on的必须是同一字符串引号风格不限正则(|)user\1兼容单双引号。#num-users文本格式断言二要求监听器内出现num-users功能上文本应为{NUMBER} users onlinecurrentUsers为数字而非字符串拼接产物。公告列表项必须以li追加到#messages文案区分 joined / left取决于connected字段。鉴权前置若尚未完成 Authentication with Socket.IO 挑战socket.request.user不存在socket.request.user.username会直接抛 TypeError连接建立即断开形成无限重连。文档末尾提示若运行中遇到错误可参照论坛中「项目完成到这一步」的参考进度原文指向 freeCodeCamp 论坛的 advanced-node-and-express 帖子锚点#announce-new-users-10。6. 延伸这套事件模式为后续挑战铺了什么路本挑战确立的「服务端广播结构化事件 客户端单一监听器更新多处 DOM」模式会被下一个挑战 Send and Display Chat Messages 直接复用客户端在表单提交时socket.emit(chat message, messageToSend)服务端监听后io.emit(chat message, { username, message })客户端再把消息追加进同一个#messages列表。也就是说第 2、3 节中你写好的#messages列表此时已经同时承载两类内容——加粗的进出公告和正常的聊天消息这正是完整聊天室的最终形态。对照 挑战顺序文件 可知这是「Advanced Node.js and Express」认证最后一组实操挑战完成它之后整个从 Express 路由、Passport 鉴权、express-session会话到 Socket.IO 实时通信的完整后端应用链路即告收尾。适用前提与范围说明本文基于 freeCodeCamp 课程仓库中的英文挑战文档challengeType: 2项目型挑战撰写所涉代码运行在 freeCodeCamp 平台提供的项目沙箱中依赖socket.io~2.3.0、passport.socketio~3.7.0、connect-mongo~3.2.0、cookie-parser~1.4.5已由平台预置为依赖。文中所有代码示例、断言正则与文件路径均取自当前仓库实际内容对未直接展示的实现细节均已用「从源码结构看」「可以推断」等措辞标注。【免费下载链接】freeCodeCampfreeCodeCamp.orgs open-source codebase and curriculum. Learn math, programming, and computer science for free.项目地址: https://gitcode.com/GitHub_Trending/fr/freeCodeCamp创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考