首页
/ freeCodeCamp Bootstrap 课程实战:为 Bootstrap Wells 添加 h4 标签(Label Bootstrap Wells)

freeCodeCamp Bootstrap 课程实战:为 Bootstrap Wells 添加 h4 标签(Label Bootstrap Wells)

2026-09-07 14:11:15作者:温艾琴Wonderful

本文基于 freeCodeCamp 课程体系中的挑战题 label-bootstrap-wells(“Label Bootstrap Wells”)展开,系统讲解 Bootstrap 3 栅格布局中 well 组件的构建过程:如何在一行两列的 col-xs-6 栅格中嵌套带 idwell 容器、为每个容器添加 #left-well / #right-wellh4 标题标签,并理解平台如何逐条执行 JS 断言来验证你的 DOM 结构。读完后,你将能够完整复刻这道题的起始代码与最终代码,并掌握该课程系列的渐进式搭建套路。

1. 课程定位:这道题在整个 Bootstrap 块中的位置

该挑战题属于 freeCodeCamp 课程中的 bootstrap 块。在 bootstrap.json 中可以确认该块的关键元数据:

  • dashedName: "bootstrap"helpCategory: "HTML-CSS"
  • 通过 required 字段声明了外部 CSS 依赖:Bootstrap 3.3.7 的 bootstrap.csshttps://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.3.7/css/bootstrap.css)。从源码结构看,平台会把该链接注入挑战页作为必需资源,这也是为什么练习中可以自由使用 container-fluidrowcol-xs-6wellbtn btn-default 等 Bootstrap 3 类名;
  • challengeOrder 数组规定了 37 道挑战的线性学习顺序,blockLayoutlegacy-challenge-list

本挑战 bad87fee1348bd9aec908854challengeOrder 中排第 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 个按钮加 target1target6
后 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 中,0programmer 类型,意味着题目附带代码编辑器与自动测试);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-6 div element, add an h4 element with the text #left-well. Above your right-well, inside its col-xs-6 div element, add an h4 element with the text #right-well.

要点拆解:

  1. 位置要求h4 必须放在对应 col-xs-6 这个 div 内部,且位于 well上方(即作为 col-xs-6 的直接子元素,排在该列中 div.well 之前),而不是塞进 well 内部;
  2. 文本要求:左边一列写 #left-well,右边一列写 #right-well——文本与 id 值一一对应,形成“标题即选择器”的直观标注,这也是后续 jQuery 选择器练习($('#left-well'))的认知铺垫;
  3. 标题级别选择:使用 h4 而非 h1h3,因为页面顶部已有 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-6well 之上插入 <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 课程文件的分区约定:

  1. YAML front matterid(MongoDB ObjectId 形式的唯一标识)、titlechallengeType: 0forumTopicId: 18223(对应官方论坛的求助主题,方便学生搜索社区讨论)、dashedName(URL slug,需符合 challenge-schema.jsslugRE = /^[a-z0-9-]+$/ 的约束);
  2. # --description--:展示给学员的题面;
  3. # --hints--:提示文案 + 内嵌 testString 断言,构成自动判题逻辑;
  4. # --seed-- / ## --seed-contents--:编辑器初始代码(HTML 文件内容);
  5. # --solutions--:官方参考答案。

这套分区与 challenge-schema.js 中的 Joi 校验模式一一对应:solutions 被校验为“文件对象数组的数组”(每个文件含 seedcontents 等字段),tests 数组的每个元素要求 texttestString 同时存在。课程构建管线(curriculum/src/get-challenges.ts 等工具)会把这些分区解析为运行时挑战对象,供 api 侧下发与客户端渲染。理解这一映射关系后,你读任何一道 freeCodeCamp 的挑战 md 文件,都能立刻分清“题面 / 判题逻辑 / 初始代码 / 参考解”四个层面。

7. 小结与后续路线

本篇以最小的改动量(两行 h4)完成了一个完整的知识点闭环:

  • 确认了 well 组件必须嵌套在 col-xs-6 列内、row 之下的栅格层级;
  • 掌握了“标题文本 = # + 容器 id”的标注约定,为 jQuery 选择器练习铺垫;
  • 理解了断言的三种校验维度:DOM 结构(:scope h4 计数)、DOM 文本(textContent 正则)、源码形态(开闭标签配对)。

bootstrap.jsonchallengeOrder,完成本题后应继续:

  1. bad87fee1348bd9aec908855.md(Give Each Element a Unique id):给 6 个按钮加 target1target6
  2. bad87fee1348bd9aec908856.md(Label Bootstrap Buttons):给按钮加 #target1#target6 文本——它的 seed 代码正是本题的官方解,印证了挑战链“上一步的解 = 下一步的 seed”的衔接机制;
  3. bad87fee1348bd9aec908857.md(Use Comments to Clarify Code):用注释完善整个 Playground 代码,作为本段栅格 + well + 按钮搭建的收尾。

至此,整个 “jQuery Playground” 静态骨架(流式容器 → 两列栅格 → 凹槽 → 按钮 → id → 标签 → 注释)搭建完毕,为后续 jQuery 交互块(curriculum/challenges/english/blocks/jquery)中的 $('...').addClass() 等练习提供了稳定的操作对象。

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