首页
/ freeCodeCamp 高级 Node 与 Express 实战:用 req.user 与 Pug 渲染个性化的 /profile 页面

freeCodeCamp 高级 Node 与 Express 实战:用 req.user 与 Pug 渲染个性化的 /profile 页面

2026-09-04 23:45:58作者:咎竹峻Karen

本文基于 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 语法一次演示了三个知识点:

  1. h2.center#welcome —— Pug 的标签简写中,紧跟标签名的 .class 语法表示 class 属性,#id 语法表示 id 属性。展开后等价于 HTML 的 <h2 class="center" id="welcome">
  2. #{username} —— Pug 的变量插值语法(string interpolation),渲染时会被替换为 render 时传入的 data.username 值,即 req.user.username 的实际用户名。
  3. 缩进即结构 —— 该行必须与 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 的请求会经历如下链路(均基于课程文档中的代码拼合):

  1. Express 路由命中 /profile 的 GET 请求,先执行 ensureAuthenticated 中间件;
  2. 中间件调用 req.isAuthenticated()(Passport 判断 req.user 是否已定义),已认证则 next()
  3. 路由处理器执行 res.render('profile', { username: req.user.username })
  4. Pug 模板引擎读取 ./views/pug/profile.pug,把 #{username} 插值为真实用户名,把 h2.center#welcomea(href='/logout') 等简写展开为完整 HTML;
  5. 服务器将渲染后的静态 HTML 发送回浏览器,页面显示 Welcome, <用户名>! 和 Logout 链接。

关于挑战类型与提交方式的补充

该挑战 frontmatter 中声明了 challengeType: 2forumTopicId: 301554。结合仓库中的类型定义 challenge-types.ts,数值 2 对应 backend 类型,其视图(viewTypes)与提交方式(submitTypes)分别映射为 backendbackend——也就是说这类挑战在网站上以独立的后端代码编辑器呈现,提交时由测试沙箱拉取你的项目代码(如上文 /_api/server.js 所示的取码接口)来运行断言,而不是像前端挑战那样提交一个部署好的 URL。frontmatter 中各字段的作用也可从 课程挑战 schema 得到印证:idtitlechallengeTypeforumTopicIddashedName 都是 schema 校验的必备字段。

如果你希望本地完整跑通这个课程块,课程文档提示使用 freeCodeCamp 提供的 boilerplate-advancednode 脚手架仓库作为起点(课程内附有该仓库链接,含已预装 pug@~3.0.0 依赖的 package.json),或自行用网站内建的项目编辑器,把所有脚手架文件整合进项目后按块内顺序逐题推进即可。

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

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.12 K
2.72 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
528
588
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
906
1.83 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
854
1.34 K
docsdocs
暂无描述
Markdown
891
5.78 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.53 K
1.01 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.34 K
1.45 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
987
506
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
540
384