freeCodeCamp 应用无障碍课程:用 nav 地标让屏幕阅读器导航更轻松
本篇技术指南围绕 freeCodeCamp 课程中的挑战「Make Screen Reader Navigation Easier with the nav Landmark」(用 nav 地标让屏幕阅读器导航更容易)展开,讲清楚 HTML5 nav 语义元素的内置地标(landmark)特性、为何用 div 包裹导航链接会损害可访问性,以及如何用一段可验证的测试断言来确认你的语义化改造是否达标。读完本文,你将掌握:把无语义容器替换为语义元素的完整实操流程、该挑战的完整初始代码与参考解答,以及 freeCodeCamp 课程仓库中这道题在 Responsive Web Design 课程里的定位与前后衔接关系。
挑战在课程结构中的位置
这道挑战位于课程仓库 curriculum 目录下,文件为 nav 地标挑战,属于 Applied Accessibility(应用无障碍)模块。该模块的组织信息由 applied-accessibility.json 描述,其中 helpCategory 标记为 HTML-CSS,challengeOrder 数组给出了全部 22 道挑战的完成顺序。按此顺序,本挑战排在第 7 位:
- 前置挑战:Make Screen Reader Navigation Easier with the header Landmark——先把页面顶部的
div改为header地标; - 当前挑战:把
header内包裹导航链接的div改为nav地标; - 后置挑战:Make Screen Reader Navigation Easier with the footer Landmark——把页脚版权信息的
div改为footer地标。
从源码结构看,这三道挑战共享同一个「Camper Cat 忍者训练页」的场景骨架,形成 header → nav → footer 的地标递进练习。再往上一层,responsive-web-design.json 显示 Applied Accessibility 是 Responsive Web Design 课程的第 4 个 block(位于 basic-html-and-html5、basic-css、applied-visual-design 之后),即学习者先掌握基础 HTML/CSS 与视觉设计后,才进入本模块学习语义化与无障碍。
核心概念:nav 元素与内置地标特性
挑战文档的核心说明(原文 description 部分)只有一句话,但信息密度很高:
nav元素是另一个带有内置地标特性的 HTML5 元素,用于方便屏幕阅读器导航。这个标签应包裹页面中的主要导航链接。
这里的关键点有两层:
- 地标(landmark)是浏览器自动赋予的。
nav不需要额外的 ARIA 属性——role="navigation"是隐式默认角色。屏幕阅读器和辅助技术(AT)通常提供「跳转到导航区域」之类的快捷操作,用户无需逐个朗读整个页面就能在多个区域之间跳跃。这与前面几道挑战中main、header元素共享同一种机制:语义元素本身即地标。 nav的语义边界是「主要导航」。它应当包裹页面的主导航链接(如站点菜单、页内锚点跳转),而不是包裹一切看起来像链接的列表。
文档同时给出了一条重要的边界规则:如果页面底部存在重复的站点链接,不必再用 nav 标记它们,用 footer(下一道挑战的内容)即可。这条规则避免了把「页脚辅助链接」也声明为导航地标,从而防止地标导航树中出现语义重复的导航区域。
练习场景:把 div 换成 nav
挑战的 instructions 部分设定的场景是:Camper Cat 在他的训练页面顶部加入了导航链接,但把它们包在了一个 div 里。任务是把这个 div 改为 nav 标签,以提升页面可访问性。
挑战文件中的 seed(初始代码)如下,注意 header 内部第二个子元素就是那个「问题 div」:
<body>
<header>
<h1>Training with Camper Cat</h1>
<div>
<ul>
<li><a href="#stealth">Stealth & Agility</a></li>
<li><a href="#combat">Combat</a></li>
<li><a href="#weapons">Weapons</a></li>
</ul>
</div>
</header>
<main>
<section id="stealth">
<h2>Stealth & Agility Training</h2>
<article><h3>Climb foliage quickly using a minimum spanning tree approach</h3></article>
<article><h3>No training is NP-complete without parkour</h3></article>
</section>
<section id="combat">
<h2>Combat Training</h2>
<article><h3>Dispatch multiple enemies with multithreaded tactics</h3></article>
<article><h3>Goodbye world: 5 proven ways to knock out an opponent</h3></article>
</section>
<section id="weapons">
<h2>Weapons Training</h2>
<article><h3>Swords: the best tool to literally divide and conquer</h3></article>
<article><h3>Breadth-first or depth-first in multi-weapon training?</h3></article>
</section>
</main>
</body>
这段初始代码已经完成了上一道挑战的改造:页面顶部是 header,正文主体是 main,每个训练分类用 section + h2 组织,具体课程条目用 article + h3。唯一残留的无语义容器,就是包裹导航 ul 的那个 div。
参考解答
挑战文件 solutions 部分给出的完整答案,只改动了导航容器这一处——<div> 变为 <nav>,闭合标签同步替换,其余内容原样保留:
<body>
<header>
<h1>Training with Camper Cat</h1>
<nav>
<ul>
<li><a href="#stealth">Stealth & Agility</a></li>
<li><a href="#combat">Combat</a></li>
<li><a href="#weapons">Weapons</a></li>
</ul>
</nav>
</header>
<main>
<section id="stealth">
<h2>Stealth & Agility Training</h2>
<article><h3>Climb foliage quickly using a minimum spanning tree approach</h3></article>
<article><h3>No training is NP-complete without parkour</h3></article>
</section>
<section id="combat">
<h2>Combat Training</h2>
<article><h3>Dispatch multiple enemies with multithreaded tactics</h3></article>
<article><h3>Goodbye world: 5 proven ways to knock out an opponent</h3></article>
</section>
<section id="weapons">
<h2>Weapons Training</h2>
<article><h3>Swords: the best tool to literally divide and conquer</h3></article>
<article><h3>Breadth-first or depth-first in multi-weapon training?</h3></article>
</section>
</main>
</body>
值得注意的结构细节:nav 包在 header 内部,而不是独立于页头。这符合文档「包裹页面主要导航链接」的定位——导航属于页头区块的组成部分,同时 nav 作为独立地标仍可被辅助技术单独定位。
测试断言:如何验证改造达标
挑战 hints 部分实际上就是判定你代码是否通过的 4 条测试断言(基于 Jest/Chai 风格断言,其中 code 是你的原始代码文本,document 是解析后的 DOM)。逐条拆解:
断言 1:页面中应恰好有一个 nav 标签。
assert.lengthOf(document.querySelectorAll('nav'), 1);
querySelectorAll('nav') 在文档中查找所有 nav 元素,长度必须为 1。这既排除了「忘记改」,也排除了「多加了一个 nav」——与文档中「重复的站点链接不必再包 nav」的规则相呼应:多个导航地标会稀释地标导航的意义。
断言 2:nav 必须包裹 ul 及其列表项。
const nav = document.querySelector('nav');
const children = nav?.querySelectorAll(`:scope ${'ul'}`);
assert.lengthOf(children, 1);
:scope 前缀确保 querySelectorAll('ul') 只在 nav 元素内部查找,验证 ul(及三个 li 导航项)确实处于 nav 内部,而不是被挪到了 nav 外面。这保证了地标语义覆盖的是完整导航集合。
断言 3:代码中不能再出现任何 div 标签。
assert.lengthOf(document.querySelectorAll('div'), 0);
这条断言把「替换」锁死为「替换」而非「新增」:不允许在保留 div 的基础上另加 nav。页面此时应当完全由语义元素构成(header、nav、main、section、article、标题与列表)。
断言 4:nav 必须成对闭合。
assert.isTrue(
code.match(/<\/nav>/g)?.length === code.match(/<nav>/g)?.length
);
这条断言直接对原始代码文本做正则匹配,比较 <nav> 开标签与 </nav> 闭标签的数量是否相等。在 HTML 中 nav 是双标签元素(不同于 img、br 等空元素),漏写闭合标签会导致 DOM 树被浏览器隐式修正,地标的语义边界随之不可预期。
规则要点与前后挑战的衔接
把文档内容归纳为三条可复用的实践规则:
- 主要导航用
nav包裹:页面顶部的站点菜单、页内锚点导航等「主导航链接」集合应放进nav,获得内置地标特性; nav不是链接容器:页脚等位置重复出现的站点辅助链接,交给footer地标即可,不要声明第二个nav;- 地标要成对闭合、数量克制:本挑战的 4 条断言(恰好一个
nav、包裹完整ul、零div、开闭标签配对)本质上就是这三条规则的可执行版本。
完成本挑战后,footer 地标挑战 的 seed 代码会直接复用本挑战的解答作为起点:页头中已是 header 包 h1 加 nav 结构,待改造的是页面底部那个包裹 © 2018 Camper Cat 的 div。可以看到课程设计上前一道挑战的 solution 就是后一道挑战的 seed,形成连续演进的页面语义化改造链。
延伸阅读(仓库内路径)
- 挑战本体:587d7788367417b2b2512aa2.md
- 模块结构与挑战顺序:applied-accessibility.json
- 所属课程(superblock)配置:responsive-web-design.json
- 前一道(header 地标):587d7787367417b2b2512aa1.md
- 后一道(footer 地标):587d7788367417b2b2512aa3.md
课程仓库中的挑战文件采用统一的 Markdown 格式组织:frontmatter 声明 id、title、challengeType(本挑战为 0,即经典 HTML/CSS 代码挑战)与 dashedName,正文按 --description--、--instructions--、--hints--、--seed--、--solutions-- 等标记分段,hints 中的 JS 片段即为平台侧的验收测试代码。理解这一格式后,你可以按同样方式在 curriculum/challenges/english/blocks/ 下查阅任意一道挑战的知识点、验收逻辑与参考解答。
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