首页
/ freeCodeCamp Advanced Node and Express 项目:Socket.IO 环境搭建与实时通信入门(Set up the Environment 挑战详解)

freeCodeCamp Advanced Node and Express 项目:Socket.IO 环境搭建与实时通信入门(Set up the Environment 挑战详解)

2026-09-04 10:05:12作者:滕妙奇

本篇基于 freeCodeCamp 课程库中「Advanced Node and Express」项目里的 Set up the Environment 挑战文档展开。读完后,你不仅会掌握将 Socket.IO 挂接到已鉴权的 Express 应用(GitHub 社交登录 + Passport)上的完整步骤,还能理解该挑战在课程结构中的定位、challengeType: 2(backend)挑战的测试判定机制,以及五条自动校验规则的具体实现。

挑战在课程中的定位

该挑战文档位于 advanced-node-and-express 挑战目录,是「Advanced Node and Express」这个大型后端项目的第 18 个挑战,也是该项目中 Socket.IO 实时通信阶段的起点。从 block 结构文件challengeOrder 可以确认它的上下文位置:

  • 前置挑战:Implementation of Social Authentication III(实现 GitHub 社交登录,对应 id 589a8eb3f9fc0f352b528e72)——也就是说,开始本挑战时你的应用已经具备 Passport 的 GitHub 策略、ensureAuthenticated 中间件和 GitHub 回调路由;
  • 后继挑战依次为:Communicate by EmittingHandle a DisconnectAuthentication with Socket.IOAnnounce New UsersSend and Display Chat Messages。本挑战搭建的 /chat 页面与 socket.io 环境正是这五个挑战共同依赖的基座。

整个 block 归属于 Quality Assurance 超级模块advanced-node-and-express 是其三个 block 之一)。此外,该挑战 front matter 中声明 challengeType: 2。对照课程共享包中的类型定义 challenge-types.ts2backend 类型,其 viewTypes 映射为 'backend'submitTypes 映射为 'backend'——这意味着该挑战在学员端以后端项目视图呈现,提交后测试代码会直接访问项目内文件(/_api/server.js 等)做正则断言,而不是在浏览器中运行前端断言。

第一步:添加 /chat 路由并改造 GitHub 回调

挑战文档给出的原始要求是:后续挑战都会用到 chat.pug 文件。因此需要在 routes.js 中添加一条指向 /chat 的 GET 路由,该路由使用 ensureAuthenticated 中间件,并渲染 chat.pug,同时把 { user: req.user } 作为响应参数传入模板:

router.get('/chat', ensureAuthenticated, (req, res) => {
  res.render('chat', { user: req.user });
});

接着修改已有的 /auth/github/callback 路由:设置 req.session.user_id = req.user.id,并重定向到 /chat。这样登录完成后用户会自动进入聊天页,且会话中保存了数据库用户 id,为后续「Socket.IO 鉴权」挑战做铺垫。

第二步:用 http.createServer 挂载 Express 并实例化 Socket.IO

文档说明 socket.io@~2.3.0 已作为依赖添加,需要在服务端按如下方式引入——注意 Socket.IO 的服务器端必须绑定在一个 原生 http 服务器上,而不是直接挂在 Express 应用上:

const http = require('http').createServer(app);
const io = require('socket.io')(http);

这里有两点值得展开:

  1. http.createServer(app) 将 Express 应用作为请求处理器挂到 Node.js 内置 http 模块创建的服务器实例上,使 Express 继续处理普通 HTTP 请求;
  2. require('socket.io')(http) 返回的 io 实例则附加在同一 http 服务器上,负责 WebSocket 长连接的管理。两者共用同一个端口和同一个 http 实例,因此必须把原来 app.listen 那一行改为 http.listen——因为 Express 本身不直接监听端口,真正的监听者变成了包裹了它的 http 服务器。

第三步:监听连接事件 io.on('connection', ...)

Socket.IO 基于事件模型:on 关键字用于监听特定事件,它接收两个参数——事件名(字符串)和事件触发时执行的处理函数。对「新连接」这一事件而言,第二个参数约定命名为 socket,它代表一个独立已连接的客户端:

io.on('connection', socket => {
  console.log('A user has connected');
});

文档要求在数据库连接建立之后加入这段监听代码。加载应用并完成鉴权登录后,服务端控制台应打印 A user has connected,这是本阶段的第一次运行验证。

第四步:客户端连接

对客户端而言,连接代码只有两行,添加到鉴权页面加载的 client.js 中:

/*global io*/
let socket = io();
  • /*global io*/ 是 JSHint 风格的注释声明,用于抑制「io 未定义」的 lint 报错——因为 io 全局对象由 chat.pug 页面中通过可靠 CDN 引入的 Socket.IO 客户端库提供;
  • 无参调用 io() 只适用于连接同 URL/同服务器上托管的 Socket.IO 服务;若要连接部署在其他位置的 Socket.IO 服务器,则需显式写出地址:io.connect('URL');

自动校验规则:五条 Hint 测试逐条解析

该挑战属于 backend 类型,提交后测试代码会通过 fetch 读取项目文件并做正则断言。文档中定义的 # --hints-- 部分包含 5 条断言,它们精确刻画了本挑战的「正确实现」边界:

1. socket.io 必须出现在依赖中——测试读取 /_api/package.json

  const url = new URL("/_api/package.json", code);
  const res = await fetch(url);
  const packJson = await res.json();
  assert.property(
    packJson.dependencies,
    'socket.io',
    'Your project should list "socket.io" as a dependency'
  );

2. 必须正确 require 内置 http 模块——读取 /_api/server.js,用成对引号反向引用 \1 保证 require 字符串与变量一致:

  const url = new URL("/_api/server.js", code);
  const res = await fetch(url);
  const data = await res.text();
  assert.match(
    data,
    /http.*=.*require.*('|")http\1/s,
    'Your project should list "http" as a dependency'
  );

3. socket.io 必须以 http 为参数实例化为 io——正则要求 iorequire('socket.io')http 三者按序出现:

  assert.match(
    data,
    /io.*=.*require.*('|")socket.io\1.*http/s,
    'You should correctly require and instantiate socket.io as io.'
  );

4. 服务端必须监听 connection 事件且第二参变量名为 socket

  assert.match(
    data,
    /io.on.*('|")connection\1.*socket/s,
    'io should listen for "connection" and socket should be the 2nd arguments variable'
  );

5. 客户端必须以 socket 为变量名完成连接——读取 /_public/client.js

  const url = new URL("/public/client.js", code);
  const res = await fetch(url);
  const data = await res.text();
  assert.match(
    data,
    /socket.*=.*io/s,
    'Your client should be connection to server with the connection defined as socket'
  );

从这些断言可以看出课程设计的约束粒度:不仅要求功能可用,还要求变量命名(httpiosocket)与官方教程一致,便于后续挑战的代码模式保持一致。

机制溯源:挑战文档如何变成可运行的课程数据

从仓库工程角度看,这类挑战是「Markdown 课程文档 + 结构化 front matter」的产物:front matter 提供 idtitlechallengeTypeforumTopicIddashedName 等元数据,正文中的 # --description-- 段落作为学员看到的题目描述,# --hints-- 段落按「一句提示语 + 一个 js 代码块」成对出现,即测试断言。解析入口位于 challenge-parser 解析器,而字段的合法性则由 challenge-schema.js 以 Joi 校验。dashedName(本文档为 set-up-the-environment)用于生成 URL 友好的锚点,例如后续挑战中「查看已完成项目」的论坛链接即按 #set-up-the-environment 定位讨论帖。

下一步:从「能连上」到「能通信」

本挑战的终点是控制台打印 A user has connected。紧接着的下一挑战 Communicate by Emitting 将在本挑战搭好的 connection 监听器基础上引入 io.emit('user count', currentUsers) 广播用户数事件,客户端则用 socket.on('user count', ...) 接收——即从「建立连接」推进到「双向事件通信」,并最终串联出完整的实时聊天室功能。

小结

  • /chat 路由 + ensureAuthenticated + chat.pug 渲染,是 Socket.IO 阶段的页面基座;
  • GitHub 回调中写入 req.session.user_id 并重定向 /chat,为后续 Socket 层鉴权埋下伏笔;
  • http.createServer(app) + require('socket.io')(http) + http.listen 是 Express 应用接入 Socket.IO 的标准三步;
  • io.on('connection', socket => ...) 与服务端 let socket = io() 构成最小可用的连接闭环;
  • 五条 hint 测试以正则断言锁定了依赖声明、变量命名与事件监听的实现细节,这也是 freeCodeCamp backend 类挑战(challengeType: 2)的典型验收方式。
登录后查看全文
热门项目推荐
相关项目推荐