freeCodeCamp 高级 Node 与 Express 实战:用 req.user 与 Pug 渲染个性化的 /profile 页面
本文基于 freeCodeCamp 课程库(curriculum)中 Advanced Node and Express 认证课程块的挑战文档 How to Put a Profile Together,讲解在已接入 Passport 认证与自定义中间件的项目中,如何把 req.user 中的用户名传入 Pug 模板,渲染出带问候语和个人主页内容的 /profile 页面,并预留登出入口。读完本文,你可以掌握 Express res.render 的第二个参数传参方式、Pug 中类名/ID 与变量插值的书写规则,以及该挑战的自动化测试是如何校验你的 server.js 源码的。
挑战在课程块中的位置与前置条件
这个挑战并非孤立存在。从 课程块结构文件 可以看到,advanced-node-and-express 块共包含 22 个挑战,本挑战(id 为 5895f70ef9fc0f352b528e6b)排在第 9 位,前一个挑战是 "Create New Middleware",后一个挑战是 "Logging a User Out"。这意味着进入本挑战时,学习者应当已经完成了:
- 用
app.set('view engine', 'pug')与app.set('views', './views/pug')配置 Pug 模板引擎,并在首页路由中调用res.render('index')(对应 Set up a Template Engine 挑战); - 配置 Passport 策略、用户对象序列化(serialize/deserialize),使已认证请求上挂载
req.user对象; - 实现并挂载认证守卫中间件
ensureAuthenticated(对应 Create New Middleware 挑战)。
其中前置中间件的实现是本挑战的前提:它调用 Passport 提供的 req.isAuthenticated() 判断 req.user 是否已定义,未登录则重定向到首页,已登录则调用 next() 放行,并作为中间件参数挂在 /profile 路由的 GET 请求处理器之前:
function ensureAuthenticated(req, res, next) {
if (req.isAuthenticated()) {
return next();
}
res.redirect('/');
}
app
.route('/profile')
.get(ensureAuthenticated, (req, res) => {
res.render('profile');
});
本挑战正是在 res.render('profile') 这一步上做增强:让模板页面拿到真实用户名。
第一步:把 req.user.username 传给 render 方法
文档原话是:在确认访问 /profile 的用户已通过认证之后,你就可以使用 req.user 中包含的信息了。具体做法是,向 render 方法传入一个包含属性 username(值为 req.user.username)的对象,作为第二个参数:
res.render('profile', { username: req.user.username });
从 Express 的机制看,res.render(viewName, data) 的第二个参数 data 会被合并进模板的局部变量作用域——在 Pug 中你可以直接以 #{username} 的形式引用它,而不需要再发一次 API 请求去客户端取数。这也是模板引擎相比"纯客户端渲染"的价值所在:服务器端一次渲染即输出含真实数据的 HTML。
注意这个挑战的测试方式值得留意:它不是发请求看渲染结果,而是直接抓取你的 server.js 源码做静态匹配。hints 部分给出的断言是:
const url = new URL("/_api/server.js", code);
const res = await fetch(url);
const data = await res.text();
assert.match(
data,
/username:( |)req.user.username/,
'You should be passing the variable username with req.user.username into the render function of the profile page'
);
正则 /username:( |)req.user.username/ 中 ( |) 是一个可选空格的分组,也就是说 username: req.user.username(冒号后带空格)和 username:req.user.username(不带空格)都能通过。这里隐含了一个要求:变量名必须恰好是 username,且值必须恰好是 req.user.username,写成 req.user.name 或其他别名都无法匹配。
第二步:在 profile.pug 中书写 Pug 模板变量
接着到 views/pug 目录下的 profile.pug 视图,在现有 h1 元素下方、保持相同缩进层级,添加文档给出的这一行:
h2.center#welcome Welcome, #{username}!
这行 Pug 语法一次演示了三个知识点:
h2.center#welcome—— Pug 的标签简写中,紧跟标签名的.class语法表示 class 属性,#id语法表示 id 属性。展开后等价于 HTML 的<h2 class="center" id="welcome">。#{username}—— Pug 的变量插值语法(string interpolation),渲染时会被替换为render时传入的data.username值,即req.user.username的实际用户名。- 缩进即结构 —— 该行必须与
h1同级缩进,才能成为h1的兄弟元素而不是其子元素。Pug 对缩进敏感,缩进错层是新手最常见的报错来源。
文档同时说明了渲染结果:生成一个带 center 类和 welcome id 的 h2 元素,文本内容为 Welcome, 后跟用户名。给元素赋 id="welcome" 的意义在于它成为页面上可定位的钩子——后续课程中的测试断言、样式选择器都可以基于该 id 精确匹配这一行欢迎语。
第三步:为页面添加登出链接
文档要求在 profile.pug 中再添加一个指向 /logout 路由的链接,该路由将承载"取消用户认证"的逻辑:
a(href='/logout') Logout
同样的 Pug 简写语法:a 是 <a> 标签的缩写,括号内是属性,href='/logout' 声明跳转地址,行尾的 Logout 是元素的文本内容。此时点击该链接只会得到一个 404(因为 /logout 路由尚未实现),这正是下一课 Logging a User Out 要补上的部分——该挑战给出的实现是在 /logout 路由中调用 req.logout() 取消认证后重定向回首页:
app.route('/logout')
.get((req, res) => {
req.logout();
res.redirect('/');
});
两个挑战前后衔接:本课负责在页面上"预留"登出入口,下一课负责让入口真正工作。这也是该课程块刻意的设计——每个挑战只推进一个最小增量,保证每步都能被独立测试验证。
数据流回顾:一次 /profile 请求的完整路径
把前后几个挑战串起来,一次已登录用户访问 /profile 的请求会经历如下链路(均基于课程文档中的代码拼合):
- Express 路由命中
/profile的 GET 请求,先执行ensureAuthenticated中间件; - 中间件调用
req.isAuthenticated()(Passport 判断req.user是否已定义),已认证则next(); - 路由处理器执行
res.render('profile', { username: req.user.username }); - Pug 模板引擎读取
./views/pug/profile.pug,把#{username}插值为真实用户名,把h2.center#welcome、a(href='/logout')等简写展开为完整 HTML; - 服务器将渲染后的静态 HTML 发送回浏览器,页面显示
Welcome, <用户名>!和 Logout 链接。
关于挑战类型与提交方式的补充
该挑战 frontmatter 中声明了 challengeType: 2 与 forumTopicId: 301554。结合仓库中的类型定义 challenge-types.ts,数值 2 对应 backend 类型,其视图(viewTypes)与提交方式(submitTypes)分别映射为 backend 和 backend——也就是说这类挑战在网站上以独立的后端代码编辑器呈现,提交时由测试沙箱拉取你的项目代码(如上文 /_api/server.js 所示的取码接口)来运行断言,而不是像前端挑战那样提交一个部署好的 URL。frontmatter 中各字段的作用也可从 课程挑战 schema 得到印证:id、title、challengeType、forumTopicId、dashedName 都是 schema 校验的必备字段。
如果你希望本地完整跑通这个课程块,课程文档提示使用 freeCodeCamp 提供的 boilerplate-advancednode 脚手架仓库作为起点(课程内附有该仓库链接,含已预装 pug@~3.0.0 依赖的 package.json),或自行用网站内建的项目编辑器,把所有脚手架文件整合进项目后按块内顺序逐题推进即可。
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