freeCodeCamp Bootstrap 课程实战:为 Bootstrap Wells 添加 h4 标签(Label Bootstrap Wells)
本文基于 freeCodeCamp 课程体系中的挑战题 label-bootstrap-wells(“Label Bootstrap Wells”)展开,系统讲解 Bootstrap 3 栅格布局中 well 组件的构建过程:如何在一行两列的 col-xs-6 栅格中嵌套带 id 的 well 容器、为每个容器添加 #left-well / #right-well 的 h4 标题标签,并理解平台如何逐条执行 JS 断言来验证你的 DOM 结构。读完后,你将能够完整复刻这道题的起始代码与最终代码,并掌握该课程系列的渐进式搭建套路。
1. 课程定位:这道题在整个 Bootstrap 块中的位置
该挑战题属于 freeCodeCamp 课程中的 bootstrap 块。在 bootstrap.json 中可以确认该块的关键元数据:
dashedName: "bootstrap",helpCategory: "HTML-CSS";- 通过
required字段声明了外部 CSS 依赖:Bootstrap 3.3.7 的bootstrap.css(https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.3.7/css/bootstrap.css)。从源码结构看,平台会把该链接注入挑战页作为必需资源,这也是为什么练习中可以自由使用container-fluid、row、col-xs-6、well、btn btn-default等 Bootstrap 3 类名; challengeOrder数组规定了 37 道挑战的线性学习顺序,blockLayout为legacy-challenge-list。
本挑战 bad87fee1348bd9aec908854 在 challengeOrder 中排第 32 位,标题为 “Label Bootstrap Wells”。它前后紧邻的题目共同构成了一条清晰的“jQuery Playground 搭建链”:
| 顺序 | 挑战 ID | 标题 | 作用 |
|---|---|---|---|
| 前 3 | bad87fee1348bd9aec908848 |
Create Bootstrap Wells | 在两个 col-xs-6 中各嵌套一个 div.well |
| 前 2 | bad87fee1348bd9aec908849 |
Add Elements within Your Bootstrap Wells | 在每个 well 内加入 3 个 button |
| 前 1 | bad87fee1348bd9aec908853 |
Add id Attributes to Bootstrap Elements | 为两个 well 分别加 id="left-well" / id="right-well" |
| 本篇 | bad87fee1348bd9aec908854 |
Label Bootstrap Wells | 为每个 well 添加 h4 标签 |
| 后 1 | bad87fee1348bd9aec908855 |
Give Each Element a Unique id | 给 6 个按钮加 target1–target6 |
| 后 2 | bad87fee1348bd9aec908856 |
Label Bootstrap Buttons | 给按钮加 #target1–#target6 文本 |
可以看出,课程采用“每步只加一层”的增量式设计:本篇的任务只是在这条链上插入两行 h4。这种设计也决定了本篇代码示例可以直接从前一步的“结束代码”开始,并直接衔接后一步的起始代码。
2. 题目要求(description 原文翻译与要点)
挑战文件 bad87fee1348bd9aec908854.md 的 front matter 声明了它的元信息:
---
id: bad87fee1348bd9aec908854
title: Label Bootstrap Wells
challengeType: 0
forumTopicId: 18223
dashedName: label-bootstrap-wells
---
其中 challengeType: 0 对应 freeCodeCamp 的编程挑战类型(在 challenge-types.ts 中,0 即 programmer 类型,意味着题目附带代码编辑器与自动测试);dashedName 是题目在路由与文件系统中的 URL 化标识。
--description-- 部分给出的核心指令是:
For the sake of clarity, let's label both of our wells with their ids. Above your left-well, inside its
col-xs-6divelement, add anh4element with the text#left-well. Above your right-well, inside itscol-xs-6divelement, add anh4element with the text#right-well.
要点拆解:
- 位置要求:
h4必须放在对应col-xs-6这个div内部,且位于well的上方(即作为col-xs-6的直接子元素,排在该列中div.well之前),而不是塞进well内部; - 文本要求:左边一列写
#left-well,右边一列写#right-well——文本与id值一一对应,形成“标题即选择器”的直观标注,这也是后续 jQuery 选择器练习($('#left-well'))的认知铺垫; - 标题级别选择:使用
h4而非h1–h3,因为页面顶部已有h3(“jQuery Playground”),用更低级别的标题保持视觉层次不喧宾夺主。
3. 起始代码:前一步骤的产物
挑战文件的 --seed--(--seed-contents--)部分定义了练习开始时编辑器中的代码。完整继承自前一步 bad87fee1348bd9aec908853.md 的解决方案:
<div class="container-fluid">
<h3 class="text-primary text-center">jQuery Playground</h3>
<div class="row">
<div class="col-xs-6">
<div class="well" id="left-well">
<button class="btn btn-default target"></button>
<button class="btn btn-default target"></button>
<button class="btn btn-default target"></button>
</div>
</div>
<div class="col-xs-6">
<div class="well" id="right-well">
<button class="btn btn-default target"></button>
<button class="btn btn-default target"></button>
<button class="btn btn-default target"></button>
</div>
</div>
</div>
</div>
对照 Bootstrap 3 的语义逐层理解这个骨架:
container-fluid:全宽流式容器,是后续挑战 “House our page within a Bootstrap container-fluid div” 的成果;h3.text-primary.text-center:蓝色(Bootstrap 3 主题色#337ab7体系中的text-primary)居中标题;row+ 两个col-xs-6:经典的一行两列栅格,每列在任意屏幕尺寸下各占 12 列网格中的 6 列;div.well:Bootstrap 的“凹槽”组件,自带浅灰背景(#f5f5f5)、内边距与 1px 边框,用于为内容制造视觉深度(“visual sense of depth”,这是 bad87fee1348bd9aec908848.md 中对well的原话描述);- 两个 well 已通过上一步获得了唯一的
id="left-well"与id="right-well"(HTML 规范要求 id 页面内唯一,该挑战也专门强调了这一点)。
col-xs-6 内部 well 上方的空行,就是本题要你填入 h4 的位置。
4. 解题:完整的最终代码
在左侧 col-xs-6 的 well 之上插入 <h4>#left-well</h4>,右侧对称插入 <h4>#right-well</h4>。挑战文件 --solutions-- 部分给出的官方参考解如下:
<div class="container-fluid">
<h3 class="text-primary text-center">jQuery Playground</h3>
<div class="row">
<div class="col-xs-6">
<h4>#left-well</h4>
<div class="well" id="left-well">
<button class="btn btn-default target"></button>
<button class="btn btn-default target"></button>
<button class="btn btn-default target"></button>
</div>
</div>
<div class="col-xs-6">
<h4>#right-well</h4>
<div class="well" id="right-well">
<button class="btn btn-default target"></button>
<button class="btn btn-default target"></button>
<button class="btn btn-default target"></button>
</div>
</div>
</div>
</div>
改动非常克制——只新增两行。渲染效果是:每个凹槽上方出现一行小标题,左列显示 #left-well,右列显示 #right-well,标题文本与容器 id 完全一致,为读者建立了“看到 #xxx 就知道对应 $('#xxx') 选择器”的心智模型。
5. 测试断言逐条解析:平台如何验证你的答案
挑战文件 --hints-- 部分共 4 条提示,每条都内嵌一段将在浏览器 DOM 环境中执行的 Chai 断言代码。按 challenge-schema.js 的 Joi 模式定义,每道公开挑战的 tests 数组中每个元素都要求 text(提示文案)与 testString(可执行断言代码)两个字段,这些断言即来源于此。下面逐条解析:
5.1 结构断言:每个 col-xs-6 必须恰好有一个 h4
const columnSixes = document.querySelectorAll('.col-xs-6');
const columnSixOneChildren = columnSixes?.[0]?.querySelectorAll(`:scope ${'h4'}`);
assert.lengthOf(columnSixOneChildren,1);
const columnSixTwoChildren = columnSixes?.[1]?.querySelectorAll(`:scope ${'h4'}`);
assert.lengthOf(columnSixTwoChildren,1);
这条断言值得细看两个细节:
querySelectorAll('.col-xs-6')按 DOM 顺序取到两个栅格列,分别索引[0](左)与[1](右);`:scope ${'h4'}`中的:scope伪类把查询范围锚定在“该col-xs-6元素及其后代”内,防止误匹配到列外部的h4。这里把'h4'放进模板字符串拼接,是一种规避静态扫描把选择器误判为完整选择器的写法,从源码结构看是课程测试代码的常见防御性技巧。
可验证的失败场景:如果把 h4 写进 well 内部,:scope h4 仍能匹配(well 是列的后代),因此该断言通过;但题目文字明确要求“above your well”,且下一条断言会进一步约束位置语义(见 5.2 的索引假设)。
5.2 文本断言:h4 的内容必须匹配 id 命名
const firstH4 = document.querySelectorAll('h4')?.[0];
assert.match(firstH4?.textContent,/#left-well/gi);
const secondH4 = document.querySelectorAll('h4')?.[1];
assert.match(secondH4?.textContent,/#right-well/gi);
document.querySelectorAll('h4') 返回页面中全部 h4 的 NodeList,断言假设 [0] 是左标签、[1] 是右标签。这个假设成立的前提正是 DOM 文档顺序:h4 必须分别位于各自 col-xs-6 的开头(即 well 之前)且左列先于右列出现。如果写反(左列写 #right-well)或漏写,assert.match 的正则 /#left-well/gi(大小写不敏感)匹配失败,测试即不通过。?. 可选链则保证即使你没写 h4,断言也只会以“值不存在”的方式失败而不是抛 TypeError。
5.3 闭合标签断言:标签必须成对
assert.match(code,/<\/h4>/g);
assert.match(code,/<h4/g);
assert.equal(code.match(/<\/h4>/g).length , code.match(/<h4/g).length);
与前两条检查“运行后的 DOM”不同,这条直接对源代码字符串 code 做正则统计:</h4> 的数量必须与 <h4 的数量严格相等。它同时隐含两个要求:必须真正使用 h4 元素(不能只写文本或图片占位),且标签要正确闭合(不能写成 <h4>#left-well 后忘记 </h4>)。这类“开闭标签计数”断言是 freeCodeCamp HTML 挑战的通用校验手法,可对照同块的 bad87fee1348bd9aec908849.md 中对 button 标签的同款检查。
6. 挑战文件的结构:一道题在仓库中是如何组织的
从工程视角看,bad87fee1348bd9aec908854.md 本身就是一个自包含的“挑战包”,遵循 freeCodeCamp 课程文件的分区约定:
- YAML front matter:
id(MongoDB ObjectId 形式的唯一标识)、title、challengeType: 0、forumTopicId: 18223(对应官方论坛的求助主题,方便学生搜索社区讨论)、dashedName(URL slug,需符合 challenge-schema.js 中slugRE = /^[a-z0-9-]+$/的约束); # --description--:展示给学员的题面;# --hints--:提示文案 + 内嵌testString断言,构成自动判题逻辑;# --seed-- / ## --seed-contents--:编辑器初始代码(HTML 文件内容);# --solutions--:官方参考答案。
这套分区与 challenge-schema.js 中的 Joi 校验模式一一对应:solutions 被校验为“文件对象数组的数组”(每个文件含 seed、contents 等字段),tests 数组的每个元素要求 text 与 testString 同时存在。课程构建管线(curriculum/src/get-challenges.ts 等工具)会把这些分区解析为运行时挑战对象,供 api 侧下发与客户端渲染。理解这一映射关系后,你读任何一道 freeCodeCamp 的挑战 md 文件,都能立刻分清“题面 / 判题逻辑 / 初始代码 / 参考解”四个层面。
7. 小结与后续路线
本篇以最小的改动量(两行 h4)完成了一个完整的知识点闭环:
- 确认了
well组件必须嵌套在col-xs-6列内、row之下的栅格层级; - 掌握了“标题文本 =
#+ 容器 id”的标注约定,为 jQuery 选择器练习铺垫; - 理解了断言的三种校验维度:DOM 结构(
:scope h4计数)、DOM 文本(textContent正则)、源码形态(开闭标签配对)。
按 bootstrap.json 的 challengeOrder,完成本题后应继续:
- bad87fee1348bd9aec908855.md(Give Each Element a Unique id):给 6 个按钮加
target1–target6; - bad87fee1348bd9aec908856.md(Label Bootstrap Buttons):给按钮加
#target1–#target6文本——它的 seed 代码正是本题的官方解,印证了挑战链“上一步的解 = 下一步的 seed”的衔接机制; - bad87fee1348bd9aec908857.md(Use Comments to Clarify Code):用注释完善整个 Playground 代码,作为本段栅格 + well + 按钮搭建的收尾。
至此,整个 “jQuery Playground” 静态骨架(流式容器 → 两列栅格 → 凹槽 → 按钮 → id → 标签 → 注释)搭建完毕,为后续 jQuery 交互块(curriculum/challenges/english/blocks/jquery)中的 $('...').addClass() 等练习提供了稳定的操作对象。
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 StartedRust0625
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