首页
/ freeCodeCamp 课程详解:用 Bootstrap 栅格系统为 Cat Photo App 打造自定义标题栏

freeCodeCamp 课程详解:用 Bootstrap 栅格系统为 Cat Photo App 打造自定义标题栏

2026-09-07 17:05:32作者:郦嵘贵Just

这篇技术指南聚焦 freeCodeCamp 前端开发认证课程中 Bootstrap 模块的第 12 个练习“Create a Custom Heading”,以仓库中该挑战的完整 Markdown 源文件(curriculum/challenges/english/blocks/bootstrap/bad87fee1348bd9aede08845.md)为核心展开。你将读懂该练习的关卡结构、响应式栅格原理、自动判题规则,并拿到可直接运行的完整解法代码与扩展说明,从而掌握用 Bootstrap 将「标题 + 图片」编排到同一行的标准做法。

一、挑战背景:在页面中编排一个自定义标题栏

本次挑战所属的 bootstrap 代码块(block)位于 Front End Development Libraries 认证的第一个学习块。在 curriculum/structure/superblocks/front-end-development-libraries.json 中可以看到该认证按 bootstrap → jquery → sass → react → redux → react-and-redux → front-end-development-libraries-projects 的顺序组织课程内容。

块级文件 curriculum/structure/blocks/bootstrap.json 中记录了 Bootstrap 模块的完整练习序列(challengeOrder),本次“Create a Custom Heading”位于第 12 位,其上一关是“Use a span to Target Inline Elements”,下一关是“Add Font Awesome Icons to our Buttons”。这意味着此时学习者已经掌握了 container-fluid 容器、row 行、col-xs-* 分栏、btn 按钮与 text-* 文本工具类的基础用法。

挑战的目标很直观:为 Cat Photo App 页面做一个自定义页头——把应用标题与一张猫咪图片放进同一行。该块在学习环境中预置了 Bootstrap 3.3.7 的 CDN 样式:

{
  "dashedName": "bootstrap",
  "helpCategory": "HTML-CSS",
  "required": [
    {
      "link": "https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.3.7/css/bootstrap.css"
    }
  ],
  "blockLayout": "legacy-challenge-list"
}

注意此处的 blockLayout: "legacy-challenge-list",说明 Bootstrap 块使用经典「挑战列表」布局渲染,这也是该块内练习保留了传统 HTML/内联编辑器交互形态的原因。

二、关卡文件的结构解剖:Front Matter 与四个标准段落

每个课程挑战在仓库中都是一个 Markdown 文件,正文由特殊标记切分为若干标准区块。本关卡的 YAML Front Matter 为:

---
id: bad87fee1348bd9aede08845
title: Create a Custom Heading
challengeType: 0
forumTopicId: 16816
dashedName: create-a-custom-heading
---

关键字段含义:

  • id:挑战在课程数据库中的全局唯一标识,是 bootstrap.jsonchallengeOrder 引用的主键;
  • challengeType: 0:标识为 html 类型。可对照 packages/shared/src/config/challenge-types.tsconst html = 0 的定义理解——HTML 类型挑战会以经典代码编辑器方式呈现,并运行浏览器内 DOM 断言;
  • dashedName:由标题派生的 URL 友好名,用于链接定位;
  • forumTopicId:历史遗留的论坛话题 ID。

Front Matter 之后依次是四个 # --xxx-- 标记段落:

  1. --description--:题目描述与练习目标;
  2. --hints--:自动判题的断言代码(每一段 Hint 附带一段可执行的 JS 校验);
  3. --seed--:起点代码(内含 --seed-contents-- 子标记,即编辑器初始渲染的 HTML);
  4. --solutions--:官方参考答案。

这种「Markdown 描述 + 嵌入式断言」的格式是 freeCodeCamp 全部课程内容的统一存储规范,字段的合法性由 curriculum/schema/challenge-schema.js 中的 Joi 校验规则约束。

三、响应式栅格原理:为什么是 row + col-xs-8 + col-xs-4

--description-- 段落给出的核心知识点是 Bootstrap 的响应式栅格系统:

  • 页面被划分为 12 列,通过把子元素放进 .row(行)内、再为每个子元素指定相对宽度类(如 col-xs-8 表示占 8/12 = 2/3 宽),即可让元素横向并排;
  • Bootstrap 的绝大多数工具类都可以直接作用于 div 元素;
  • 栅格类在移动优先(mobile-first)的 Bootstrap 3 中,col-xs-* 对额外小屏幕及以上所有尺寸生效——这也是本关只在 col-xs-* 前缀上做文章的原因:col-xs-8col-xs-4 之和恰为 12,标题与图片因此能严丝合缝地占据整行。

具体任务只有一句话级别的操作,却直接演示了栅格最核心的用法:把「第一张图片」与 h2 一起嵌套进同一个 <div class="row">,其中 h2 放进 <div class="col-xs-8">、图片放进 <div class="col-xs-4">。描述中特意提示观察点:图片此时会自动缩放到恰好适合文本并排的宽度——这是 img-responsive 类与分栏宽度的共同效果。

四、起点代码:看清你拿到手的状态

--seed-contents-- 中呈现了编辑器初始代码(源文件第 62–119 行):

<link href="https://fonts.googleapis.com/css?family=Lobster" rel="stylesheet" type="text/css">

<style>
  h2 {
    font-family: Lobster, Monospace;
  }

  .thick-green-border {
    border-color: green;
    border-width: 10px;
    border-style: solid;
    border-radius: 50%;
  }
</style>

<div class="container-fluid">
  <h2 class="text-primary text-center">CatPhotoApp</h2>

  <a href="#"><img class="img-responsive thick-green-border" src="https://cdn.freecodecamp.org/curriculum/cat-photo-app/relaxing-cat.jpg" alt="A cute orange cat lying on its back."></a>

  <img src="https://cdn.freecodecamp.org/curriculum/cat-photo-app/running-cats.jpg" class="img-responsive" alt="Tuxedo cats running on dirt ground.">
  <div class="row">
    <div class="col-xs-4">
      <button class="btn btn-block btn-primary">Like</button>
    </div>
    <div class="col-xs-4">
      <button class="btn btn-block btn-info">Info</button>
    </div>
    <div class="col-xs-4">
      <button class="btn btn-block btn-danger">Delete</button>
    </div>
  </div>
  <p>Things cats <span class="text-danger">love:</span></p>
  <ul>
    <li>catnip</li>
    <li>laser pointers</li>
    <li>lasagna</li>
  </ul>
  <p>Top 3 things cats hate:</p>
  <ol>
    <li>flea treatment</li>
    <li>thunder</li>
    <li>other cats</li>
  </ol>
  <form action="https://freecatphotoapp.com/submit-cat-photo">
    <label><input type="radio" name="indoor-outdoor"> Indoor</label>
    <label><input type="radio" name="indoor-outdoor"> Outdoor</label>
    <label><input type="checkbox" name="personality"> Loving</label>
    <label><input type="checkbox" name="personality"> Lazy</label>
    <label><input type="checkbox" name="personality"> Crazy</label>
    <input type="text" placeholder="cat photo URL" required>
    <button type="submit">Submit</button>
  </form>
</div>

起点代码值得注意的几个细节:

  • 页面上存在两张猫图:第一张 relaxing-cat.jpg 被包在 <a href="#"> 链接中,并且叠加了自定义圆形绿边框类 .thick-green-border;第二张 running-cats.jpg 是独立的整行图片。描述中反复说“你的第一张图片 / topmost img”,正是为了精确锁定嵌套在 <a> 中的那张图;
  • 页面下方已经存在一个 row,其中三个按钮各占 col-xs-4(合计 12 列)——这是一个可对照参考的“标准分栏范例”,说明为何标题与图片也必须合计 12 列;
  • container-fluid 提供全宽响应式容器;h2 目前使用 text-primary(蓝色)与 text-center(居中)工具类,而这些排版类与栅格完全正交、可以自由组合。

五、实现步骤与官方解法

动手改动的范围极小:仅需调整 h2 与其上方第一张图片的容器结构。逐步操作如下:

  1. container-fluid 内部、现有 <h2 class="text-primary text-center">CatPhotoApp</h2>上方或包裹处新建 <div class="row">
  2. 在该 row 内新建 <div class="col-xs-8">,把 h2 元素移入其中;
  3. 在该 row 内再新建 <div class="col-xs-4">,把包含第一张图片的 <a href="#"><img ...></a> 整体移入其中;
  4. 确保两列宽度合计为 12,并保证所有新增 div 标签都正确闭合。

官方解法完整代码(源文件第 121–182 行):

<link href="https://fonts.googleapis.com/css?family=Lobster" rel="stylesheet" type="text/css">

<style>
  h2 {
    font-family: Lobster, Monospace;
  }

  .thick-green-border {
    border-color: green;
    border-width: 10px;
    border-style: solid;
    border-radius: 50%;
  }
</style>

<div class="container-fluid">
  <div class="row">
    <div class="col-xs-8">
      <h2 class="text-primary  text-center">CatPhotoApp</h2>
    </div>
    <div class="col-xs-4">
        <a href="#"><img class="img-responsive thick-green-border" src="https://cdn.freecodecamp.org/curriculum/cat-photo-app/relaxing-cat.jpg" alt="A cute orange cat lying on its back."></a>
    </div>
  </div>
  <img src="https://cdn.freecodecamp.org/curriculum/cat-photo-app/running-cats.jpg" class="img-responsive" alt="Tuxedo cats running on dirt ground.">
  <div class="row">
    <div class="col-xs-4">
      <button class="btn btn-block btn-primary">Like</button>
    </div>
    <div class="col-xs-4">
      <button class="btn btn-block btn-info">Info</button>
    </div>
    <div class="col-xs-4">
      <button class="btn btn-block btn-danger">Delete</button>
    </div>
  </div>
  <p>Things cats <span class="text-danger">love:</span></p>
  <ul>
    <li>catnip</li>
    <li>laser pointers</li>
    <li>lasagna</li>
  </ul>
  <p>Top 3 things cats hate:</p>
  <ol>
    <li>flea treatment</li>
    <li>thunder</li>
    <li>other cats</li>
  </ol>
  <form action="https://freecatphotoapp.com/submit-cat-photo">
    <label><input type="radio" name="indoor-outdoor"> Indoor</label>
    <label><input type="radio" name="indoor-outdoor"> Outdoor</label>
    <label><input type="checkbox" name="personality"> Loving</label>
    <label><input type="checkbox" name="personality"> Lazy</label>
    <label><input type="checkbox" name="personality"> Crazy</label>
    <input type="text" placeholder="cat photo URL" required>
    <button type="submit">Submit</button>
  </form>
</div>

需要注意,rowcol-xs-* 构成了新的 DOM 层级,原先直接躺在 container-fluid 下的 h2 与第一张图片被整体“上移”进栅格结构中;running-cats.jpg 则保持原样留在行外,继续以 img-responsive 独立换行显示。

六、从 --hints-- 反推判题逻辑:DOM 断言逐条拆解

--hints-- 段落里的每段 JS 都是真实运行在浏览器环境中的断言,它们精确刻画了“什么样的 DOM 结构才算过关”。逐条理解它们,等于读懂了 freeCodeCamp 如何用 document.querySelector 校验嵌套结构:

Hint 1:h2 与图片必须同处一个 .row

const row = document.querySelector('div.row');
const h2 = row?.querySelectorAll(`:scope ${'h2'}`)
const image = row?.querySelectorAll(`:scope ${'img'}`)
assert.lengthOf(h2,1);
assert.lengthOf(image ,1);
  • querySelector('div.row') 命中页面第一个 div.row。注意页面中共有两个 row——按钮行与本关新建的标题行,由于 HTML 文档顺序,这里的第一个 row 恰好是你新建的标题行,因此标题与第一张图片必须出现在文档里更靠前的那个 row 中;
  • :scope 限定只在当前 row 内部查找(而非整页搜索),assert.lengthOf(image, 1) 要求该行内恰好出现一张 img,即图片必须被移进行而不是继续留在 container-fluid 层。

Hint 2:图片要放进 col-xs-4 且不得再嵌一层 div

const column = document.querySelector('div.col-xs-4');
const div = column?.querySelectorAll(`:scope ${'div'}`);
const img = column?.querySelectorAll(`:scope ${'img'}`)
assert.isEmpty(div);
assert.lengthOf(img,1)
  • 页面里唯一的 div.col-xs-4 就是你新建的分栏(按钮行里不存在 col-xs-4 之外的类组合?事实上按钮行使用 col-xs-4,此处会命中按钮行中第一个 col-xs-4……但要求该列不含任何嵌套 div 且含恰好一张 img。为避免歧义,官方解法的结构保证:标题行先于按钮行出现,querySelector 命中第一个匹配节点,因此按正确顺序编写即通过);
  • assert.isEmpty(div) 强调列内不能有多余的包装 div,图片应直接位于该列下(可经 <a> 包裹)。

Hint 3:h2 放进 col-xs-8,同样不允许中间再套 div

const column = document.querySelector('div.col-xs-8');
const div = column?.querySelectorAll(`:scope ${'div'}`);
const h2 = column?.querySelectorAll(`:scope ${'h2'}`)

assert.isEmpty(div);
assert.lengthOf(h2, 1);

Hint 4:标签闭合完整性

assert.match(code,/<\/div>/g);
assert.match(code,/<div/g);
assert.equal(code.match(/<\/div>/g).length,code.match(/<div/g).length);
  • 这段断言面向源码文本而非 DOM:统计源码中 <div</div> 的出现次数并要求两者相等,防止因漏写闭合标签造成隐性 HTML 结构错误——这正是本次新增两个嵌套 div 后最易犯的错误。

七、放在更大图景中:这一关在课程体系里的位置与复用价值

从仓库结构看,本文件只是 Bootstrap 模块 31 个练习之一(curriculum/structure/blocks/bootstrap.jsonchallengeOrder 完整列出),同目录下相邻文件(如 bad87fee1348bd9aedc08845.md)延续了同样的 Cat Photo App 项目,逐步叠加 Font Awesome 图标、响应式单选/复选框、表单控件等。这种“同一项目反复迭代”的设计让每一关改动都很小、可验证性强。

这一关沉淀的可复用技能点包括:

  • Bootstrap 3 栅格三分法col-xs-8 + col-xs-4(或三个 col-xs-4)是“图标标题 + 缩略图”“侧栏 + 主内容”等布局的原型;
  • 媒体类与容器组合img-responsive 使图片宽度跟随分栏(不超出父容器),这是图片能“正好适配文本旁”的根本原因;
  • 工具类正交组合text-primary text-center.thick-green-border 等类可与栅格类共存于同一元素,互不干扰;
  • 读懂断言式 Hintassert.lengthOf / assert.isEmpty / assert.equal 组合出的 DOM 校验,是课程后续所有 HTML/CSS/jQuery 挑战通用的判题语言。如果学习时遇到“Hint 不通过”,优先检查:目标元素是否在正确的父级里、是否嵌套了多余 div、闭合标签数量是否匹配。

对课程贡献者与研究者而言,若要在本地验证该文件的规范合法性,可参考 curriculum/schema/challenge-schema.js 与同级测试对 Front Matter 与内容段的约束;若需了解各挑战类型如何映射到渲染与提交流程,可对照 packages/shared/src/config/challenge-types.tsviewTypessubmitTypes 的映射表——challengeType: 0(html)对应 classic 视图与基于测试的提交方式,与 --hints-- 里那些浏览器内断言完全吻合。

总之,这一课麻雀虽小,却完整串联了“栅格分栏 → DOM 嵌套 → 自动化断言”的 freeCodeCamp 前端教学闭环,是理解经典前端教程如何把响应式布局拆解为可操作、可检验步骤的绝佳样本。

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

项目优选

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