freeCodeCamp 课程详解:用 Bootstrap 栅格系统为 Cat Photo App 打造自定义标题栏
这篇技术指南聚焦 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.json 中challengeOrder引用的主键;challengeType: 0:标识为html类型。可对照 packages/shared/src/config/challenge-types.ts 中const html = 0的定义理解——HTML 类型挑战会以经典代码编辑器方式呈现,并运行浏览器内 DOM 断言;dashedName:由标题派生的 URL 友好名,用于链接定位;forumTopicId:历史遗留的论坛话题 ID。
Front Matter 之后依次是四个 # --xxx-- 标记段落:
--description--:题目描述与练习目标;--hints--:自动判题的断言代码(每一段 Hint 附带一段可执行的 JS 校验);--seed--:起点代码(内含--seed-contents--子标记,即编辑器初始渲染的 HTML);--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-8与col-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 与其上方第一张图片的容器结构。逐步操作如下:
- 在
container-fluid内部、现有<h2 class="text-primary text-center">CatPhotoApp</h2>的上方或包裹处新建<div class="row">; - 在该
row内新建<div class="col-xs-8">,把h2元素移入其中; - 在该
row内再新建<div class="col-xs-4">,把包含第一张图片的<a href="#"><img ...></a>整体移入其中; - 确保两列宽度合计为 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>
需要注意,row 与 col-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.json 的 challengeOrder 完整列出),同目录下相邻文件(如 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等类可与栅格类共存于同一元素,互不干扰; - 读懂断言式 Hint:
assert.lengthOf / assert.isEmpty / assert.equal组合出的 DOM 校验,是课程后续所有 HTML/CSS/jQuery 挑战通用的判题语言。如果学习时遇到“Hint 不通过”,优先检查:目标元素是否在正确的父级里、是否嵌套了多余 div、闭合标签数量是否匹配。
对课程贡献者与研究者而言,若要在本地验证该文件的规范合法性,可参考 curriculum/schema/challenge-schema.js 与同级测试对 Front Matter 与内容段的约束;若需了解各挑战类型如何映射到渲染与提交流程,可对照 packages/shared/src/config/challenge-types.ts 中 viewTypes、submitTypes 的映射表——challengeType: 0(html)对应 classic 视图与基于测试的提交方式,与 --hints-- 里那些浏览器内断言完全吻合。
总之,这一课麻雀虽小,却完整串联了“栅格分栏 → DOM 嵌套 → 自动化断言”的 freeCodeCamp 前端教学闭环,是理解经典前端教程如何把响应式布局拆解为可操作、可检验步骤的绝佳样本。
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 StartedRust0627
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