freeCodeCamp 高级 Node 与 Express 实战:用模板引擎(Pug)把服务端变量渲染到页面
本文以 freeCodeCamp 课程「Advanced Node and Express(高级 Node 与 Express)」模块中的项目型挑战 Use a Template Engine's Powers 为主体,完整讲解如何在 Express 中通过 res.render 将服务端变量(title、message)传递给 Pug 模板并完成渲染。读完后,你将掌握 Pug 中变量插值的两种写法、Pug 的缩进语法与条件分支,以及该项目挑战的自动测试是如何校验你的页面输出,从而独立完成这一经典的教学项目。
挑战在 freeCodeCamp 课程体系中的位置
该挑战定义于 5895f70bf9fc0f352b528e64.md,其 frontmatter 声明了 challengeType: 2,即项目型(Project)挑战——你需要在一个真实可运行的项目代码库中动手完成功能,而不是只回答问答。项目型挑战的类型枚举定义见 challenge-types.ts。
从课程结构文件 advanced-node-and-express.json 可以看到,该挑战是 advanced-node-and-express 块中的第二道题(challengeOrder 数组第二项),紧跟在 Set up a Template Engine 之后;而该块整体隶属于 quality-assurance 超级块(见 quality-assurance.json),与 Quality Assurance and Testing with Chai、Quality Assurance Projects 并列。这一安排说明:模板引擎是本块的第一块基石,后续所有关于 Passport 认证、用户序列化、社交认证等内容都建立在此之上。
挑战文档说明,你需要通过以下方式之一完成代码:
- 克隆配套的项目模板仓库
boilerplate-advancednode(freeCodeCamp 提供的 GitHub 仓库),在本地完成这些挑战; - 或使用你偏好的在线站点构建器,但务必纳入模板仓库中的全部文件。
回顾前置知识:模板引擎的作用与配置
在讲“传变量”之前,需要回顾上一题 Set up a Template Engine 中已经完成的三件事,它们是本题渲染生效的前提:
pug@~3.0.0已安装并写入项目的package.json依赖;- 通过 Express 的
set方法告知应用使用的视图引擎:
app.set('view engine', 'pug');
- 将
views指向 Pug 模板目录:
app.set('views', './views/pug');
模板引擎允许你在应用中直接使用静态模板文件(例如 Pug 编写)。运行时,模板引擎会把模板文件中的变量替换为服务端提供的真实值,再把模板转换成静态 HTML 发送给客户端。 这样做的好处是:HTML 页面的设计更简单,且无需客户端再发起 API 调用就能展示变量数据。
上一题的测试用例会通过 /_api/app 接口断言 app.settings['view engine'] 为 "pug"、app.settings.views 为 "./views/pug",并请求首页断言渲染出的 HTML 包含 FCC Advanced Node and Express 与 pug-success-message——这些配置若未正确完成,本题的变量渲染也就无从谈起。
Pug 中引用变量的两种写法
挑战文档的核心知识点是:模板引擎最强大的特性之一,就是允许在把模板渲染成 HTML 之前,把变量从服务端传入模板文件。 在 Pug 文件中有两种等价的引用方式:
方式一:内联插值 #{variable_name}
在元素内的文本中,用 #{variable_name} 的形式内联引用变量,可以与其他文本混排:
p Hello, #{name}!
方式二:元素级赋值 p=variable_name
在元素名后紧跟不带空格的等号,把变量的值直接赋给该元素的文本内容:
p=variable_name
注意这里的等号与元素名之间不能有空格,这是 Pug 语法的硬性要求:p = variable 会被解析为带 = 属性的元素,而 p=variable 才是内容赋值。
文档同时强调了 Pug 的哲学:用空白和制表符(缩进)来表达嵌套关系,从而大幅减少写出美观站点所需的代码量。 这正是 Pug 区别于传统 HTML 的关键——没有闭合标签,层级完全由缩进决定。
官方示例逐行解析:脚本内联与 if/else 分支
挑战文档给出了一段完整的 Pug 示例,它同时演示了两个高频场景:script 标签后跟 . 的内联文本块,以及基于服务端变量的 if/else 条件渲染。
head
script(type='text/javascript').
if (foo) bar(1 + 5);
body
if youAreUsingPug
p You are amazing
else
p Get on it!
对应的渲染结果为:
<head>
<script type="text/javascript">
if (foo) bar(1 + 5);
</script>
</head>
<body>
<p>You are amazing</p>
</body>
从源码结构看这段示例的几个要点:
script(type='text/javascript').末尾的点号表示其后缩进的内容是原样保留的文本块(literal block)。这样if (foo) bar(1 + 5);不会被当成 Pug 的控制流,而是作为 JavaScript 源码原封不动地输出到<script>中;if youAreUsingPug之后的p You are amazing/else分支是 Pug 模板自身的条件逻辑,它读取的是服务端传入的变量youAreUsingPug,渲染时只有真值分支会被输出;- 示例 HTML 只出现
<p>You are amazing</p>,说明youAreUsingPug为真。这恰好印证了主题:分支走向由服务端注入的变量决定,模板本身不包含任何客户端请求逻辑。
实战步骤:把 title 和 message 传入 index.pug
你的项目模板中已经包含 index.pug 文件,其中使用到了两个变量:title 和 message。要让它们被渲染,需要在 Express 首页路由中调用 res.render,并把一个包含变量键值对的对象作为第二个参数传入。具体要求:
title赋值为Hello;message赋值为Please log in。
即:
res.render('index', { title: 'Hello', message: 'Please log in' });
参数说明:
| 参数 | 含义 | 本例取值 |
|---|---|---|
| 第一个参数 | 模板名(不含扩展名),相对于 views 目录(./views/pug) |
'index' → 渲染 views/pug/index.pug |
| 第二个参数 | 传入模板的变量对象,键名即模板中可引用的变量名 | { title: 'Hello', message: 'Please log in' } |
刷新页面后,你就应看到这些值按 index.pug 中的布局渲染到对应位置。挑战文档还提示:若你在本地遇到报错,可对照论坛中“已完成到这一步的项目”自查(原文档提供了论坛链接,本文按仓库引用规范不再列出)。
自动测试如何验证你的实现
挑战文档的 --hints-- 部分给出了自动测试的核心断言,它能帮我们精确理解“做对了”的判据:
const url = new URL("/", code);
const res = await fetch(url);
const data = await res.text();
assert.match(
data,
/pug-variable("|')>Please log in/gi,
'Your projects home page should now be rendered by pug with the projects .pug file unaltered'
);
解读这段测试:
- 测试直接向运行中应用的根路径
/发起fetch请求,取得返回的 HTML 文本——这验证的是服务端渲染的最终产物,而不是你的模板文件内容; - 正则
/pug-variable("|')>Please log in/gi匹配pug-variable="Please log in或pug-variable='Please log in这类写法,即断言message的值Please log in出现在一个名为pug-variable的元素属性中,且引号形式不限、大小写不敏感(gi标志); - 断言失败信息中的“the projects .pug file unaltered”表明:你不能靠修改
index.pug里写死文本来作弊,模板文件应保持原样,值必须来自res.render传入的第二个参数对象。
配合上一题的测试(断言首页包含 FCC Advanced Node and Express 与 pug-success-message,以及通过 /_api/app 校验 view engine 与 views 设置),整个模板引擎相关链路——依赖声明、应用设置、渲染调用、变量注入——都被逐层覆盖。
小结
围绕 Use a Template Engine's Powers 挑战文档 的核心脉络,本篇完整覆盖了:
- 模板引擎的价值:运行时用服务端真实值替换模板变量,输出静态 HTML,免去客户端 API 调用;
- Pug 的两种变量引用写法:内联
#{variable_name}与元素赋值p=variable_name(等号前不留空格); - 用缩进表达嵌套、用
.保留脚本原文、用if/else做服务端条件渲染的完整示例及其生成的 HTML; - 动手操作:
res.render('index', { title: 'Hello', message: 'Please log in' }); - 测试判据:请求首页后,
Please log in必须出现在pug-variable属性中,且模板文件不得被篡改。
完成本题后,你可以在 advanced-node-and-express 挑战列表 中继续下一题 Set up Passport,开始用户认证的构建。
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 StartedRust0622
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