首页
/ freeCodeCamp 高级 Node 与 Express 课程:用 Socket.IO 的 emit 机制实现在线用户计数通信

freeCodeCamp 高级 Node 与 Express 课程:用 Socket.IO 的 emit 机制实现在线用户计数通信

2026-09-05 13:12:34作者:昌雅子Ethen

本文基于 freeCodeCamp 课程仓库中「Advanced Node and Express」项目块里的实战挑战 Communicate by Emitting 展开。该挑战是课程中 Socket.IO 章节的核心一步:在已搭好 Express + Passport 身份认证的聊天项目上,通过服务端 io.emit 与客户端 socket.on 双向通信,把"当前在线用户数"实时广播给所有连接的浏览器。读完本文,你将完整掌握 Socket.IO 事件广播的写法、计数逻辑的插入时机,以及课程测试框架如何逐条断言你的 server.jsclient.js 实现。

一、挑战定位:它属于哪个项目、处于哪个环节

这道挑战在 advanced-node-and-express 块的挑战顺序定义 中对应 id 589fc831f9fc0f352b528e75。从该 JSON 的 challengeOrder 数组可以看到,它是整个 Node.js 综合项目(Authenticator 项目)倒数第几关里的 Socket.IO 环节,前后顺序为:

  1. Set up the Environment(id 589fc830f9fc0f352b528e74):引入 socket.io、创建 http 服务器、监听 connection 事件、客户端 io() 连接;
  2. Communicate by Emitting(本文主题,id 589fc831f9fc0f352b528e75):服务端广播事件、客户端监听事件;
  3. Handle a Disconnect(id 589fc831f9fc0f352b528e76):处理断开连接时的计数回退;
  4. Authentication with Socket.IOAnnounce New UsersSend 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)声明在监听器外部,监听器内只负责读写;
  • 时序:先 ++currentUsersio.emit,保证新连接者收到的是含自己在内的正确计数;
  • 事件契约:服务端 emit 与客户端 on 的事件名必须严格一致(本例为 'user count'),这也是课程正则断言卡死的地方。

延伸阅读(均在当前仓库内,路径相对仓库根目录):

登录后查看全文
热门项目推荐
相关项目推荐