freeCodeCamp 高级 Node 与 Express 课程:用 Socket.IO 的 emit 机制实现在线用户计数通信
本文基于 freeCodeCamp 课程仓库中「Advanced Node and Express」项目块里的实战挑战 Communicate by Emitting 展开。该挑战是课程中 Socket.IO 章节的核心一步:在已搭好 Express + Passport 身份认证的聊天项目上,通过服务端 io.emit 与客户端 socket.on 双向通信,把"当前在线用户数"实时广播给所有连接的浏览器。读完本文,你将完整掌握 Socket.IO 事件广播的写法、计数逻辑的插入时机,以及课程测试框架如何逐条断言你的 server.js 与 client.js 实现。
一、挑战定位:它属于哪个项目、处于哪个环节
这道挑战在 advanced-node-and-express 块的挑战顺序定义 中对应 id 589fc831f9fc0f352b528e75。从该 JSON 的 challengeOrder 数组可以看到,它是整个 Node.js 综合项目(Authenticator 项目)倒数第几关里的 Socket.IO 环节,前后顺序为:
- Set up the Environment(id
589fc830f9fc0f352b528e74):引入socket.io、创建 http 服务器、监听connection事件、客户端io()连接; - Communicate by Emitting(本文主题,id
589fc831f9fc0f352b528e75):服务端广播事件、客户端监听事件; - Handle a Disconnect(id
589fc831f9fc0f352b528e76):处理断开连接时的计数回退; - Authentication with Socket.IO、Announce New Users、Send and Display Chat Messages(id
589fc831f9fc0f352b528e77~589fc832f9fc0f352b528e79):Socket.IO 鉴权与聊天消息收发。
挑战 front matter 中 challengeType: 2 表明这是一个"基于项目的挑战"(project-based challenge):不像普通题目只给一个编辑器,而是让你在完整的 server.js / routes.js / client.js / chat.pug 多文件项目上动手,由课程的测试运行器对运行中的项目发起请求来验证实现。
前一步 Set up the Environment 已经完成的铺垫(本文会用到):
- 服务端用 Node 内置的
http模块把 Express app 包装成 http server,并实例化 Socket.IO:
const http = require('http').createServer(app);
const io = require('socket.io')(http);
- 用
http.listen替代原来的app.listen; - 服务端监听连接事件,
socket参数代表每一个已连接的客户端:
io.on('connection', socket => {
console.log('A user has connected');
});
- 客户端
client.js(由认证后的/chat页面加载,Socket.IO 的 CDN 脚本已在chat.pug中引入)建立了连接:
/*global io*/
let socket = io();
理解这些铺垫后,本文的主题就是:连接建立之后,如何让服务端主动把数据推给所有客户端。
二、Emit 是什么:Socket.IO 最常用的通信方式
原文档给出的定义很直接:Emit(发出)是你最常用的通信方式。当你从服务端向 io 发出(emit)内容时,你实际上是向所有已连接的 socket 发送了一个事件名和对应的数据。这属于典型的"广播"(broadcast to all)语义——不需要指定某一个 socket,io.emit 会把事件送达全部在线客户端。
文档选用的教学例子正是本次挑战要实现的完整功能:每当有新用户连接进来,就把当前的在线用户数广播出去。这是一个最小但完整的实时广播闭环,之后聊天室的"新成员公告"、"消息推送"都建立在这个模式之上。
三、服务端实现:三步完成计数与广播
第 1 步:定义用户计数字段
在开始监听连接(io.on('connection', ...))的代码之前,先声明一个模块级变量来记录在线用户数:
let currentUsers = 0;
注意"just before where you are currently listening for connections"这一位置要求:变量必须声明在连接监听器外部,这样它才能跨多次连接持久存在,而不是每次连接都被重置。
第 2 步:在连接监听器内自增计数
当有人连接进来时,先自增计数,再广播。因此把自增语句放进 connection 监听器的回调内部:
++currentUsers;
"先加后发"的顺序是刻意的——文档原话是 "you should increment the count before emitting the count"。如果先发后加,新连接的用户收到的第一个数字就会比自己实际的在线人数少 1。
第 3 步:发出名为 'user count' 的事件
自增完成后(仍然在连接监听器内),发出事件。事件名固定为 'user count',携带的数据就是 currentUsers 本身:
io.emit('user count', currentUsers);
至此,完整的连接处理回调应当是:
io.on('connection', socket => {
++currentUsers;
io.emit('user count', currentUsers);
console.log('A user has connected');
});
三个关键 API 的分工可以概括为:
| API | 作用域 | 本文中的用途 |
|---|---|---|
io.on('connection', ...) |
整个 Socket.IO 实例(服务端) | 任何客户端连入时触发,socket 参数代表该客户端 |
io.emit('事件名', 数据) |
整个 Socket.IO 实例(服务端) | 把事件广播给所有已连接客户端 |
socket.on('事件名', 回调) |
单个客户端(服务端处理)/ 单个连接(客户端) | 监听某一具体事件,回调中拿到携带的数据 |
四、客户端实现:用 on 监听广播事件
服务端发出了事件,客户端要能收到。原文档强调:与在服务端监听连接类似,客户端同样使用 on 关键字。把以下代码加到 client.js 中:
socket.on('user count', function(data) {
console.log(data);
});
这里的 socket 就是前一步里 let socket = io(); 建立的连接对象;每当服务端执行一次 io.emit('user count', ...),这个回调就会以广播携带的数值作为 data 参数被调用。
预期运行结果
文档给出的验收方式:启动应用并完成 GitHub 认证进入 /chat 页面后,浏览器控制台应打印出 1,表示当前在线用户数为 1。再打开更多浏览器客户端并依次认证,会看到数字依次递增。这个"多客户端联动"的现象正是 io.emit 广播语义的直接体现——任何一个客户端连接,所有客户端都会收到更新后的计数。
五、课程如何验证你的实现:测试断言逐条解读
作为 challengeType: 2 的项目挑战,仓库中这道题自带的 # --hints-- 段就是测试运行器要执行的断言集。test-runner 脚本 会被注入到你的项目运行环境中执行这些测试。从 挑战源文件 可以看到三条断言,它们分别抓取自运行项目的 /_api/server.js 与 /public/client.js 两个端点的源码文本,用正则匹配关键写法:
断言 1:currentUsers 必须已定义(检查服务端)
const url = new URL("/_api/server.js", code);
const res = await fetch(url);
const data = await res.text();
assert.match(
data,
/currentUsers/s,
'You should have variable currentUsers defined'
);
它只要求 server.js 中出现 currentUsers 这个标识符,对应第 1 步的 let currentUsers = 0;。
断言 2:服务端必须在每次新连接时 emit 计数(检查服务端)
assert.match(
data,
/io.emit.*('|")user count('|").*currentUsers/s,
'You should emit "user count" with data currentUsers'
);
这条正则的 s 标志允许多行匹配,结构上要求:io.emit( 之后出现字符串 'user count'(单双引号均可),其后跟上 currentUsers。也就是说,事件名必须精确为 user count、数据必须恰好传 currentUsers 变量,写成 io.emit('user count', this.users) 之类就无法通过。
断言 3:客户端必须监听 'user count' 事件(检查客户端)
const url = new URL("/public/client.js", code);
const res = await fetch(url);
const data = await res.text();
assert.match(
data,
/socket.on.*('|")user count('|")/s,
'Your client should be connection to server with the connection defined as socket'
);
它检查 client.js 中存在 socket.on('user count', ...) 形式的监听(事件名同样要求精确为 user count),并且监听器要挂在名为 socket 的连接对象上——这呼应了前一步 let socket = io(); 的命名约定。
可以看出课程的验收策略:它不模拟真实浏览器连接,而是直接对你部署的 server.js / client.js 源码做正则级匹配,验证关键 API 调用(变量定义、io.emit 参数、socket.on 事件名)是否齐全且拼写正确。这也是学习时自查代码的最快方式——逐条对照正则,确认事件名 'user count' 的引号、大小写、变量名完全一致。
六、向前衔接:断开连接与后续挑战
本文的挑战只处理了"连接时计数 +1",在线人数只增不减。下一关 Handle a Disconnect 会补齐另一半:'disconnect' 事件不能像 'connection' 那样挂在 io 上监听,而必须挂到每个 socket 上(因为"断开"是某个具体客户端的行为)。它要求你在现有的 connection 监听器内部再加一个监听:
socket.on('disconnect', () => {
/*anything you want to do on disconnect*/
});
并在其中执行 --currentUsers 后重新 io.emit('user count', currentUsers),保证所有客户端持续拿到最新的在线数。该关的断言同样包含对 socket.on('disconnect', ...) 与客户端 user count 监听的正则检查,与本文的测试模式一脉相承。
再往后,Authentication with Socket.IO(id 589fc831f9fc0f352b528e77)会把 Passport 认证体系接入 Socket.IO 握手环节,Send and Display Chat Messages(id 589fc832f9fc0f352b528e79)则把本文学到的 emit/on 模式升级为点对点(向特定 socket 发送)与聊天消息渲染。可以说,本文这一个"在线用户数广播"的最小闭环,就是整个聊天室功能的通信骨架。
七、要点小结与延伸阅读
- 广播语义:
io.emit('事件名', 数据)向所有已连接 socket 发送事件;io是 Socket.IO 服务端实例,socket是单个连接对象,二者 API 对称(都有on/emit),作用域不同; - 状态放置:跨连接持久化的状态(如
currentUsers)声明在监听器外部,监听器内只负责读写; - 时序:先
++currentUsers后io.emit,保证新连接者收到的是含自己在内的正确计数; - 事件契约:服务端 emit 与客户端 on 的事件名必须严格一致(本例为
'user count'),这也是课程正则断言卡死的地方。
延伸阅读(均在当前仓库内,路径相对仓库根目录):
- 本篇挑战源文件(含完整描述与测试断言):communicate-by-emitting
- 前置挑战(Socket.IO 环境与连接监听):set-up-the-environment
- 后续挑战(断开连接处理):handle-a-disconnect
- 挑战顺序与所属项目块定义:advanced-node-and-express 块结构
- 项目挑战的测试运行器入口:test-runner
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0623
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00