深度解析 freeCodeCamp "Comment out HTML" 挑战:用 HTML 注释临时隐藏页面元素
本指南围绕 freeCodeCamp 开源课程中 Responsive Web Design 认证的 "Comment out HTML"(注释 HTML)挑战展开,逐层讲解 HTML 注释语法 <!-- --> 的用途、"只注释 h1 与 p、保留 h2 可见"的任务拆解方式、seed 初始代码的陷阱,以及挑战自带 5 条自动化测试断言的判定逻辑。读完本文,你既能顺利通过该关卡,也能理解 freeCodeCamp 挑战文件(Markdown + front matter)的编写规范、challengeType 的语义,以及隐藏在其背后的 DOM 断言机制。
一、挑战在课程地图中的位置
该挑战的源文件位于 curriculum/challenges/english/blocks/basic-html-and-html5/bad87fee1348bd9aedf08804.md,属于 Basic HTML and HTML5 区块(block)。从仓库的区块结构文件 curriculum/structure/blocks/basic-html-and-html5.json 可以看到其完整编排顺序,它处在一个刻意设计的教学序列中:
- Say Hello to HTML Elements —— 认识第一个 HTML 元素;
- Headline with the h2 Element —— 标题元素;
- Inform with the Paragraph Element —— 段落元素;
- Fill in the Blank with Placeholder Text —— 占位文本;
- Uncomment HTML(bad87fee1348bd9aedf08802.md)—— 反注释,让代码生效;
- Comment out HTML(本挑战)—— 正注释,让代码失效;
- Delete HTML Elements —— 直接删除元素。
也就是说,freeCodeCamp 先教你用注释“隐藏”元素,再教你“取消注释”让它们恢复,最后才进入“删除元素”,通过注释与删除的对照,让学习者理解两者的区别:注释是临时性停用代码,而删除是永久移除代码。整个区块又隶属于 curriculum/structure/superblocks/responsive-web-design.json 中的 Responsive Web Design 认证路径,是零基础学员接触 HTML 后最早遇到的几个关卡之一。
二、HTML 注释语法:<!-- 与 -->
挑战的 # --description-- 部分直接点明核心语法:
Remember that in order to start a comment, you need to use
<!--and to end a comment, you need to use-->
HTML 注释由起始定界符 <!-- 与结束定界符 --> 包裹,介于两者之间的全部内容都不会被浏览器解析和渲染。注意该语法与多语言注释的差异:
- HTML 注释不支持嵌套:第一个
-->就会结束注释; - 注释定界符必须成对出现,只写
<!--而不写-->会把后面整段代码都“吞掉”; - 注释内的文本(包括其中的 HTML 标签)对用户不可见,但会原样保留在源代码中,方便后续恢复。
正是利用“注释内容不渲染、但代码仍保留在源文件中”这两个特性,开发者在日常工作中常用注释来:
- 给后续维护者留说明文字;
- 临时屏蔽某段功能代码,而无需删除它(方便调试与回退);
- 在页面开发初期占位。
本挑战前面的 "Uncomment HTML" 已给出定义:Commenting is a way that you can leave comments for other developers within your code without affecting the resulting output that is displayed to the end user. 同时它也是一条“让代码暂时不生效的捷径”:make code inactive without having to delete it entirely(见 Uncomment HTML 挑战源文件)。而本挑战则是从“取消注释”反过来,练习“把代码注释掉”。
三、任务拆解:到底要做什么
挑战的 # --instructions-- 是一句非常具体、也暗含陷阱的指令:
Comment out your
h1element and yourpelement, but not yourh2element.
即:让 h1 与 p 从页面上“消失”,但 h2 必须保持可见。
初看很简单,难点在于 seed(初始代码)的形态。挑战给出的初始 HTML 是这样的:
<!--
<h1>Hello World</h1>
<h2>CatPhotoApp</h2>
<p>Kitty ipsum dolor sit amet, shed everywhere shed everywhere stretching attack your ankles chase the red dot, hairball run catnip eat the grass sniff.</p>
-->
这里存在一个非常典型的“坑”:三个元素被一条巨大的注释整体包裹。此时页面上什么都看不见。而任务要求的是“注释掉 h1 和 p、但不要注释 h2”。因此你不能简单地保留这一条大注释,而必须把这条注释拆解、重排为三条互不干扰的独立片段:
- 在
<h1>之前开始注释、并在<h1>之后立刻用-->结束,只包裹住h1; - 让
<h2>独立暴露在外,不被任何<!--/-->包裹; - 对
<p>再用一对<!--/-->单独包裹。
description 里特意强调:Here you'll need to end the comment before your h2 element begins.(你需要在 h2 元素开始之前结束注释)——这正是提示你要放弃整块注释、改为分段注释的核心暗示。
四、参考解法:逐行还原思路
挑战文件的 # --solutions-- 部分给出了官方参考答案:
<!--<h1>Hello World</h1>-->
<h2>CatPhotoApp</h2>
<!--<p>Kitty ipsum dolor sit amet, shed everywhere shed everywhere stretching attack your ankles chase the red dot, hairball run catnip eat the grass sniff.</p> -->
逐行拆解这套解法:
<!--<h1>Hello World</h1>-->:h1两侧分别紧跟<!--与-->,它被整体注释,不再出现在渲染结果中;<h2>CatPhotoApp</h2>:完全裸露、无注释包裹,正常渲染为页面标题;<!--<p>...</p> -->:p段落被独立注释(注意结束定界符前多了一个空格,这在语法上完全合法,注释中多余空白会被忽略)。
观察一个细节:注释定界符既可以独占一行(如 seed 中的写法),也可以与标签同处一行(如 solution 中的写法)。HTML 解析器只关心 <!-- 与 --> 的位置,不关心周围有没有空白或换行。理解这一点后,把整块注释“切割”成三个独立单元就不难了。
如果不想照抄官方解法,只要满足“两个被注释、一个不被注释”即可,例如下面这种保留换行的写法同样正确:
<!--
<h1>Hello World</h1>
-->
<h2>CatPhotoApp</h2>
<!--
<p>Kitty ipsum dolor sit amet, shed everywhere shed everywhere stretching attack your ankles chase the red dot, hairball run catnip eat the grass sniff.</p>
-->
五、读懂 5 条自动化测试断言
freeCodeCamp 的每个挑战都内置 # --hints-- 自动化测试,只有全部通过才算过关。本挑战共有 5 条断言,全部直接写在源文件中,我们来逐条解析它们的判定逻辑(这些断言使用 Chai 断言库风格,并在浏览器上下文中对“你提交后的代码”求值):
1. h1 必须被注释掉
assert.isEmpty(document.querySelectorAll('h1'));
document.querySelectorAll('h1') 会查询最终渲染后的 DOM。一旦 h1 位于 <!-- --> 之内,浏览器就不会把它构建进 DOM,查询结果为空集合(NodeList 长度为 0),assert.isEmpty 通过。反过来说,只要代码里残留着未被注释的 <h1>,这条断言就会失败。
2. h2 必须保持可见
assert.isNotEmpty(document.querySelectorAll('h2'));
与上一条正好相反:h2 必须真实存在于 DOM 中,querySelectorAll 返回非空集合,断言才通过。这条测试确保了“but not your h2 element”那半句要求被落实。
3. p 必须被注释掉
assert.isEmpty(document.querySelectorAll('p'));
逻辑与 h1 完全一致:p 也不能出现在渲染后的 DOM 中。
4. 每一条注释都必须正确闭合
assert.isAbove(code.match(/[^fc]-->/g).length, 1);
这是唯一一条针对源代码文本而非 DOM 的断言。它用正则 /[^fc]-->/g 在代码字符串中查找 --> 出现的次数,并要求大于 1。之所以排除前面紧跟字母 f 或 c 的 -->(负向字符类 [^fc]),是为了防止把 <!-- 起始定界符自身的那部分错误计数。它从正面保证:存在至少两处由 --> 完成的注释闭合(对应 h1 与 p 两条独立注释),从而间接验证注释结构是“成对、闭合、分段”的。
5. h1、h2、p 的书写顺序不许改变
assert.strictEqual(code.match(/<([a-z0-9]){1,2}>/g)[0],'<h1>');
assert.strictEqual(code.match(/<([a-z0-9]){1,2}>/g)[1],'<h2>');
assert.strictEqual(code.match(/<([a-z0-9]){1,2}>/g)[2],'<p>');
这条规则禁止你通过重排元素顺序这种“偷懒方式”来投机取巧(例如把 h2 挪到注释块外面、其余元素丢进注释)。它依次抽取代码中出现的短标签,并断言第一个是 <h1>、第二个是 <h2>、第三个是 <p>,即标签出现顺序与初始结构一致,你只能通过注释来调整可见性。
综上,本挑战的测试矩阵可归纳为:
| 断言目标 | 判定方式 | 失败示例 |
|---|---|---|
h1 被注释 |
渲染后 DOM 无 h1 |
忘记加注释或注释未闭合 |
h2 不被注释 |
渲染后 DOM 存在 h2 |
沿用初始的大注释块,把 h2 也包进去 |
p 被注释 |
渲染后 DOM 无 p |
忘记加注释或注释未闭合 |
| 注释闭合完整 | 源码中 --> 出现 2 次以上 |
只写 <!-- 不写 --> |
| 元素顺序不变 | 依次出现 <h1>、<h2>、<p> |
重排元素顺序 |
六、底层原理:为什么“注释掉”后元素就不可见
这套测试验证方式其实反映了 HTML 渲染的标准行为:
- HTML 源码被浏览器下载后,先经过 tokenizer(词法分析) 阶段。当解析器读到
<!--,会进入注释状态并一直吞掉字符,直到遇见-->; - 被注释的内容不会生成任何 DOM 节点,因此
document.querySelectorAll('h1')这类 DOM 查询自然查不到被注释的元素; - freeCodeCamp 的测试正是在页面加载完成后、对运行时 DOM 执行查询(对
code文本的断言除外),所以“注释影响 DOM 结构”这一浏览器机制,恰好成为机器可判定的通关标准。
这也解释了为什么该类挑战把“注释”作为 HTML 的必备基本功:掌握注释就能精确控制“哪些内容进入 DOM、哪些内容留在源码里”,这对调试、协作和渐进式开发都至关重要。相关断言在整个 Basic HTML 区块中被大量复用(同区块多个挑战的 # --hints-- 都使用了 assert.* 家族断言,见 curriculum/challenges/english/blocks/basic-html-and-html5 目录下的各挑战文件)。
七、挑战文件本身:Markdown 与 front matter 的结构
该挑战同时是 freeCodeCamp 课程体系的内容即代码(curriculum-as-code)的典型样本。一个挑战 = 一个 Markdown 文件,用 --- 包裹的 YAML front matter + 若干 # --section-- 语义化区块构成。其 front matter 字段含义如下:
| 字段 | 本挑战取值 | 作用 |
|---|---|---|
id |
bad87fee1348bd9aedf08804 |
全局唯一标识,被区块结构文件与种子数据引用 |
title |
Comment out HTML |
挑战标题 |
challengeType |
0 |
挑战类型编号 |
videoUrl |
Scrimba 教学视频地址 | 关联视频课程(此处仅作外部参考) |
forumTopicId |
16782 |
关联论坛讨论主题 |
dashedName |
comment-out-html |
URL 友好的短名称 |
其中 challengeType: 0 并非随意取值。在 packages/shared/src/config/challenge-types.ts 中,const html = 0 被定义为 HTML 类型挑战;同一文件中的 viewTypes 映射把 html 类型对应为 classic 视图(challenge-types.ts),submitTypes 则把其提交方式对应为 tests(challenge-types.ts)——也就是说,这一类挑战会以内置的经典单文件编辑器呈现,并通过运行 # --hints-- 中的测试来判断对错。
这些 front matter 字段并非自由填写,而是受严格 schema 校验约束。在 curriculum/schema/challenge-schema.js 中可以看到:
challengeType: Joi.number().min(0).max(33).required(),
即 challengeType 必须是 0~33 之间的整数且必填,0 正好处于合法区间内。整个课程通过这类 Joi schema(challenge-schema.js、curriculum-schema.js、meta-schema.js 等)保证上万份挑战文件的格式一致性。
而区块内挑战的先后顺序则由 curriculum/structure/blocks/basic-html-and-html5.json 的 challengeOrder 数组维护——它按 { "id", "title" } 列出全部 28 个挑战的次序,并声明 "helpCategory": "HTML-CSS" 便于课程导航分类。因此,编辑一道挑战的内容只要改对应 .md 文件,而调整其在课程中的位置则需要同步修改区块结构 JSON,二者是分离的。
八、递进式教学:从注释到删除的设计逻辑
把本挑战放进 Basic HTML and HTML5 区块的更大图景中看,能更清楚 freeCodeCamp 的教学思路。第 5 关 "Uncomment HTML" 与本关 "Comment out HTML" 构成一组“镜像练习”:
- Uncomment HTML:seed 中
h1、h2、p全被注释,任务是把三条注释去掉,让三个元素重新可见(其官方解法见 bad87fee1348bd9aedf08802.md 的# --solutions--); - Comment out HTML:seed 中三个元素被一条注释整体隐藏,任务变成有选择地注释其中两个、保留一个。
二者共同训练“开/关”代码的能力;紧接着的 Delete HTML Elements 则更进一步,教学习者如何彻底移除元素。三个关卡连起来,恰好覆盖了代码生命周期管理的三种动作:注释(临时禁用)→ 反注释(恢复启用)→ 删除(永久移除)。从 “Hello World” 的页面骨架(h1/h2/p)反复出现在这些早期关卡中可以看出,它们共用同一段 CatPhotoApp 示例文案,属于刻意设计的“脚手架课程”,让学员把注意力完全集中在单一新语法上。
九、常见错误与自检清单
通关前,对照以下检查点自查,可避免最常见的翻车原因:
- 没有拆开初始大注释:如果你仍保留 seed 中那个同时包裹三个元素的
<!-- ... -->,h2也会被隐藏,第 2 条断言必然失败。务必在h1结束后先写-->; - 只写了
<!--忘了-->:会出现“注释吞掉后续所有标签”的情况,第 4 条断言(-->出现次数)会失败; - 删除了元素而不是注释元素:任务要求注释而非删除;虽然两种方式都能让元素“不可见”,但第 5 条关于标签顺序的断言会因缺少标签而失败;
- 调整了
h1/h2/p的相对顺序:即使结果“看起来对”,也会被第 5 条断言拦截; - 在
h2内部或跨元素写注释:请确认每条注释都完整包住目标元素、且不波及相邻的h2。
用一句话概括本挑战的成功标准:源码中保留 <h1> 与 <p> 的完整标签但用注释包裹它们,同时让 <h2> 保持裸奔状态,且不改变三者出现顺序。 掌握这条规则,你不仅能顺利通关,也真正理解了 HTML 注释的语法边界与它在页面渲染中的角色。
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 StartedRust0624
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